Thursday, November 28, 2013

One code fits all (screens)

Hello,

After my Hebrew blog posts started to get published on the wonderful iAndroid website i decided to transform this blog into the English edition.
Slowly but surely i'll translate my earlier posts to English but i wanted its debut will be with a fresh post so here it is, i hope you will like it. 

For those of you who don't know, all of my posts until now (including this one) or derived from AndconLab; the developer code lab that  +Ran Nachmany+Amir Lazarovich and myself created last year for the great MultiScreenX convention we helped organize.

Motivation

Ladies and gentlemen, i'm proud to announce the demise of Android as a mobile phone OS and i am more than proud to announce the birth of Android as an all round OS.
How can i say that?
Simply!
Most of your potential clients have Android running on screens that range from the size of 2" (a smartwatch) all the way to 100" (a T.V. set) and from QHD to 4K.
In the past you could have dealt with this kind of situation in one of three ways:
  1. Ignore the issue (as most of you did).
  2. Create a vast and complicated mechanism to figure out and adjust your layouts dynamically at runtime.
  3. Create different apps per device and use the Google Play to slice up the market.

All of the above had great disadvantages and when Google figured that out (2 years ago) it created a better way, called Fragments, to adjust yourself to any situation with grace, ease and not a lot of coding. 

How does it work?

Android has always "known" what the device's screen size and resolution is but until Honeycomb (3.X, which was made specifically for tablets) it had a limited use "out of the box", the smallest Android unit was the Activity which had a .java implementation and an .xml layout and it symbolized 1 full screen of the device (no matter what size it was).
The concept of Fragments is that it's an implementation until which is smaller or equal to the activity in terms of screen usage and that it has a dynamic manager which can push or pop Fragments dynamically from an Activity so an Activity could display one or more Fragments at the same time while adjusting to any screen with a low memory and runtime overhead and with the minimum amount of development.

Current market state (why are you not using it?!)

Warning: the author of this post is about to use name dropping to make himself look important, please disregard that :)

I recently met Mr. Reto Meier who claimed that developers categorize most tasks into two categories:
  1. "This will take less than a minute"
  2. "This will take forever".
He continued to claim that most people will put off items from category #2 until they will check-off all the items in category #2 but category #1 will never be empty so they will only do an item from #2 when it's absolutely impossible to continue without it.
Sounds familiar?
The only problem with that is that he argued that the real-life definitions of the aforementioned categories are closer to these:  
  1. "This will take roughly 30 minutes".
  2. "This will be longer than 30 minutes".
which would implies that:
     a. forever = more than 30 minutes :)
     b. tasks that seem longer than half an hour to execute will never get done.
Now, given that most tasks that will improve your product in a big way will probably take more than 30 minutes would suggest that a rational developer will do these ASAP rather than putting them off for later, right?
Wrong?
Even if you're reading this while mumbling to yourselves, "what is this non-sense?",at least you're thinking about it so that's good.

Step 0 - What are we starting with?

A classic example of a ListView Activity:

public class MainActivity extends SherlockActivity implements OnItemClickListener{

 private ListView mList;
 private ProgressDialog mProgressDialog;
 private BroadcastReceiver mUpdateReceiver;

 @Override
 protected void onCreate(Bundle savedInstanceState) {
  super.onCreate(savedInstanceState);

  setContentView(R.layout.main_activity);
  mList = (ListView) findViewById(R.id.list);

  mList.setOnItemClickListener(this);
 }

 @Override
 protected void onPause() {
  super.onPause();
  if (null != mUpdateReceiver) {
   unregisterReceiver(mUpdateReceiver);
   mUpdateReceiver = null;
  }
 }

 @Override
 protected void onResume() {
  super.onResume();
        mUpdateReceiver = new BroadcastReceiver() {
            //TODO: [Ran] handle network failure
            @Override
            public void onReceive(Context context, Intent intent) {
                if (intent.getAction().equalsIgnoreCase(CommunicationService.RESULTS_ARE_IN)) {
                    new lecturesLoader().execute((Void) null);
                }

                if (null != mProgressDialog)
                    mProgressDialog.dismiss();
            }
        };

        final IntentFilter filter = new IntentFilter();
        filter.addAction(CommunicationService.RESULTS_ARE_IN);
  registerReceiver(mUpdateReceiver, filter);

  new lecturesLoader().execute((Void)null);
 }

 @Override
 public void onItemClick(AdapterView list, View view, int position, long id) {
  Intent i = new Intent(this,SingleLectureActivity.class);
  i.putExtra(SingleLectureActivity.EXTRA_LECTURE_ID, id);
  startActivity(i);
 }

    @Override
    public boolean onCreateOptionsMenu(Menu menu) {
        getSupportMenuInflater().inflate(R.menu.main, menu);
        return true;
    }

    @Override
    public boolean onOptionsItemSelected(MenuItem item) {
        switch (item.getItemId()) {
            case R.id.action_refresh:
                refreshList(true);
                return true;

            default:
                return super.onOptionsItemSelected(item);
        }
    }

