Wednesday, February 09, 2011

Strategies for Honeycomb and Backwards Compatibility

Each new Android release heralds two things: A raft of new developer APIs, and a chorus of questions on how to use them while staying backwards compatible.

Android 3.0 (Honeycomb) is notable in that it introduces Fragments, possibly the most fundamental change to how Activity UIs are constructed since Android 1.0. To add to the excitement, Honeycomb is also the first Android platform release optimized specifically for extra-large screen (tablet) devices.

The good news is that we plan to have the same fragment APIs available as a static library for use with older versions of Android; the plan is to go right back to 1.6.

The easiest short-term fix is to create separate sets of Activities
When the static Fragments library becomes available you'll be able to skip this step entirely (or simply deprecate these Activities from your application). 
The introduction of Fragments (and to a lesser degree the Action Bar) represents a significant change to the code the lives within your Activity classes.

The following technique demonstrates how you can  create two separate sets of Activities: one set that supports Fragments and another set that is designed to work without them.

To select the right set of Activities at runtime, you need to include a launcher Activity in your manifest that detects support (or lack thereof) for Fragments and then starts the appropriate Activity.

<activity
  android:name="InitialActivity"
  android:label="@string/app_name">
  <intent-filter>
    <action android:name="android.intent.action.MAIN" />
    <category android:name="android.intent.category.LAUNCHER" />
  </intent-filter>
</activity>


Within InitialActivity, you can use reflection to check if Fragments are supported on the current device.

  private static boolean fragmentsSupported = false;

  private static void checkFragmentsSupported() throws NoClassDefFoundError {
    fragmentsSupported = android.app.Fragment.class != null;
  }

  static {
    try {
      checkFragmentsSupported();
    } catch (NoClassDefFoundError e) {
      fragmentsSupported = false;
    }
  }


Then within onCreate forward the application to the "real" first Activity that either uses Fragments or not depending on the capabilities of the device.

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

    Intent startActivityIntent = null;
    if (!fragmentsSupported)
      startActivityIntent = new Intent(this, MainNonFragmentActivity.class);
    else
      startActivityIntent = new Intent(this, MainFragmentActivity.class);

    startActivity(startActivityIntent);
    finish();
  }
}


Because you're calling finish immediately after starting the new Activity, the back button behaviour of your application works as expected.

Within the MainNonFragmentActivity you simply inflate a layout resource that doesn't use Fragments

setContentView(R.layout.non_fragment_main);

While the MainFragmentActivity inflates a layout that does.

What about the rest of the Honeycomb APIs?

The upcoming static Fragment library means you probably won't need to implement the previous technique in order to use Fragments as the building blocks of your UI, but what about the other new Honeycomb APIs like the Action Bar and Animators?

Let's ignore the non-Fragment Activities and focus on the scenario where Fragments are available, but none of the other Honeycomb APIs are.

The first step is to determine the availability of the new APIs. You can implement the same exception catching technique I used above for each of the classes you wish to use, but I've found it simpler to bundle all the new APIs together and build only two variations:

  private static boolean shinyNewAPIsSupported = 
    android.os.Build.VERSION.SDK_INT > 10;

Maintaining two sets of Activities doesn't require a parallel set of Fragments, layouts and resources

Once again, I'm going to create a parallel set of Activities: One that uses the shiny new APIs like the Action Bar and animations, and another that uses traditional techniques to create a similar user experience.

The technique described above can be used in the same way - this time using the shinyNewAPIsSupported variable to determine which Activity to start.

Because most of the user-interface logic is contained within the Fragments rather than the Activity, these parallel Activities don't do much beyond inflate slightly different layouts. Where the Action Bar is available the Activity will handle the effect of navigation and action clicks. If the Action Bar isn't available, the layout used will likely incorporate a custom version that mimics its behavior.

Only the highlighted ActionBarFragment in the following snippet would be different between the pre- and post-Honeycomb app layout.

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
  android:orientation="vertical"
  android:layout_width="fill_parent"
  android:layout_height="fill_parent">
  <fragment class="com.paad.hc_backwards.ActionBarFragment"
    android:layout_width="fill_parent" 
    android:layout_height="wrap_content"/> 
  <LinearLayout
    android:layout_width="fill_parent"
    android:layout_height="fill_parent">
    <fragment class="com.paad.hc_backwards.SelectionFragment"
      android:layout_weight="3"
      android:layout_width="fill_parent"
      android:layout_height="fill_parent"/>
    <fragment class="com.paad.hc_backwards.ContentFragment"
      android:layout_weight="1"
      android:layout_width="fill_parent"
      android:layout_height="fill_parent"/>
  </LinearLayout>
</LinearLayout>


