Multi-platform fragments, Part II

In Part I I describe a pretty well-known way of using resources to make an application look good on a variety of devices.  It works pretty well as far as it goes, but 20% of Android users out there are still running Eclair, and some of them are using HVGA screens.  For them we’ll need, reluctantly, to back away from fragments.  It turns out that that isn’t so bad either.

First move the existing layout directories, from Part I, “layout” and “layout-port” so that they apply only to large screens.  Rename them “layout-large” and “layout-large-port”.  The application will continue to behave as it did in Part 1, as long as the device screen qualifies as “large”.   BTW, here’s some handy code that will let you know what kind of screen Android thinks it is dealing with:

Configuration config = getResources().getConfiguration();

Log.d(TAG, "Orientation: " + config.orientation);

Log.d("TAG, "Size: " + (config.screenLayout & Configuration.SCREENLAYOUT_SIZE_MASK));

Here’s a new version of the main layout.  Put it in “layout”, the default for normal (and small) platforms:

<?xml version="1.0" encoding="utf-8"?>

<LinearLayout

xmlns:android="http://schemas.android.com/apk/res/android"

android:orientation="vertical"

android:layout_width="fill_parent"

android:layout_height="fill_parent"

>

 

<ListView

android:id="@+id/contacts"

android:layout_width="fill_parent"

android:layout_height="fill_parent"

android:layout_weight="2"

/>

</LinearLayout>

The trick is that this version is completely missing the FrameLayout that the previous versions used to position the fragment. Here’s a new version of onCreate that determines whether to use fragments, or not:

@Override

public void onCreate(Bundle state) {

super.onCreate(state);

 

setContentView(R.layout.main);

 

final boolean useFrag

= null != findViewById(R.id.contact_detail);

 

if (useFrag) { installFragment(); }

 

//...

}

It only installs the visible fragment showing the contact details if there is a place to put it.  On small screens, then, no fragment will get created.

There’s just one more piece: what to do when there’s a click in the list of contacts.  In the old version we created a new fragment and put it on the stack.  In this version, though, we either stack a fragment or a new activity, depending on whether there’s a place to put the fragment or not.  The activity’s onClickHandler calls launchDetails to navigate to the contact details, how ever they are represented in the UI.  It looks like this:

void launchDetails(int pos, boolean useFrag) {

//... get the name and id from a cursor

 

if (useFrag) { stackFragment(id, name); }

else { stackActivity(id, name); }

}


private void stackFragment(String id, String name) {

FragmentTransaction xact

= getSupportFragmentManager().beginTransaction();


xact.replace(

R.id.contact_detail,

ContactDetailFragment.newInstance(id, name),

FRAG_TAG);

xact.addToBackStack(null);

xact.setTransition(FragmentTransaction.TRANSIT_FRAGMENT_OPEN);

xact.commit();

}


private void stackActivity(String id, String name) {

Intent intent = new Intent();

intent.setClass(this, ContactDetailActivity.class);

intent.putExtra(ContactDetails.TAG_ID, id);

intent.putExtra(ContactDetails.TAG_CONTACT, name);

startActivity(intent);

}

That’s it!  When this application runs on a small screen, it will launch a new activity into the back-stack, instead of a new fragment.  It looks like this:

No frags1 No frags2

This idea comes from one of Diane Hackborne’s posts on the Android Developer’s blog.  It’s so tasty, though that it bears repeating.  The rest of the docs for fragments are here.

There are a couple of other things about this app that are worthy of note.  First, as is, it will treat an xlarge screen like a small or a normal one.  That’s probably not right.  Also, I suppose, how it is that the demo has an Activity and a Fragment with the same behavior could use a little explanation.

You can find all the code for this demo application here.

Advertisements

Multi-platform fragments, Part I

This is going to be a bit long.  I’ve been away from Android for a while, mostly looking at embedded Linux and Meego… but that’s another story. I’ve been meaning to write about some clever tricks for building applications that can work on a wide variety of platforms.  I’ll do it in two posts.

The advent of tablets/slates/pads, brings the problem of making an application’s UI look good on a wide variety of devices to a whole new level.  Until Honeycomb, with device independent pixels and a couple of fine tuned bitmaps, you could probably cover most of the devices out there.  Now that Fragments have arrived, an app may actually have substantially different behaviours on different platforms.  On a small screen it may show only a single window at a time and use the back button for navigation.  On a larger screen, however, it might work need multiple windows to make use of the space.  Sounds like a nightmare…

It turns out that it isn’t so bad.  A little experimentation and a close read of the docs reveal couple of cute tricks will contain the whole issue in just a few lines of code.

First off, use the ACL (Android Compatibility Library).  An app that is based on Fragments, without the ACL cannot run on pre-Honeycomb Android.  Game over.  In order to support a wide variety of platforms, you’ve got to code to the ACL.

To use it, you just copy it from its home:

$ANDROID_SDK/extras/android/compatibility/v4/android-support-v4.jar

… to a directory named “lib” in your project 9and add it to your Eclipse build path).

