Book Image

Instant Android Fragmentation Management How-to

By : Gianluca Pacchiella
Book Image

Instant Android Fragmentation Management How-to

By: Gianluca Pacchiella

Overview of this book

There are currently 7 different versions of operating systems for Android. A growing issue is fragmentation. With the number of Android users and the variety of versions available, Android fragmentation is a huge problem. This little book is the solution. Instant Android Fragmentation Management How-to is a step-by-step guide to writing applications that can run on all devices starting from Android 1.6. With simple solutions for complex problems, this book will walk you through the biggest issues facing Android developers today.This book will take you through the newest features in the latest version of Android, and shows you how to utilize them in the older versions using the compatibility library. This practical guide allows you to focus on  creating the best application possible without worrying about compatibility.All the heavy lifting is done for you. Using user interface, adapting your application will work perfectly on any Android operating system. Asynchronous data management will also allow your applications to run smoothly on any device.Everything you need to run your app on any version of Android is right here.
Table of Contents (7 chapters)
Instant Android Fragmentation Management How-to
Credits
About the Author
About the Reviewers
www.PacktPub.com
Preface

Fragments (Should know)


This is the most important section. Here you will learn how to create an Android application designed to be not only backwards compatible with versions down to API level 4, but also capable of showing contents depending on the context. In a phone with normal-size display, it will only show a list (single-paned configuration), but when a larger screen is available, a view with the details of the selection is also displayed (multi-paned configuration).

How to do it...

Let's start creating a simple application composed of a single Activity and two Fragments. One shows a list of items and the second one shows the data related to the selection.

  1. Import all the necessary classes:

    import android.content.*;
    import android.support.v4.app.*;
    import android.view.*;
    import android.widget.*;
    import android.os.Bundle;
  2. Define the Activity that will contain all the code, extending the FragmentActivity class from the Support Library:

    public class FragmentCompatibility extends FragmentActivity {
       ...
    }
  3. Implement its method onCreate(), where we are going to set the initial layout and do what is necessary in order to manage it:

    @Override
    public void onCreate(Bundle savedInstanceState) {
      super.onCreate(savedInstanceState);
      setContentView(R.layout.main);
    
      /*
      * There is the main_container view so we are not in multi paned
      * and we attach the fragment at runtime (we cannot modify lately
      * the fragment organization if it's defined in XML)
      */
      boolean isMultiPaned = (findViewById(R.id.main_container) == null);
      if (!isMultiPaned) {
    
        /*
        * If we are coming from a previous instance we don't
        * have to reattach the SmallListFragment.
        */
        if (savedInstanceState != null) {
          return;
        }
        SmallListFragment slf = new SmallListFragment();
    
        getSupportFragmentManager().beginTransaction()
        .add(R.id.main_container, slf).commit();
      }
    }
  4. Create the Fragment showing the list of primary options using ListFragment:

        public static class SmallListFragment extends ListFragment {
              ….
        }
  5. Implement the onActivityCreate() method for this class, where we set the content of the list:

    @Override
    public void onActivityCreated(Bundle b) {
      super.onActivityCreated(b);
      setListAdapter(
        new ArrayAdapter<String>(getActivity(),
        android.R.layout.simple_list_item_1,
        itemTitleArray
        )
      );
      // First, we need to understand if is multi paned
      mIsMultiPaned = (getActivity().findViewById(R.id.main_container) == null);
    
    }
  6. Implement the onListItemClick() method that shows to the user the selected content updating the adjacent fragment or substituting the list:

    @Override
    public void onListItemClick(ListView l, View v, int position, long id) {
        if (mIsMultiPaned) {
        //mDetail.updateContent(position);
        } else {
          SmallFragment sf = new SmallFragment();
    
          FragmentTransaction transaction =
          getActivity().getSupportFragmentManager().beginTransaction();
          transaction.replace(R.id.main_container, sf);
          transaction.addToBackStack(null);
          transaction.commit();
        }
    
      }
    }
  7. Add the definition of the Fragment that will display the details:

        public static class SmallFragment extends Fragment {
          ...
        }
  8. Implement its onCreateView() method, where we simply deflate a layout file representing the contents of the Fragment:

        public View onCreateView(
            LayoutInflater inflater,
            ViewGroup container,
            Bundle savedInstanceState) {
            View v = inflater.inflate(R.layout.simple, null);
    
            return v;
        }

