InfoSpace Tech Blog

Building for Multiple Screens

| | Comments

I recently presented a talk on Building For Multiple Screens at the Big Android BBQ with the goal of bridging the gap between the concepts and theory behind building for multiple screens and practice of building for multiple screens. The talk was received well and I thought it would be helpful to turn it into a blog post.

There are three main things that, if you pay attention to, will help you build your app so that it scales and feels natural on a variety of screen sizes.

  • Planning and understanding your application workflow and navigation.
  • Creating layouts for your workflow that allow you to take full advantage of multiple screen sizes.
  • Implement your application workflow using and taking advantage of the idioms built in to AndroidTM.

This post isn’t going to go into any depth on the first two, but if you’re interested I’ve posted my presentation as well as a sample RSS Reader application which will help walk you through the concepts.

The Workflow

One of the most common workflows I see people asking for help with is the multi-pane workflow. A typical example of this workflow is when you start with a list, tap on a row in the list, and get additional content related to the row you initially clicked on.

On a phone we’d expect tapping on the row to navigate us to another screen with the additional data. On a tablet, where we have a lot more room for content, we have the expectation of being able to keep the context of our original list while also viewing the additional data about the row we selected.

The Layouts

For the purpose of this post we’re going to focus on two layouts.

default/phone: res/layout/main.xml

1
2
3
4
5
6
7
8
9
10
11
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/fragment_container"
    android:orientation="vertical"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <!-- No Fragments defined -->

</LinearLayout>

tablets: res/layout-large/main.xml and/or res/layout-sw600dp/main.xml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/fragment_container"
    android:orientation="horizontal"
    android:layout_width="match_parent"
    android:layout_height="match_parent">
    <Fragment
        android:name="your.package.FirstFragment"
        android:id="@+id/first_fragment"
        android:layout_weight="1"
        android:layout_width="0dp"
        android:layout_height="match_parent" />
    <Fragment
        android:name="your.package.SecondFragment"
        android:id="@+id/second_fragment"
        android:layout_width="0dp"
        android:layout_height="match_parent" />
</LinearLayout>

The layouts are both for the same activity. Again, I won’t go into details about the layout other than to show you the XML and call attention to one important point.

Notice that the first layout doesn’t include any fragments while the second one does. The reason is that for the default/phone workflow we simply want to replace the current fragment on screen with a new one or a previous one as we navigate through the content.

Fragments that are declared in the XML layout file cannot be programmatically replaced using a FragmentTransaction. Only fragments added via a FragmentTransaction can be replaced. Instead you have to programmatically show/hide Fragments declared in a layout. We’ll handle this in the code below but I wanted to call attention to it as it helps provide some context for the code we’re about to dive into.

Handling The Activity Life-Cycle

Activity start-up

The first thing we want to look at is what we do when the activity starts up.

1
2
3
4
5
6
7
8
9
10
11
@Override
public void onCreate(Bundle savedInstanceState)
{
  super.onCreate(savedInstanceState);
  setContentView(R.layout.main);

  if(savedInstanceState == null)
  {
      this.displayFragment(R.id.first_fragment);
  }
}

Notice that because we’ve used the built-in configuration qualifier idioms we only have to reference R.layout.main in the call to setContentView and AndroidTM will chose the correct layout for us automatically.

We also want to make sure that we don’t go through all the work of setting up our fragment if we’re just resuming from the background. So we put in a quick check to see if we’re being passed any saved state.

Displaying a Fragment

You may have noticed that there appears to be a lot of magic in the onCreate method because it just calls displayFragment. Let’s de-mystify that magic and show you what’s really going on when we call displayFragment.

The first thing displayFragment does is check to see if our fragment already exists in our layout. We’ll get to the inner workings of what getFragment is doing in just a minute, but it’s important to understand why we need to try to get a handle to the fragment if it exists.