 private void refreshList(boolean showProgressbar) {
  if (showProgressbar) {
   mProgressDialog = ProgressDialog.show(this, getString(R.string.progress_dialog_starting_title), getString(R.string.progress_dialog_starting_message));
  }

  Intent i = new Intent(this,CommunicationService.class);
  startService(i);
//  ServerCommunicationManager.getInstance(getApplicationContext()).startSearch("Android", 1);
 }

 ////////////////////////////////
 // Async task that queries the DB in background
 ////////////////////////////////
 private class lecturesLoader extends AsyncTask {
  private SQLiteDatabase db;

  @Override
  protected Cursor doInBackground(Void... params) {
   db = new DatabaseHelper(MainActivity.this.getApplicationContext(), DatabaseHelper.DB_NAME,null , DatabaseHelper.DB_VERSION).getReadableDatabase();
   return DBUtils.getAllLectures(db);
  }

  @Override
  protected void onPostExecute(Cursor result) {
   if (0 == result.getCount()) {
    //we don't have anything in our DB, force network refresh
    refreshList(true);
   }
   else {
    LecturesAdapter adapter = (LecturesAdapter) mList.getAdapter();
    if (null == adapter) {
     adapter = new LecturesAdapter(MainActivity.this.getApplicationContext(), result);
     mList.setAdapter(adapter);
    }
    else {
     adapter.changeCursor(result);
    }
   }
   db.close();
  }
 }
}
The layout file which is called main_activity.xml looks like this (simple and familiar, right?) :

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:paddingBottom="@dimen/activity_vertical_margin"
    android:paddingTop="@dimen/activity_vertical_margin"
    tools:context=".EventsListActivity" >

    <ListView
        android:id="@+id/list"
        android:layout_width="fill_parent"
        android:layout_height="fill_parent"
        android:cacheColorHint="#000000"
        android:divider="@color/divider_color"
        android:dividerHeight="1dp"
        tools:listitem="@layout/event_list_item" >

    </ListView>

</RelativeLayout>

Step 1 - From Activity to FragmetActivity

Replace this line:
public class MainActivity extends SherlockActivity implements OnItemClickListener
With this line:
public class MainActivity extends SherlockFragmentActivity implements LecturesListFragment.callback{
Which also means that the familiar OnItemClickListener is replaced with the following code:

////////////////////////////
 // Fragment interface
 ///////////////////////////
 @Override
 public void onLectureClicked(long lectureId) {
  if (mTwoPanes) {
   SingleLectureFragment f = new SingleLectureFragment();
   Bundle b = new Bundle();
   b.putLong(SingleLectureFragment.LECTURE_ID, lectureId);
   f.setArguments(b);
   getSupportFragmentManager().beginTransaction().replace(R.id.lecture_details_container, f).commit();
  }
  else {
   Intent i = new Intent(this,SingleLectureActivity.class);
   i.putExtra(SingleLectureActivity.EXTRA_LECTURE_ID, lectureId);
   startActivity(i);
  }
 }

 @Override
 public void fetchLecturesFromServer() {
  Intent i = new Intent(this,CommunicationService.class);
  startService(i);
  mProgressDialog = ProgressDialog.show(this, getString(R.string.progress_dialog_starting_title), getString(R.string.progress_dialog_starting_message));
 }
Please focus at the first method which has a naive implementation of an interesting concept which is that there are some scenarios in which we would like to force a single Fragment display even if there's room and definition of a 2-Fragment display, that if clause distinguishes between the 2 possible scenarios to determine 2 possible courses of action:


  • start a new Activity (same as always).
  • use the FragmentManager to perform an inter-fragment operation (in this case; replace).

  • That implicit freedom is the basis to why i like the concept of Fragments so much.

    Step 2 - Creating the Fragment.

    The aforementioned LectureListFragment looks like this:
    
    public class LecturesListFragment extends SherlockFragment implements OnItemClickListener{
    
     private ListView mList;
     private View mRootView;
     private BroadcastReceiver mUpdateReceiver;
     private callbacks mListener;
    
     public interface callbacks {
      public void onLectureClicked(long lectureId);
      public void fetchLecturesFromServer();
     }
    
     @Override
     public void onCreate(Bundle savedInstanceState) {
      super.onCreate(savedInstanceState);
    
      mUpdateReceiver = new BroadcastReceiver() {
       //TODO: [Ran] handle network failure
       @Override
       public void onReceive(Context context, Intent intent) {
        if (intent.getAction().equalsIgnoreCase(CommunicationService.RESULTS_ARE_IN)) {
         reloadLecturesFromDb();
        }
       }
      };
     }
    