Consider an example app that displays information about your Contacts. It uses fragments and is meant for a tablet in landscape orientation Here’s its layout, the file “main.xml” in the directory “res/layout”.

<?xml version="1.0" encoding="utf-8"?>

<LinearLayout

xmlns:android="http://schemas.android.com/apk/res/android"

android:orientation="horizontal"

android:layout_width="fill_parent"

android:layout_height="fill_parent"

>

<ListView

android:id="@+id/contacts"

android:layout_width="0dp"

android:layout_height="fill_parent"

android:layout_weight="1"

/>

<FrameLayout

android:id="@+id/contact_detail"

android:layout_width="0dp"

android:layout_height="fill_parent"

android:layout_weight="2"

android:background="@color/blue"

/>

 </LinearLayout>

The FrameLayout will be replaced by a fragment, in the code.  That looks like this:

public class ContactViewer extends FragmentActivity {

private static final String FRAG_TAG

= ContactViewer.class.getCanonicalName() + ".fragment";

public void onCreate(Bundle state) {

super.onCreate(state);


setContentView(R.layout.main);

 

installFragment();

 

// ...

}


private void installFragment() {

FragmentManager fragMgr = getSupportFragmentManager();

 

if (null != fragMgr.findFragmentByTag(FRAG_TAG)) { return; }

  FragmentTransaction xact = fragMgr.beginTransaction();

xact.add(

R.id.contact_detail,

ContactDetailFragment.newInstance(null, null),

FRAG_TAG);

xact.commit();

}

Pretty straighforward fragment code.  ContactDetailFragment is the fragment class and R.id.contact_detail is where it goes, the FrameLayout.  I’ve mentioned, previously, using fragment’s tagging facility to prevent leaking them.

If you run this on a tablet, landscape WXGA, it looks pretty good:

Landscape

Running it on a phone, in portrait WVGA800, is another story:

Smooshed landscape

The screen is acutually still big enough to support two windows but the proportions have to be different and they have to be layed out vertically.  This turns out to be dead simple.  In the “res” directory, create a new sub-directory named “layout-port”, next to the original “layout”.  Copy “main.xml” into it and reorient it for the smaller portrait screen:

<?xml version="1.0" encoding="utf-8"?>

<LinearLayout

xmlns:android="http://schemas.android.com/apk/res/android"

android:orientation="vertical"

android:layout_width="fill_parent"

android:layout_height="fill_parent"

>

<ListView

android:id="@+id/contacts"

android:layout_width="fill_parent"

android:layout_height="0dp"

android:layout_weight="2"

/>

 

<FrameLayout

android:id="@+id/contact_detail"

android:layout_width="fill_parent"

android:layout_height="0dp"

android:layout_weight="1"

android:background="@color/blue"

/>

</LinearLayout>

We might copy “contact_detail.xml” over as well and tweak font sizes and such a little so that everything looks nice.  With no code changes the app UI will now look pretty good on a wide variety of screens — including the Honeycomb tablet rotated to portrait.

Portrait

The Android system allows you to group resources according to the configuration of the runtime device screen.  The documentation has the details of how this works.  Basically, Android will prefer resources from a subdirectory of “res” the that most closely corresponds to the device on which your app is running.  It is all covered here:

http://developer.android.com/guide/practices/screens_support.html

All of this was introduced back in the Eclair days.  In Part II, though, I have a neat trick that’s buried in the Honeycomb docs that makes this same code compatible with even older and smaller phones.