Wednesday, June 13, 2012

Designing for Backwards Compatibility

A common issue in Android development is backwards compatibility. How can we add cool new features from the most recent Android API while still ensuring that it runs correctly on devices running older versions of Android? This post discusses the problem by means of a simple example, and proposes a scalable, well-designed solution.

(Note: please read this short post before continuing forward).

The Problem

Let's say we are writing an application that reads and writes pictures to new albums (i.e. folders) located on external storage, and that we want our application to support all devices running Donut (Android 1.6, SDK version 4) and above. Upon consulting the documentation, we realize there is a slight problem. With the introduction of Froyo (Android 2.2, SDK version 8) came a somewhat radical change in how external storage was laid out and represented on Android devices, as well as several new API methods (see android.os.Environment) that allow us access to the public storage directories. To ensure backwards compatibility all the way back to Donut, we must provide two separate implementations: one for older, pre-Froyo devices, and another for devices running Froyo and above.

Setting up the Manifest

Before we dive into the implementation, we will first update our uses-sdk tag in the Android manifest. There are two attributes we must set,

  • android:minSdkVersion="4". This attribute defines a minimum API level required for the application to run. We want our application to run on devices running Donut and above, so we set its value to "4".
  • android:targetSdkVersion="15". This attribute is a little trickier to understand (and is incorrectly defined on blogs all over the internet). This attribute specifies the API level on which the application is designed to run. Preferably we would want its value to correspond to the most recently released SDK ("15", at the time of this posting). Strictly speaking, however, its value should be given by the largest SDK version number that we have tested your application against (we will assume we have done so for the remainder of this example).

The resulting tag in our mainfest is as follows:

<uses-sdk 
    android:minSdkVersion="4"
    android:targetSdkVersion="15" >
</uses-sdk>

Implementation

Our implementation will consist of an abstract class and two subclasses that extend it. The abstract AlbumStorageDirFactory class enforces a simple contract by requiring its subclasses to implement the getAlbumStorageDir method. The actual implementation of this method depends on the device's SDK version number. Specifically, if we are using a device running Froyo or above, its implementation will make use of new methods introduced in API level 8. Otherwise, the correct directory must be determined using pre-Froyo method calls, to ensure that our app remains backwards compatible.

public abstract class AlbumStorageDirFactory {

  /**
   * Returns a File object that points to the folder that will store 
   * the album's pictures. 
   */
  public abstract File getAlbumStorageDir(String albumName);

  /**
   * A static factory method that returns a new AlbumStorageDirFactory 
   * instance based on the current device's SDK version.
   */
  public static AlbumStorageDirFactory newInstance() {
    // Note: the CompatibilityUtil class is implemented 
    // and discussed in a previous post, entitled 
    // "Ensuring Compatibility with a Utility Class".
    if (CompatabilityUtil.isFroyo()) {
      return new FroyoAlbumDirFactory();
    } else {
      return new BaseAlbumDirFactory();
    }
  }
}

The two subclasses and their implementation are given below.The class also provides a static factory newInstance method (note that this method makes use of the CompatabilityUtil utility class, which was both implemented and discussed in a previous post). We discuss this method in detail in the next section.

The BaseAlbumDirFactory subclass handles pre-Froyo SDK versions:

public class BaseAlbumDirFactory extends AlbumStorageDirFactory {

  /**
   * For pre-Froyo devices, we must provide the name of the photo directory 
   * ourselves. We choose "/dcim/" as it is the widely considered to be the 
   * standard storage location for digital camera files.
   */
  private static final String CAMERA_DIR = "/dcim/";

  @Override
  public File getAlbumStorageDir(String albumName) {
    return new File (
                    Environment.getExternalStorageDirectory() 
                    + CAMERA_DIR 
                    + albumName
    );
  }
}

The FroyoAlbumDirFactory subclass handles Froyo and above:

public class FroyoAlbumDirFactory extends AlbumStorageDirFactory {

  @Override
  public File getAlbumStorageDir(String albumName) {
    return new File(
        Environment.getExternalStoragePublicDirectory(
            Environment.DIRECTORY_PICTURES
        ), 
        albumName
    );
  }
}

Making Sense of the Pattern

Take a second to study the structure of the code above. Our implementation ensures compatibility with pre-Froyo devices through a simple design. To ensure compatibility, we simply request a new AlbumStorageDirFactory and call the abstract getAlbumStorageDir method. The subclass is determined and instantiated at runtime depending on the Android device's SDK version number. See the sample activity below for an example on how an ordinary Activity might use this pattern to retrieve an album's directory.

