Friday, February 11, 2011

The Static Starter Pattern

One of my favorite things about Android is that it is not another MVC framework. Now some people find this as a weakness, but not me. I think MVC has its faults. Still many people think of application in terms of MVC, and for them Android's Activity class is as close to a controller as you will get. Pretty much every "screen" or "page" in an Android application has a single Activity behind it (there are exceptions, I know, but bear with me.) Inevitably you need to transition between Activities, and this is one of the places where Android can seem a little weird. You need to create an Intent and then pass that to the startActivity method on a Context object (usually the Activity that you are transitioning from.) That's not too bad. However, it is often the case that a given Activity expects some data -- a model if you like -- that it will use to create the UI (the view if you like.) Let's take a concrete example from Android in Practice. The MediaMogul application (chapter 11) allows the user to create a slideshow by selecting pictures, music, and video from their SD card. Then there is an Activity that plays the slideshow called SlideshowActivity. Here is code from the Activity that starts the SlideshowActivity.
Intent intent = new Intent(this, SlideshowActivity.class);
intent.putExtra("videoUri", videoUri);
intent.putExtra("imageFileNames", images);
intent.putExtra("selectedSong", song);
startActivity(intent);
And here is code from SlideshowActivity to retrieve this data:
Intent intent = getIntent();
ArrayList<String> images = intent.getStringArrayListExtra("imageFileNames");
Song song = intent.getParcelableExtra("selectedSong"); // custom Parcelable class
Uri videoUri = intent.getParcelableExtra("videoUri");
There is an obviously ugly issue here. Both Activities need to know the keys (imageFileNames, selectedSong, videoUri) to use to put/get from the Intent's extras. This is such a common thing in Android, even in the framework, that there is a simple pattern for dealing with it. Just use public constants declared in the class that will use the Intent extras:
public class SlideshowActivity extends Activity{
public static final String EXTRA_IMAGE_FILE_NAMES = "imageFilesNames";
public static final String EXTRA_SELECTED_SONG = "selectedSong";
public static final String EXTRA_VIDEO_URI = "videoUri";
...
Intent intent = getIntent();
ArrayList<String> images = intent.getStringArrayListExtra(EXTRA_IMAGE_FILE_NAMES);
// etc.
And then of course in the calling class you would now have:
intent.putExtra(SlideshowActivity.EXTRA_IMAGE_FILES_NAMES, images):
// etc.
Problem solved, right? Well not exactly. You still don't know what the types of the extras should, and you do not know if a particular extra is required or optional. Documenting your code can help with this:
public class SlideshowActivity extends Activity{
  /**
   * An ArrayList of Strings. Required.
   */
  public static final String EXTRA_IMAGE_FILE_NAMES = "imageFileNames";
...
}
So we put the type information and wether the field is required or not as a comment. Wait, doesn't this feel more like Ruby or Python instead of Java? Isn't there some way to use the language syntax and compiler to state this information in a better way instead of relying on code comments? That's where the Static Starter Pattern kicks in:
public class SlideshowActivity extends Activity{
  public static void start(ArrayList<String> images, Song song, Uri videoUri, Context ctx){
    Intent intent = new Intent(ctx, SlideshowActivity.class);
    intent.putExtra(EXTRA_IMAGE_FILE_NAMES, images);
    intent.putExtra(EXTRA_SELECTED_SONG, song);
    intent.putExtra(EXTRA_VIDEO_URI, videoUri);
    ctx.startActivity(intent);
  }
...
}
From this method signature, I know the types of all of the extras and I know that they are all required. If I wanted to make one of them optional, then I could simply overload the start method with only two of the parameters, removing the optional parameter. Of course, somebody could bypass this and still use an Intent.
Now this is not the best pattern for every Activity. The most obvious example is if you want your Activity to be started by other applications. Another application won't be able to invoke a static method. You will have to use the action/Intent-filter path for this. Of course this is pretty rare, how many of your Activities are meant to be started by other apps? Even if this is the case, you can still use the pattern for use within your app.
This pattern is also not limited to Activities. You can do it with Services as well, especially IntentServices:
public class AwesomeService extends IntentService{
  public static void start(String someString, int someInt, Context ctx){
    Intent intent = new Intent(ctx, AwesomeService.class);
    intent.putExtra(EXTRA_SOME_STRING, someString);
    intent.putExtra(EXTRA_SOME_INT, someInt);
    ctx.startService(intent);
  }
}
If your IntentService handles multiple actions, this can be built into the pattern:
public class AwesomeService extends IntentService{
  public static startUploadFile(String someFileName, Context ctx){
    Intent intent = new Intent(ctx, AwesomeService.class);
    intent.setAction(ACTION_UPLOAD_FILE); // ACTION_UPLOAD_FILE is a constant
    intent.putExtra(EXTRA_SOME_FILE_NAME, someFileName);
    ctx.startService(intent);
  }
}
You get the idea. You could extend this to BroadcastReceivers as well. I discussed this pattern a bit with my Android in Practice co-author, Charlie Collins. He liked it too, and pointed out that others have used it as well. Personally, I'm not a fan of static methods or of passing around the Context like that, but the positives seem to outweigh the negatives. What do you think? Useful? Over-engineered?

6 comments:

  1. This is exactly the kind of practice promoted by Effective Java, using the compiler to avoid mistakes, and I think it works well.

    ReplyDelete
  2. I like pass Intent created like 'new Intent(this, SomeActivity.class)' and pass to static method that will just add extras. With this modified intent you can call startActivity or whatever you want.

    ReplyDelete
  3. i just discovered that google does this in their own contacts app. take a look at the buildIntent method here (https://github.com/android/platform_packages_apps_contacts/blob/master/src/com/android/contacts/activities/PhotoSelectionActivity.java) for example.

    ReplyDelete
  4. Anonymous7:35 AM

    For an even more automated way to do this, have a look at Android Annotations, and its IntentBuilder: https://github.com/excilys/androidannotations/wiki/HowItWorks#StartingAnAnnotatedActivity

    ReplyDelete
  5. Anonymous3:04 AM

    Starter pattern does not run until I set flag to intent as follows

    intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_MULTIPLE_TASK);

    otherwise it gives exception as follows

    android.util.AndroidRuntimeException: Calling startActivity() from outside of an Activity context requires the FLAG_ACTIVITY_NEW_TASK flag. Is this really what you want?

    Why?

    ReplyDelete