     @Override
     public View onCreateView(LayoutInflater inflater, ViewGroup container,
       Bundle savedInstanceState) {
    
      mRootView = inflater.inflate(R.layout.single_list_layout, null);
      mList = (ListView) mRootView.findViewById(R.id.list);
      mList.setOnItemClickListener(this);
    
      reloadLecturesFromDb();
    
      return mRootView;
     }
    
     @Override
     public void onAttach(Activity activity) {
      super.onAttach(activity);
      if (!(activity instanceof callbacks)) {
       throw new IllegalStateException("Activity must implement callback interface in order to use this fragment");
      }
      mListener = (callbacks) activity;
     }
    
     @Override
     public void onPause() {
      super.onPause();
      if (null != mUpdateReceiver) {
       getActivity().unregisterReceiver(mUpdateReceiver);
      }
     }
    
     @Override
     public void onResume() {
      super.onResume();
      if (null != mUpdateReceiver) {
       IntentFilter filter = new IntentFilter();
       filter.addAction(CommunicationService.RESULTS_ARE_IN);
       getActivity().registerReceiver(mUpdateReceiver, filter);
      }
     }
    
     @Override
     public void onItemClick(AdapterView list, View view, int position, long id) {
      if (null != mListener) {
       mListener.onLectureClicked(id);
      }
     }
    
     public void reloadLecturesFromDb() {
      new lecturesLoader().execute((Void) null);
     }
    
     ////////////////////////////////
     // Async task that queries the DB in background
     ////////////////////////////////
     private class lecturesLoader extends AsyncTask {
      private SQLiteDatabase db;
    
      @Override
      protected Cursor doInBackground(Void... params) {
       db = new DatabaseHelper(getActivity().getApplicationContext(), DatabaseHelper.DB_NAME,null , DatabaseHelper.DB_VERSION).getReadableDatabase();
       return DBUtils.getAllLectures(db);
      }
    
      @Override
      protected void onPostExecute(Cursor result) {
       if (0 == result.getCount()) {
        //we don't have anything in our DB, force network refresh
        if (null != mListener) {
         mListener.fetchLecturesFromServer();
        }
       }
       else {
        LecturesAdapter adapter = (LecturesAdapter) mList.getAdapter();
        if (null == adapter) {
         adapter = new LecturesAdapter(getActivity().getApplicationContext(), result);
         mList.setAdapter(adapter);
        }
        else {
         adapter.changeCursor(result);
        }
       }
    
       db.close();
      }
     }
    
    }
    
    
    Please notice that the LectureLoader class has migrated from the MainActivity to here and that we have new (and exciting) life-cycle events to use in order to intercept events and modify our behavior accordingly.
    The Fragment's layout looks like this (suspiciously close to the old main_activity.xml):
    
    
    <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:tools="http://schemas.android.com/tools"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:paddingBottom="@dimen/activity_vertical_margin"
        android:paddingTop="@dimen/activity_vertical_margin"
        tools:context=".EventsListActivity" >
    
        <ListView
            android:id="@+id/list"
            android:layout_width="fill_parent"
            android:layout_height="fill_parent"
            android:cacheColorHint="#000000"
            android:divider="@color/divider_color"
            android:dividerHeight="1dp"
            tools:listitem="@layout/event_list_item" >
    
        </ListView>
    
    </RelativeLayout>
    The change is in the main_activity.xml which looks like this by default:
    
    
    <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:tools="http://schemas.android.com/tools"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:paddingBottom="@dimen/activity_vertical_margin"
        android:paddingTop="@dimen/activity_vertical_margin"
        android:background="@color/LecturesListBackground"
        tools:context=".EventsListActivity" >
    
        <fragment
            android:name="com.gdg.andconlab.ui.LecturesListFragment"
            android:layout_width="fill_parent"
            android:layout_height="fill_parent"
            android:id="@+id/lectures_fragment"
            />
    
    </RelativeLayout>
    And when the device has a screen with more than 600dp (under the res/layout-sw600dp) will look like this (automatically):
    
    
    <?xml version="1.0" encoding="utf-8"?>
    <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="horizontal" >
    
        <fragment
            android:id="@+id/lectures_fragment"
            android:name="com.gdg.andconlab.ui.LecturesListFragment"
            android:layout_width="fill_parent"
            android:layout_height="fill_parent"
            android:layout_weight="3" />
    
        <LinearLayout
            android:id="@+id/lecture_details_container"
            android:layout_width="fill_parent"
            android:layout_height="fill_parent"
            android:layout_weight="2"
            >
    
        </LinearLayout>
    
    </LinearLayout>
    And... that's it, i hope you noticed that the "scary" immigration to Fragments is not that scary at all and that you've understood the enormous benefit that you get from it.

    * This post is also available in Hebrew herehttp://iandroid.co.il/dr-iandroid/archives/15644
    Royi is a Google Developer Expert for Android in 2013, a mentor at Google's CampusTLV for Android and (last but not least) the set top box team leader at Vidmind, an OTT TV and Video Platform Provider. www.vidmind.com

    No comments:

    Post a Comment