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.
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.
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.
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.
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:
minSdkVersion
to be the oldest version that you are willing
to supporttargetSdkVersion
to be something relatively recent, unless
you have specific reasons to use some specific versionThe 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:
CalendarView
(https://github.com/SimonVT/android-calendarview)Switch
(https://github.com/BoD/android-switch-backport)DatePicker
(https://github.com/SimonVT/android-datepicker)NumberPicker
(https://github.com/SimonVT/android-numberpicker)TimePicker
(https://github.com/SimonVT/android-timepicker)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.
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.
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).
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.
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.
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.
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.