Efficient Multi-Screen Resource Selection

Dave Smith
Dave Smith

Since its inception, one of the key elements of the Android platform has been the tools provided in its resource framework to select appropriate assets tailored to the user's device type. Over time, this system has been added to here and there to include differentiators for different screen sizes, resolution densities, presence of different hardware items like keyboards, and so on. Undoubtedly, the most common use of this system is to create differentiation for different screen configurations (both size and resolution density). As the Android device landscape has grown, developers like myself have found themselves struggling to keep up with the additions Google is making to the SDK to allow applications to properly adapt.

Today, basically three main device classes exist that Android applications need to support:

  • Handsets (phones)
  • Small tablets (7" category)
  • Large tablets (10" and above)

Within these categories you will find minor differences in resolution, aspect, ratio, and physical size; but for the design of an application's user interface, these three should remain the focus. In most cases, if your application is going to support both tablets and handsets, those user experiences should be quite different, and this often means that the layouts used for each Activity of Fragment will also be very different. On top of this, the difference in screen real estate between a small and large tablet may also require a slightly different layout between them.

Universal Applications and Size Qualifiers

Google still recommends that you should present your application as a single entity to the user that runs optimized for each device type when the user runs it, rather than creating separate applications for the handset and tablet. So to accomplish this, and to keep our code from being full of branches for different devices, we want to make use of resource qualifiers to create separate layout assets that the device chooses at runtime to best suit its configuration.

Google's first attempt at creating qualifiers to separate layouts by device was to create a handful of size "buckets": small, normal, large, xlarge. Initially, this worked out well as handset devices were in the small/normal category, the first 7" tablets on the market were large, and the first 10" tablets were xlarge... everything had a place.

However, more and more devices began appearing in the market with new screen sizes and the lines began to blur about where a device might present itself. A new solution to the problem was presented in Android 3.2, in the form of new qualifiers to pick resources based on a devices current width/height or "smallest" width; the latter being a new designation for the devices size by representing it in terms of the width (in density-independent pixels, or dp) of the shortest side of the screen. Doing this qualification in terms of the "dp" of the screen is more conducive to layout design, as we have an inherent minimum constraint that the layout can expect from the device. Using the new paradigm, it has become common to define small tablets as having a smallest width of 600dp, and large tablets at 720dp, with handsets being everything that falls below that line. For applications targeting Android 3.2 or higher as a minimum this new system also works well because everything still has a place.

There is one additional difficulty created by today's marketplace. Developers wishing to create an application that runs universal on handsets and tablets are compelled to support a minimum Android version no higher than 2.2, or at best 2.3. In addition, tablet devices in the 7" and 10" form factor really started to come about with the release of Android 3.0 and many remain either at that version or 3.1...still below the version limit for the new qualifiers.

So what do we do? If we want to support handsets at all, and preferably older tablets, we are forced to use the old size qualifiers. But as new handset devices begin to commonly reach 5" screens, we start to see handsets encroaching on the "large" qualification we are trying to reserve for tablets, so we need to leverage the new qualifiers on devices that can understand them. We could just create duplicates of each resource in a directory with the old size qualifier and place it an another directory with a corresponding smallest width qualifier. It is common also that dimensions or other resource items will need to be specified differently for tablet and handset. This may lead us to a directory structure like the following:

  1. We place any layouts that are common to all devices and layouts that should be displayed only on handset devices in the default directory.

    res/layout/
      main.xml
    res/values/
      dimens.xml
      strings.xml
    
  2. If our application needs some additional layouts and resources to create tablet UI separate from handset, we add the *-large and *-sw600dp directories and place those files there (the same layout files in each place). The reason we need to have them defined in both is so the devices running pre-3.2 and 3.2+ can both find the correct resource.

    res/layout-large/
      main.xml
    res/values-large/
      dimens.xml
    res/layout-sw600dp/
      main.xml
    res/values-sw600dp/
      dimens.xml
    
  3. If there are any further layouts or resource that need to be specified differently for large tablets versus small tablets, we can add the *-xlarge and *-sw720dp directories as well.

    res/layout-sw720dp/
      main.xml
    res/values-sw720dp/
      dimens.xml
    res/layout-xlarge/
      main.xml
    res/values-xlarge/
      dimens.xml
    

However, you can probably already see that this creates a maintenance issue for the application as almost every resource asset has to be maintained twice. There is also a secondary problem in that all the files have the same name, even though they represent different layout content, and in some IDE tools this can become confusing as to which file you are working with.

Reducing Duplication

In order to reduce the number of resource copies, we can employ a technique called aliasing. This involves placing the resource items themselves into a common directory (meaning they have different names, usually describing their function), and then placing links that represent each resource with its common name into qualified directories. This reduces the number of directories we need since the layout aliases can fit into the values-* directories with other qualified resources.

So, for example, we now have the following:

res/layout
  main.xml
  main_smalltablet.xml
  main_largetablet.xml
res/values/
  dimens.xml
  strings.xml
res/values-large/
  layout.xml
  dimens.xml
res/values-sw600dp/
  layout.xml
  dimens.xml
res/values-sw720dp/
  layout.xml
  dimens.xml
res/values-xlarge/
  layout.xml
  dimens.xml

The layout resources for handset (and any future small-screen device we didn't anticipate) is named properly directly in the default directory. It is always good practice to keep as much in your default locations as possible to avoid a ResourceNotFoundException on a device some day in the future. The layout.xml files in the qualified directories contain the aliases, and look like the following:

res/layout-large/layout.xml and res/layout-sw600dp/layout.xml

<resources>
    <item name="main" type="layout">@layout/main_smalltablet</item>
</resources>

res/layout-xlarge/layout.xml and res/layout-sw720dp/layout.xml

<resources>
    <item name="main" type="layout">@layout/main_largetablet</item>
</resources>

These files normalize the other two layouts to have the same R.layout.main resource identifier on larger screen devices. You can see that we still need to copy the alias file between the size qualified and smallest-width qualified directories, but this duplication is much easier to manage; we still only have one layout file to maintain.

This approach, and the one previous, both still suffer from one increasing problem: the *-large directories are considered a valid resource qualifier on 5"+ handset devices. The smallest width qualifier may have higher precedence than the size qualifier, but any matching qualifier will win over a default selection...which is what we want all handsets to use.

A Proposed Solution

Taking all of this into account, the following is the approach I have chosen to take moving forward...

One Final Assumption

I made note of the fact that Android tablet devices smaller than 10" were not prevalent in the market in until after Android 3.2; the original Galaxy Tab 7 is the only major device in this category running Android 2.2 and its sales were less than spectacular. So to combat the issue of devices encroaching on "large", we can remove this qualified directory with a minimal impact. Any 7" device running the application on pre-3.2 software will simply load the handset UI (less than ideal, but also the lesser evil). For larger tablets, however, the device population running Android 3.0 and 3.1 is significant enough that using the older qualifier makes good sense.

The Compromise

Therefore, on most applications, where layout resources for all tablets are the same and no special distinctions need to be made between large and small, I use the following:

res/layout/
  **ALL layout resources, named appropriately**
res/values/
  **Default resource values**
res/values-sw600dp/
  **Tablet specific aliases and resource values**

In this paradigm we need not duplicate any resources or even aliases to resources, and the casualties in terms of devices not optimized is minimal. In cases where special adaptions need to be made for larger tablets only, I add back in the xlarge and sw720dp buckets:

res/layout/
  **ALL layout resources, named appropriately**
res/values/
  **Default resource values**
res/values-sw600dp/
  **Tablet specific aliases and resource values**
res/values-sw720dp/
  **Large tablet specific alises and resource values**
res/values-xlarge/
  **Copy of large tablet aliases and resource values**

This reduces the copied resources down to a single set, and the layout copies are only aliases. Until such time when we can remove support for device versions that do no recognize the new qualifier system, this approach is a good compromise that covers most devices in use and is still easy to maintain.