Remember that our default/phone workflow did not define any fragments in the layout. Only our layout for larger screens defines the fragments in the layout. This is useful to us in code as having a non-null fragment is our queue that we need to update the fragments content and not push a new fragment onto the screen. In other words, it’s our primary technique to respond to our workflow without having to hard code each possible workflow scenario into our code.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public void displayFragment(int id)
{
  Fragment fragment = this.getFragment(id);

  // the fragment is null if it's not already being displayed
  // or it's been declared in the currently displayed layout
  if(fragment == null)
  {
      fragment = this.createFragment(id);
      this.addFragmentToBackStack(fragment, id);
  }
  else
  {
      fragment.loadData();
  }
}

Checking to see if a fragment exists

When defining a fragment in a layout you give the fragment an id using the android:id attribute and (optionally) a tag using the android:tag attribute on the <Fragment> element.

My preference is to only define the android:id and then when I push fragments using a transaction I use the R.id.YOUR_FRAGMENT_ID value as the tag (converted as a string). This allows us to simplify checking for the fragment as we’ve done in our getFragment method.

1
2
3
4
5
6
7
8
9
10
11
public void getFragment(int id)
{
  FragmentManager manager = getSupportFragmentManager();
  Fragment fragment = manager.findFragmentByTag(Integer.toString(id));

  if(fragment == null)
  {
      fragment = manager.findFragmentById(id);
  }
  return fragment;
}

Adding a fragment to the back stack

In the case that getFragment returns null for the fragment we asked for; We need to create an instance of the fragment and then, using a fragment transaction, push the fragment to the screen by adding it to the back stack.

Adding a fragment to the back stack allows AndroidTM to automatically undo the transaction when the user clicks the back button.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public void addFragmentToBackStack(Fragment fragment, int id)
{
  FragmentManager manager = getSupportFragmentManager();
  if(fragment != null && manager != null && !fragment.isInLayout())
  {
      FragmentTransaction transaction = manager.beginTransaction();
      // perform any custom fragment workflows here.
      // like hiding/showing fragments declared in the layout
      // using transaction.hide(...) or transaction.show(../)
      transaction.replace(R.id.fragment_container, fragment, Integer.toString(id));
      transaction.addToBackStack(null);
      transaction.commit();
  }
}

Popping the back stack

Because we’ve added the transaction to the back stack we don’t have to do any additional work for the hard/soft back buttons to work correctly but in practice there is a little work we need to do.

In the case that your application is using an ActionBar you’re going to need to invalidate the options menu when popping items so that any options that have been added by the fragment that was displayed are removed.

We also need to update the home button if we’re using an ActionBar to ensure that the home button provides the correct visual feedback to the user.

I also like to track, in the method, whether or not we actually popped something and return that value. The reason is that I believe the user has an expectation that the app will close when mashing the back button, but not when mashing the home button in the action bar. So in the case of the back button we’re going to want to call finish on our activity if no back stack was popped, whereas for the home button we wouldn’t do anything.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
private boolean popBackStack()
{
  boolean wasBackStackPopped = false;
  FragmentManager manager = getSupportFragmentManager();
  if(manager != null)
  {
      if(this.shouldPopBackStack(manager))
      {
          manager.popBackStack();
          wasBackStackPopped = true;
      }
      this.invalidateOptionsMenu();
  }

  this.updateHomeButton();
  return wasBackStackPopped;
}

The shouldPopBackStack and updateHomeButton methods need to contain the custom logic, which is specific to your apps workflow, to decide if something in the back stack should be popped and what type of visual feedback to provide. In the case of a dual-pane app we wouldn’t want to pop the back stack or show the back arrow on the home button if the only two fragments that are displayed are the original two fragments defined in the layout file.

Wrapping it all up

Building for multiple screens doesn’t require much additional work once you understand the tools at your disposal. The only requirements are:

  • Understanding the workflow and navigation of your app in order to identify opportunities for customized layouts.
  • Creating the custom layouts, using configuration qualifiers, that handle the size specific workflow.
  • Implement the pushing and popping of fragments using the techniques outlined above.

Android is a trademark of Google Inc.

Comments