Backwards Compatibility Strategies and Tactics

Android is an ever-moving target, averaging about 2.5 API level increments per year. The Android Developer site maintains a chart and table showing the most recent breakdown of OS versions making requests of the Play Store.

Most devices tend to be clustered around 1-3 minor releases. However, these are never the most recent release, which takes time to percolate through the device manufacturers and carriers and onto devices, whether those are new sales or upgrades to existing devices.

Some developers panic when they realize this.

Panic is understandable, if not necessary. This is a well-understood problem, that occurs frequently within software development — ask any Windows developer who had to simultaneously support everything from Windows 98 to Windows XP, or Windows XP through Windows 8.1. Moreover, there are many things in Android designed to make this problem as small as possible. What you need are the strategies and tactics to make it all work out.

Think Forwards, Not Backwards

Android itself tries very hard to maintain backwards compatibility. While each new Android release adds many classes and methods, relatively few are marked as deprecated, and almost none are outright eliminated. And, in Android, “deprecated” means “there’s probably a better solution for what you are trying to accomplish, though we will maintain this option for you as long as we can”.

Despite this, many developers aim purely for the lowest common denominator. Aiming to support older releases is noble. Ignoring what has happened since those releases is stupid, if you are trying to distribute your app to the public via the Play Store or similar mass-distribution means.

Why? You want your app to be distinctive, not decomposing.

For example, as we saw in the chapter on the action bar, adding one line to the manifest (android:targetSdkVersion="11") gives you the action bar, the holographic widget set (e.g., Theme.Holo), the new style of options menu, and so on. Those dead-set on avoiding things newer than Android 2.1 would not use this attribute. As a result, on Android 3.0+ devices, their apps will tend to look old. Some will not, due to other techniques they are employing (e.g., running games in a full-screen mode), but many will.

You might think that this would not matter. After all, how many people in 2011 were even using Android 3.x? 5%?

However, those in position to trumpet your application — Android enthusiast bloggers chief among them — will tend to run newer equipment. Their opinion matters, if you are trying to have their opinion sway others relative to your app. Hence, if you look out-of-touch to them, they may be less inclined to provide glowing recommendations of your app to their readers.

Besides, not everything added to newer versions of Android is pure “eye candy”. It is entirely possible that features in the newer Android releases might help make your app stand out from the competition, whether it is making greater use of NFC or offering tighter integration to the stock Calendar application or whatever. By taking an “old features only” approach, you leave off these areas for improvement.

And, to top it off, the world moves faster than you think. It takes about a year for a release to go from release to majority status (or be already on the downslope towards oblivion, passed over by something newer still). You need to be careful that the decisions you make today do not doom you tomorrow. If you focus on “old features only”, how much rework will it take you to catch up in six months, or a year?

Hence, this book advocates an approach that differs from that taken by many: aim high. Decide what features you want to use, whether those features are from older releases or the latest-and-greatest release. Then, write your app using those features, and take steps to ensure that everything still works reasonably well (if not as full-featured) on older devices. This too is a well-trodden path, used by Web developers for ages (e.g., support sexy stuff in Firefox and Safari, while still gracefully degrading for IE6). And the techniques that those Web developers use have their analogous techniques within the Android world.

Aim Where You Are Going

One thing to bear in mind is that the OS distribution chart and table shown above is based on devices contacting the Play Store. Hence, this is only directly relevant if you are actually distributing through the Play Store.

If you are distributing through the Amazon AppStore, or to device-specific outlets (e.g., BlackBerry World), you will need to take into account what sorts of devices are using those means of distribution.

If you are specifically targeting certain non-Play Store devices, like the Kindle Fire, you will need to take into account what versions of Android they run.

If you are building an app to be distributed by a device manufacturer on a specific device, you need to know what Android version will (initially) be on that device and focus on it.

If you are distributing your app to employees of a firm, members of an organization, or the like, you need to determine if there is some specific subset of devices that they use, and aim accordingly. For example, some enterprises might distribute Android devices to their employees, in which case apps for that enterprise should run on those devices, not necessarily others.

A Target-Rich Environment

There are a few places in your application where you will need to specify Android API levels of relevance to your code.

The most important one is the android:minSdkVersion attribute, as discussed early in this book. You need to set this to the oldest version of Android you are willing to support, so you will not be installed on devices older than that.