Now it's time to write the layout files.

  1. Create a file with the path res/layout/main.xml, declaring the single-paned UI:

    <?xml version="1.0" encoding="utf-8"?>
    <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
        android:id="@+id/main_container"
        android:layout_width="match_parent"
        android:layout_height="match_parent" >
        <FrameLayout
            android:id="@+id/detail_container"
            android:layout_width="0dp"
            android:layout_height="match_parent"
        />
    </LinearLayout>
  2. Create a file with the path res/layout-land/main.xml with the multi-paned UI:

    <?xml version="1.0" encoding="utf-8"?>
    <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
        android:orientation="horizontal"
        android:layout_width="match_parent"
        android:layout_height="match_parent" >
        <fragment android:name="org.ktln2.android.packt.FragmentCompatibility$SmallListFragment"
            android:id="@+id/list_fragment"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:layout_weight="1"
            />
        <fragment android:name="org.ktln2.android.packt.FragmentCompatibility$SmallFragment"
            android:id="@+id/detail_fragment"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:layout_weight="1"
            />
     </LinearLayout>

How it works...

The point of the code written in the How to do it... section is to create a simple application capable of adapting its content from the context, and of course, make it launchable from any device with a version of Android starting from API level 4.

This is possible using a specific custom class, made available by the compatibility package, named FragmentActivity; be aware of this, otherwise the Fragment-related stuff won't work properly.

The code creates a single Activity with two Fragments inside. One is a simple list of random items taken from a simple array and the other is a very simple Fragment containing a constant text. The application chooses how to organize the layout using the device's orientation. When the device is in landscape mode, the Fragments are displayed side by side, otherwise we start with the application showing only the list, and then after selecting one item in the list, we switch to the detail replacing the list with the other Fragment.

It's the job of the Activity class to manage the Fragments displayed. There are two ways to manage the Fragments:

  • Statically: By including the Fragment in the XML

  • Dynamically: By loading the Fragment during runtime with FragmentManager

The important point to note here is that a Fragment defined in the XML can't be removed with FragmentManager during runtime, only Fragments loaded dynamically can be removed. This is very important and can result in a very wrong behavior or worse; seems to work correctly, but under the hood it introduces some very nasty bug (for example, pieces of UI that appear multiple times).

Note

A very useful tool is the Hierarchy Viewer, which is included with the SDK. This tool shows the activities in a graphical hierarchical tree while the application is running. The .bat file can be found at <SDK_ROOT\tools\hierarchyviewer.bat>.

Let me explain how Android works and how it saves the state of the UI between state transitions.

A state transition happens when an Activity is paused or destroyed, which can happen quite frequently, for example during a phone call (remember, the Android device may be a phone) or even when the device orientation changes!

This last case may be a surprise when your application appears to be working just fine, but then crashes when the orientation changes.

This is because a change in orientation destroys and rebuilds the UI (almost) from scratch. The system makes the onSaveInstanceState() method available, which is called before an Activity may be killed and passes to it a Bundle instance where we can save all that we think is valuable in order to recreate the actual state. The state can be restored in the onCreate() method where the system will pass back the same Bundle.

The system saves the state of the UI elements for which an ID has been defined, so for example, if we have an EditText method defined into the XML with an associated ID, any text written into it will survive from a state change.

In our code, we have chosen to replace ListFragment with the Fragment containing the detail, but in order to do so, we must create it programmatically from the beginning. But here there is a subtle point; since the container view has an ID associated to it, it will have the ListFragment saved from the previous state. So we must check if we are coming from a previous state and just in case avoid reattaching the Fragment; the code used is the following:

if (savedInstanceState != null) {
    return;
}

If instead we are in portrait mode, without previous instances, we can simply attach ListFragment, using FragmentManager and its methods.

Note

Note that while with the normal Android's API, FragmentManager is returned from getFragmentManager() and the Support Library must be called with getSupportFragmentManager().

In order to understand the remaining code, we must master the Fragments' lifecycle, as shown in the following table:

Fragment

Activity

onAttach()

onCreate()

 

onCreateView()

onCreate()

onActivityCreated()

 

onStart()

onStart()

onResume()

onResume()

onPause()

onPause()

onStop()

onStop()

onDestroyView()

 

onDestroy()

onDestroy()

onDetach()

 

An Activity and its Fragments have a tight relationship; what is more important for us now is the creation time, that is, when the Activity's onCreate() method is called. As stated previously, the Fragment may be directly placed in the XML layout by using a Fragment tag or dynamically loading the Fragment at runtime. In all the cases, the onCreateView() method of the Fragment must return this layout.

Notice that only after the Activity's onCreate() method has returned can we rely on proper initialization of the content view hierarchy. At this point, the Fragment's onActivityCreate() method is called.