public class SampleActivity extends Activity {

  private AlbumStorageDirFactory mAlbumFactory;

  @Override
  public void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);

    // Instantiate the AlbumStorageDirFactory. Instead of
    // invoking the subclass' default constructors directly,
    // we make use of the Abstract Factory design pattern,
    // which encapsulates the inner details. As a result, the
    // Activity does not need to know `anything` about the
    // compatibility-specific implementation--all of this is
    // done behind the scenes within the "mAlbumFactory" object.     
    mAlbumFactory = AlbumStorageDirFactory.newInstance();

    // get the album's directory
    File sampleAlbumDir = getAlbumDir("sample_album");
  }

  /**
   * A simple helper method that returns a File corresponding
   * to the album named "albumName". The helper method invokes
   * the abstract "getAlbumStorageDir" method, which will return
   * correct location of the directory depending on the subclass
   * that was returned in "newInstance" (which depends entirely
   * on the device's SDK version number).
   */
  private File getAlbumDir(String albumName) {
    return mAlbumStorageDirFactory.getAlbumStorageDir(albumName);
  }
}

There are a couple benefits to organizing the code the way we have:

  • It's easily extendable. While there is certainly no need to separate our implementations into classes for simple examples (such as the one discussed above), doing so is important when working with large, complicated projects, as it will ensure changes can quickly be made down the line.
  • It encapsulates the implementation-specific details. Abstracting these details from the client makes our code less cluttered and easier to read (note: in this case, "the client" was the person who wrote the Activity class).

Conclusion

Android developers constantly write code to ensure backwards compatibility. As projects expand and applications become more complex, it becomes increasingly important to ensure your implementation is properly designed. Hopefully this post helped and will encourage you to more elegant solutions in the future!

Leave a comment if you have any questions or criticisms... or just to let me know that you managed to read through this entire post without getting distracted!


2 comments :

  1. Hi Alex. Thanks for sharing... I just completed a course in OOD and Programming. I really enjoyed it and you follow suit - you speak simply and unpretentiously - your thought line and variables used to instruct, along with the points conveyed, are easy to grasp... even though I've never used an Android device! But I nevertheless pick up intuitively what's being addressed BY VIRTUE OF the design pattern and what I know of Java.

    I got lost with the 'SampleActivity' class and I was thrown by the call to the 'getAlbumDir' method instead of 'getAlbumSTORAGEdir' method and well, if I followed you alright, I think when you said just above it in ref. to backwards compatibility about making a call to 'getAlbumStorageDirtwo' method you really meant 'getAlbumStorageDirFACTORY(1)' method. [The caps and the one are illustrative since I think you inadvertently left out the word Factory and following your thought train the #1 implementation of the getAlbumStorageDirFactory method is for the pre-Froyo SDK. If I am wrong, please correct me, but if not, I thought you might want to update your post.. because I think you will have a lot of readers with the way you write as I see it now, and a lot of people can learn from you. I'm glad I ran into you myself.].

    Sincerely,
    Bill

    ReplyDelete
  2. Hi Bill,

    You were right that there was a typo, but I think you meant the getAlbumStorageDir method (there is no "factory" at the end). This must have been a stupid copy/paste error or something... I remember changing the names of the methods at the last second. Thanks a lot for pointing that out! I have also updated/commented the SampleActivity implementation... please let me know if the extra details make it more clear (or if you have any suggestions on what needs additional clarification... it's a bit difficult for me since I was the one who wrote it :P). Also, if you could let me know what exactly you meant by "the #1 implementation of the getAlbumStorageDirFactory method is for the pre-Froyo SDK", that'd be great... I didn't follow that part.

    Thanks a lot for your (extremely nice) comment... from what I can tell, you speak rather "unpretentiously" as well haha. I think it's cool that you are reading this even though you don't know Android... and actually kind of flattering since this blog was inspired/motivated by my intense interest in Android development. I'm currently working on a pretty massive tutorial on Loaders and the LoaderManager, so the next few posts will probably be mostly geared towards Android developers. But I will keep in mind that there are at least a couple people out there who read my posts who are primarily interested in the design patterns. I definitely hope to cover some cooler design patterns (i.e. anything but the abstract factory pattern :P).

    Thanks again for the comment, Bill... and don't hesitate to point out any other typos/areas that need additional clarification if you see them!

    Alex

    p.s. Your comment just inspired me to go back and re-read every single one of my posts... I have a zero tolerance for typos in this blog. :)

    ReplyDelete