There is also android:targetSdkVersion, mentioned in passing earlier in this chapter. In the abstract, this attribute tells Android “this is the version of Android I was thinking of when I wrote the code”. Android can use this information to help both backwards and forwards compatibility. Historically, this was under-utilized. However, with API Level 11 and API Level 14, android:targetSdkVersion took on greater importance. Specifying 11 or higher gives you the action bar and all the rest of the look-and-feel introduced in the Honeycomb release. Specifying 14 or higher will give you some new features added in Ice Cream Sandwich, such as automatic whitespace between your app widgets and other things on the user’s home screen. In general, use a particular android:targetSdkVersion when instructions tell you to.

The third place — and perhaps the one that confuses developers the most – is the build target. This shows up as compileSdkVersion in build.gradle for Android Studio and Gradle users.

Part of the confusion is the multiple uses of the term “target”. The build target has nothing to do with android:targetSdkVersion. Nor is it strictly tied to what devices you are targeting.

Rather, it is a very literal term: it is the target of the build. It indicates:

The net is that you set your build target to be the lowest API level that has everything you are using directly.

Lint: It’s Not Just For Belly Buttons

In the old days, the only way to find out that you were using a newer class or method than what was in your minSdkVersion would be to set your build target to be the same as your minSdkVersion. That way, any attempt to use something newer than your minimum would be greeted with compile errors. This works, but at a high cost: it makes intentionally using newer capabilities very painful, forcing you to use reflection to access them.

Nowadays, this is no longer needed, thanks to Lint.

Lint is part of the standard build process, adding new errors and warnings for things that are syntactically valid but probably not the right answer. In particular, Lint will tell you if you are using classes or methods that are newer than your minSdkVersion, even if they are valid for your build target.

Hence, the targeting strategy nowadays is:

A Little Help From Your Friends

The simplest way to use a feature yet support devices that lack the feature is to use a compatibility library that enables the feature for more devices. The Android Support package is one such compatibility library, though it also offers other classes as well.

With a compatibility library, the API for using the library is nearly identical to using the native Android capability, mostly involving slightly different package names (e.g., android.support.v4.app.Fragment instead of android.app.Fragment).

So, if there is something new that you want to use on older devices, and the new feature is not obviously tied to hardware, see if there is a “backport” of the feature available to you. Examples include backports of:

Avoid the New on the Old

If the goal is to support new capabilities on new devices, while not losing support for older devices, that implies we have the ability to determine what devices are newer and what devices are older. There are a few techniques for doing this, involving Java and resources.

Java

If you wish to conditionally execute some lines of code based on what version of Android the device is running, you can check the value of Build.VERSION, referring to the android.os.Build class. For example:


if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.GINGERBREAD) {
  // do something only on API Level 9 and higher
}

Any device running an older version of Android will skip the statements inside this version guard and therefore will not execute.

That technique is sufficient for Android 2.0 and higher devices. If you are still supporting Android 1.x devices, the story gets a bit more complicated, and that will be discussed later in the book.

If you decide that you want your build target to match your minSdkVersion level — as some developers elect to do — your approach will differ. Rather than blocking some statements from being executed on old devices, you will enable some statements to be executed on new devices, where those statements use Java reflection (e.g., Class.forName()) to reference things that are newer than what your build target supports. Since using reflection is extremely tedious in Java, it is usually simpler to have your build target reflect the classes and methods you are actually using.

@TargetAPI

One problem with this technique is that your IDE will grumble at you, saying that you are using classes and methods not available on the API level you set for your minSdkVersion. To quiet down these Lint messages, you can use the @TargetAPI annotation.

For example, earlier in the book, we saw code like this:


@TargetApi(Build.VERSION_CODES.HONEYCOMB)
static public <T> void executeAsyncTask(AsyncTask<T, ?, ?> task, T... params) {
  if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
    task.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, params);
  }
  else {
    task.execute(params);
  }
}

This utility method executes an AsyncTask using a multi-threaded thread pool. That is the default behavior of execute() on API Level 10 and below. On higher versions of Android, we can explicitly opt into the multi-threaded thread pool by using executeOnExecutor(), but that method does not exist prior to API Level 11. Hence, we check our API level at runtime via Build.VERSION.SDK_INT, see if we are on HONEYCOMB or higher, and branch accordingly. However, for a project with a minSdkVersion of 10 or below, Lint will still complain — Lint is just not sophisticated enough to realize that we are correctly handling newer API levels. The @TargetApi(Build.VERSION_CODES.HONEYCOMB) annotation tells Lint that we have indeed confirmed that we are “doing the right thing”, at least through API Level 11.