There's more...

Now let's talk about some other options, or possibly some pieces of general information that are relevant to this task.

Creating the context adapting interface

When we create the two possible layouts of the Fragments, we choose the landscape and portrait orientation as a switch, but this is not completely the correct approach.

We know well how it is possible to provide various versions of the same resource (layouts, images, and so on) by placing it in a directory whose name is appended with some specific qualifiers that identify the configuration under which the resources must be used (in the previous case, layout-land has been used as the directory's name to indicate as configuration to the device in landscape orientation). The qualifiers can be mixed together, but only in a specific order.

From API level 13 (that is, version 3.2), two new qualifiers are available:

  • w<N>dp: This qualifier specifies a minimum available screen width in dp units at which the resource should be used—defined by the <N> value. This configuration value will change when the orientation changes between landscape and portrait to match the current actual width.

  • h<N>dp: This qualifier specifies a minimum available screen height in dp units at which the resource should be used—defined by the <N> value. This configuration value will change when the orientation changes between landscape and portrait to match the current actual height.

With these qualifiers, it is also possible to use the extended layout on devices that have, for example, the screen width on portrait mode large enough to contain it. If we decide that the switch happens at 600 dp of screen width, we can place our extended layout XML file into a directory named res/layout-w600dp/.

Another trick useful in cases like these is the use of <include> and <merge> tags into your layout. In this way, we can create only one specific layout file and reference it from another if we think it must be equal. If we want to use res/layout-w600dp/main.xml as our real extended layout, we can reference it from res/layout-land/main.xml with the following piece of code:

<?xml version="1.0" encoding="utf-8"?>
<merge>
  <include layout="@layout/skeleton_extended"/>
</merge>

Here we have renamed it to skeleton_extended.xml, the multi-paned layout.

The final words are about managing themes and making them as version-independent as possible. If, for example, we want to use a light theme (the default one is dark) and in particular the Holo theme (a particular theme included in all Android OS starting from Honeycomb that is a compatibility requirement for Android devices running Android 4.0 and forward) with devices with an API level equal or greater than 11, we need to declare our custom theme. Create two directories, one with the path res/values/ and the other named res/values-v11/.

In the first, create the styles.xml file with the following content:

<resources>
    <style
            name="AppTheme"
            parent="android:Theme.Light" />
</resources>

In the other instead write the following content:

<resources>
    <style
            name="AppTheme"
            parent="android:Theme.Holo.Light" />
</resources>

Finally insert the following code line in the AndroidManifest.xml file as an attribute of the <application> tag:

android:theme="@style/AppTheme"

It's important to note that these considerations don't help the backward compatibility directly, but they avoid the loss of possibilities offered from new devices.

Menu

Starting with the Honeycomb version, the way the menu is managed is also different. Because of ActionBar, now it's possible to present some menu options on it so that it becomes easily accessible. The ratio to be used in choosing the options to place in the ActionBar should follow the FIT scheme—Frequent, Important, or Typical.

So the method used for building the menu, that is, OnCreateOptionsMenu(), is called—when an action bar is present—at Activity start (on pre-Honeycomb devices, this function is activated only by pressing the menu button). For example, we can define a simple menu with two options in it, into a file at the res/menu/main.xml path.

<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android">
    <item android:id="@+id/menu_new"
          android:title="New"
          android:showAsAction="ifRoom"/>
    <item android:id="@+id/help"
          android:title="Help" />
</menu>

Since we have indicated the ifRoom value in the showAsAction attribute, this option will be inserted to the right side of the ActionBar (in case there are more options with the same value set, only those that fit into the action bar will be displayed, the others will be shown normally by the menu button).

In pre-Honeycomb devices without ActionBar, all the options will appear normally with the usual menu buttons.

Fragments without UI

Since the Fragments are components at application level and not at UI level, it's possible to instance these without associating layout elements to them.

We do this programmatically with the add(fragment, tag) function.

This is available with an instance of FragmentTransaction. The tag parameter is a normal string (do not confuse this parameter with the tags used in the View class) that can then be used to find the Fragment with the findFragmentByTag() function.

If you are wondering why you would want to use a Fragment with no UI, keep in mind that in this way Fragments are not destroyed when the UI is recreated (such as during orientation changes).

minSdkVersion and targetSdkVersion

Since the devil is in the detail, it is important to understand the role of the variables in the <uses-sdk> tag used in AndroidManifest.xml, which expresses the application's compatibility with one or more versions of the Android platform.

As the meaning of minSdkVersion is rather obvious, let me quote an excerpt from the documentation of targetSdkVersion:

This attribute informs the system that you have tested against the target version and the system should not enable any compatibility behaviors to maintain your app's forward-compatibility with the target version. The application is still able to run on older versions (down to minSdkVersion).

... if the API level of the platform is higher than the version declared by your app's targetSdkVersion, the system may enable compatibility behaviors to ensure that your app continues to work the way you expect. You can disable such compatibility behaviors by specifying targetSdkVersion to match the API level of the platform on which it's running.

In our case, we want to create applications installable from devices with API level 4, and in particular, we want to use capabilities introduced with Honeycomb (that is, API level 11), so finally the AndroidManifest.xml file must contain the following content:

    <uses-sdk
        android:minSdkVersion="4"
        android:targetSdkVersion="11" />

For the Eclipse users, it's possible to set these values initially from the Android projects creation wizard:

The targetSdkVersion is Build SDK as set in the shown dialog.

The maxSdkVersion must be set manually.

Dialog

As you can clearly see in the code, there is a special type of Fragment, a ListFragment, which is a fragment that displays a list of items by binding them to a data source and exposes event handlers when the user selects an item.

Support Library also provides the backward compatible implementation of the FragmentDialog class used to display, obviously, dialog windows. In the documentation, it is explained as follows:

A fragment that displays a dialog window, floating on top of its activity's window. This fragment contains a Dialog object, which it displays as appropriate based on the fragment's state. Control of the dialog (deciding when to show, hide, dismiss it) should be done through the API here, not with direct calls on the dialog.

Let's write some example code in order to show how this is supposed to work:

  1. Import ordinary classes that are used to create a Dialog:

    import android.app.Dialog;
    import android.app.AlertDialog;
  2. Create a class extending FragmentDialog:

        static public class DialogCompatibility extends DialogFragment {
         …
         }
  3. Override the method that is used to create the Dialog:

          @Override
          public Dialog onCreateDialog(Bundle savedInstanceState) {
          return new AlertDialog.Builder(getActivity())
          .setTitle("Fragment and dialog")
          .create();
        }
  4. Add an option in the menu's resource file:

        <item android:id="@+id/menu_dialog"
              android:title="Dialog"
              android:showAsAction="ifRoom"
              />
  5. Finally, add the following code snippet in the Activity class' onOptionsItemSelected() function to call this Dialog:

      @Override
      public boolean onOptionsItemSelected(MenuItem item) {
        switch (item.getItemId()) {
          case R.id.menu_dialog:
          DialogCompatibility dc = new DialogCompatibility();
          DialogCompatibility.newInstance().show(getSupportFragmentManager(), "dialog");
          return true;
          default:
          return super.onOptionsItemSelected(item);
        }
      }

Obviously this is a very simple example and much more could be said, but it's left as an exercise for the reader (as, for example, how to embed a dialog into an Activity).

VERSION_CODES

Not all the possible problems can be addressed with the Support Library, so it is necessary to learn some ways to manage the different availability of features between versions.

One solution could be the creation of different APKs, one for each particular version of Android, and uploading each one separately on the Android market; this is not particularly smart, since it causes a lot of code duplication and is maintainability hell.

A better solution is to create branches in the interested code, using an if statement and checking for VERSION_CODES. This is accessible from the android.os.Build package and it presents an enumeration of all versions of Android. To be able to check for the actual version at runtime, the SDK_INT field must be used in the android.os.Build.VERSION package.

At the end, we should write some code similar to the following:

if (android.os.Build.VERSION.SDK_INT => android.os.Build.VERSION_CODES.HONECOMB) {
   // ...
} else if (android.os.Build.VERSION.SDK_INT =>
android.os.Build.VERSION_CODES.GINGERBREAD){
     // ...
}

A more sophisticated approach would be to use the resource system in order to set appropriate Boolean variables with values of interest. Suppose we create two values files, one with the path res/values/bools.xml and with the following content:

<?xml version="1.0" encoding="utf-8"?>
<resources>
   <bool name="isHoneycomb">false</bool>
</resources>

The other at the path res/values-v11/bools.xml with the following content:

<?xml version="1.0" encoding="utf-8"?>
<resources>
   <bool name="isHoneycomb">true</bool>
</resources>

Inside the code, the isHoneycomb variable can be referenced with a simple piece of code, as shown as follows:

Resource r = getResources();
boolean isHoneycomb = r.getBoolean(R.bool.isHoneycomb)

This can be used directly in the code.