The key is that both sets of Activities will use the same fragments. Interaction between and within Fragments is usually maintained within each fragment, so only code related to the Action Bar (and other missing APIs) will need to be changed within the two Activity sets.

I expect all the apps I use on my phone to be optimized for tablets

Now lets take a look at the changes inherent in the introduction of the extra-large display. Fragments are designed specifically to make it easier to create flexible layouts, so I'm going to assume the existence of either Honeycomb or the static Fragment library when considering our options.

Device independent pixels (dp) and flexible layouts (like Relative Layout and Linear Layout) let your app scale to the dimensions and orientation of tablets. However, an app running on a 10" landscape device 1280 pixels across has a hell of a lot of whitespace when it was designed for a 4" portrait display at 480 pixels.

Consider the typical app design pattern where making a selection on one Activity determines what content is displayed another. Making a selection hides the selection Activity and displays the selected content.

Using Fragments you can easily construct layout variations for different screens

On a tablet it makes sense to position the selection and content Fragments within a single Activity, while on a phone only one should be visible at a time.

You can handle this as we did pre-Honeycomb by creating a res/layout-xlarge resource folder into which we put the side-by-side layout to use on an extra-large (tablet display).

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
  android:layout_width="fill_parent"
  android:layout_height="fill_parent">
  <fragment class="com.paad.hc_backwards.SelectionFragment"
    android:id="@+id/selection_fragment"
    android:layout_weight="3"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent"
  />
  <fragment class="com.paad.hc_backwards.ContentFragment"
    android:id="@+id/content_fragment"
    android:layout_weight="1"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent">
  </fragment>
</LinearLayout>


In practice, we'll usually want to define a portrait and landscape specific mode using port or land resource qualifiers:

res/layout-xlarge-port
res/layout-xlarge-land

For phones, we'll use the same Fragments, but the layout (stored in res/layout) would contain only one Fragment stored in a containing layout.

<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
  android:id="@+id/selection_fragment_frame"
  android:layout_width="fill_parent"
  android:layout_height="fill_parent">
  <fragment class="com.paad.hc_backwards.SelectionFragment"
    android:id="@+id/selection_fragment"
    android:tag="full_screen_fragment"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent"
  />
</FrameLayout>


Within our code this produces a dilemma

The selection and content Fragments will interact slightly differently in phone or tablet mode. In both cases, making a selection should be reflected in the content Fragment - however in the phone layout this should also replace the selection Fragment with the newly updated content Fragment.

public void makeSelection(int i) {
  boolean extraLargeScreen = getResources().getConfiguration().screenLayout >
                            Configuration.SCREENLAYOUT_SIZE_LARGE;
  if (!extraLargeScreen) {
    FragmentTransaction ft = fragmentManager.beginTransaction();
    ft.hide(selectionFragment);
    ft.add(R.id.selection_fragment_frame, contentFragment);
    ft.addToBackStack(null);
    ft.commit();
  }
  contentFragment.setSelection(i);
}


Note that by calling addToBackStack we ensure that pressing the back button will undo this transaction, hiding the content Fragment and displaying the selection Fragment.

Fragments are a great way to componetize your UI

The new Android 3.0 (Honeycomb) SDK introduces a number of great new APIS, many of them designed to make it easier to build tablet version of your apps by providing a flexible mechanism for building different layouts depending on screen size and orientation. The availability of a static Fragment library will simplify the process of creating flexible layouts that are backwards compatible.

It is my firm belief that any app developed for mobile should be available for tablets. When I put my credentials into a shiny new tablet, I expect it to install all the apps I use on my phone - and I expect all of them to be optimized for the tablet form factor.

That's not to say you shouldn't create apps that are designed to work only on tablets, but I believe these tablet-specific editions should be made available side-by-side with phone versions that have been optimized to work on tablets.