However, by using @TargetApi(Build.VERSION_CODES.HONEYCOMB), we are implicitly saying that we have not checked to see if we are doing things properly for higher versions of Android. So long as all the classes, methods, and such that we reference in this executeAsyncTask() method are available in API Level 11, we are fine. If we change the implementation to reference something from, say, API Level 14, now Lint will start complaining again. This is what we want, so we are alerted to the problem and can fix it. Hence, only set the @TargetApi() annotation to the API level that are you explicitly handling. Do not just set it to some arbitrarily high level (or, worse, use @SuppressWarning to try to get Lint to shut up entirely).

Resources

The aforementioned version guards only work for Java code. Sometimes, you will want to have different resources for different versions of Android. For example, you might want to make a custom style that inherits from Theme.Holo for Android 3.0 and higher. Since Theme.Holo does not exist on earlier versions of Android, trying to use a style that inherits from it will fail miserably on, say, an Android 2.2 device.

To handle this scenario, use the -vNN suffix to have two resource sets. One (e.g., res/values-v11/) would be restricted to certain Android versions and higher (e.g., API Level 11 and higher). The default resource set (e.g., res/values/) would be valid for any device. However, since Android chooses more specific matches first, an Ice Cream Sandwich phone would go with the resources containing the -v11 suffix. So, in the -v11 resource directories, you put the resources you want used on API Level 11 and higher, and put the backwards-compatible ones in the set without the suffix. This works for Android 2.0 and higher. You can also use -v3 for resources that only will be used on Android 1.5 (and no higher) or -v4 for resources that only will be used on Android 1.6.

Components

One variation on the above trick allows you to conditionally enable or disable components, based on API level.

Every <activity>, <receiver>, or <service> in the manifest can support an android:enabled attribute. A disabled component (android:enabled="false") cannot be started by anyone, including you.

We have already seen string resources be used in the manifest, for things like android:label attributes. Boolean values can also be created as resources. By convention, they are stored in a bools.xml file in res/values/ or related resource sets. Just as <string> elements provide the definition of a string resource, <bool> elements provide the definition of a boolean resource. Just give the boolean resource a name and a value:


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

The above example has a boolean resource, named on_honeycomb, with a value of false. That would typically reside in res/values/bools.xml. However, you might also have a res/values-v11/bools.xml file, where you set on_honeycomb to true.

Now, you can use @bool/on_honeycomb in android:enabled to conditionally enable a component for API Level 11 or higher, leaving it disabled for older devices.

This can be a useful trick in cases where you might need multiple separate implementations of a component, based on API level. For example, later in the book we will examine app widgets — those interactive elements users can add to their home screens. App widgets have limited user interfaces, but API Level 11 added a few new capabilities that previously were unavailable, such as the ability to use ListView. However, the code for a ListView-backed app widget may be substantially different than for a replacement app widget that works on older devices. And, if you leave the ListView app widget enabled in the manifest, the user might try choosing it and crashing. So, you would only enable the ListView app widget on API Level 11 or higher, using the boolean resource trick.

Testing

Of course, you will want to make sure your app really does work on older devices as well as newer ones.

At build time, one trick to use periodically is to change your build target to match your minSdkVersion, then see where the compiler complains. If everything is known (e.g., resource attributes that will be ignored on older versions) or protected (e.g., Java statements inside a version guard if statement), then you are OK. If, however, you see complaints about something you forgot was only in newer Android releases, you can take steps to fix things.

You will also want to think about Android versions when it comes to testing, a topic that will be covered later in this book.

Keeping Track of Changes

Each Android SDK release is accompanied by API release notes, such as this set for Android 4.4/API Level 19.

Similarly, each Android SDK release is accompanied by its “API Differences Report”, a roster of each added, removed, or modified class or method. For example, this API Differences Report points out the changes between API Level 18 and API Level 19.

Other changes are called out in the JavaDocs for Build.VERSION_CODES, with particular emphasis on what happens when you set a specific API level as your android:targetSdkVersion. Note that this roster is not complete, but may mention some things not mentioned in the other locations.

Each class, method, and field in the JavaDocs has a notation as to what API level that particular item was added. Class API levels appear towards the top of the page; method and field API levels appear on the right side of the gray bar containing the method signature or field declaration. Also, in the JavaDocs “Android APIs” column on the left, there is a drop-down that allows you to filter the contents based upon API level.