17 comments:

  1. So lets say I want to use a image as the background to one of my activities.
    I have to crop the image at least a half dozen ways to support different screen resolutions and orientations.
    This quickly adds bytes to my apk especially if I want to support high res tablets.
    I run out of space for apps all the time on my
    Nexus One.
    I know it supports moving apps to SD now, but its that a complete solution?

    In this scenario should I make one large app that supports both tablets and phones or should I just have 2 apps?

    ReplyDelete
  2. Anonymous7:26 p.m. GMT

    I'm in Germany - and I can't even officially buy an Android 2.3 device here. The emulator for 3.0 is so slow it's not even funny anymore.

    Sorry, but for the time being I'll continue writing for 2.2 (Nexus One & Galaxy Tab).

    ReplyDelete
  3. Can an Android 1.6 -device even inflate the layout-xml if that contains fragments?
    It certainly does NOT work for animations that are not supported on older APIs even if they are not used on the current device.

    ReplyDelete
  4. Anonymous8:52 p.m. GMT

    ^ What Anonymous said.

    ReplyDelete
  5. Agree with buddy from Germany @7:26 : It is a shame the 2.3 is still under 1% in penetration and only available in two countries and Honeycomb emulator is a joke...Google really needs to figure out how to provide real emulated development environment.

    ReplyDelete
  6. Just seen the new HP babies...makes me wonder is Mathias Duarte is still working with them, or who created that sorry UI of Honeycomb. webOS looks sooo much better.

    ReplyDelete
  7. Will the Android Market be able to show screenshots of the tablet-optimised and phone-optimised versions? (An interface could change significantly for a 2"phone, 7"tablet, 10"tablet and 50"HDTV.)

    I trust the Google maps API will be updated with MapFragments at some point? Do you intend to release the updated source code for Earthquake? Seems like a good example of how to use a MapFragment.

    If those fools at Motorola decide to charge $800 for the XOOM then they deserve not to have any tablet optimised apps. Unless of course they kindly giveaway some tablets to developers for free. Hint, hint. :)

    ReplyDelete
  8. Anonymous8:45 a.m. GMT

    I still miss the (for me) most important information: As long as there's no fragment library, or if I want to save the KBs the library will need, do I have to code both Activity and Fragment (and use different XML layouts) for e.g. a list? Or is there a way to reuse the same code (except with workarounds like external helper classes)?

    ReplyDelete
  9. @Marcus: No. What you would do is create the "content layout" that contains the actual UI that you want to inflate within the Fragment. Then you have two "container layouts" -- one that includes the Fragment, and another that uses tags to pull the content layout(s) in.

    Within your code you inflate whichever container layout is appropriate -- the one with Fragments or the one without.

    ReplyDelete
  10. @Anonymous (last one): You can use the same underlying layouts to define the various Views that make up the UI, but you'll need different Activities and different "container layouts" that package those underlying layouts either within Fragments or more traditional layouts.

    ReplyDelete
  11. Reto Meier:
    It's about the ability of older Androids to tolerate such a second layout as long as it's not used.
    As I said, that does not work for animations that are not used = not referenced from the layout used on the older device. They would simply force close the entire application instead.

    So: Has this approach been tried and verified to work flawlessly on Android 1.5-3.0?

    ReplyDelete
  12. It seems quite complicated for now to manage both APIs. As some others said, I will wait a bit before doing some smartphone/tablet compatible development. Looking forward for fragment/action bar library and waiting on official 3.0 device available in my country (Switzerland). Maybe Google will send us some tablet before the Google IO event and we could develop on them :-D

    ReplyDelete
  13. Reto - you said:

    "When I put my credentials into a shiny new tablet, I expect it to install all the apps I use on my phone"

    This is the first time that we expect one user to be running two Android devices (phone & tablet) with the same data, which raises the interesting question of syncing their application data & settings between the devices.

    How do you see that working ? Is there a recommended approach to the problem ?

    ReplyDelete
  14. Mmm, though the introduction of Fragment seems very interesting, maintening both versions (w or wo Fragment) seems to me overkilling for the moment. yes it is okay in a simple hello world tutorial, but in a real application, this is another story. Not even saying this increases the combination of tests to do.

    One comment: using VERSION.SDK_INT crashes 1.5 device... we cannot rely on that without doing yet another is_XYZ_supported() trick. worth mentionning in an article related to compatibility ;-) (still have 5% of my users in 1.5, and will probably support that platform for months)

    Anyway, limitation from older versions should not stop the progress of new version of Android. Thanks very much for giving such valuable examples :-)

    ReplyDelete
  15. Hi
    I tested fragmentsSupported = android.app.Fragment.class != null;
    but you get java.lang.VerifyError. I Tried this on 1.5 and 1.6
    I dunno if reflections work.
    I tried to catch it with NoClassDefFoundError, This failed then I tried general Exception and this failed too.
    Also as tl said:
    Integer.parseInt(Build.VERSION.SDK) > 10
    Is better then SDK_INT cuz I want to support 100 % of my customers.
    Even though it is deprecated

    ReplyDelete
  16. Just what I was looking for. Really nice article, thank you.

    ReplyDelete
  17. A couple small comments on your sample:

    (1) I strongly recommend against using reflection to find out if an API is available. It is safer to check the API version. It is possible for someone to build an older version of the platform where they have back-ported some features from a newer one. In that case, you would mistakenly think that the full newer feature is available, when it may not be.

    (2) This is the best way to test against running on at least a certain platform version:

    private static boolean shinyNewAPIsSupported =
    android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.HONEYCOMB;

    These version codes are static final int constants, so the actual value is built in to your app; this code will work on any platform version, it isn't actually retrieving the HONEYCOMB constant at run time.

    ReplyDelete