Android Development, Part 5a: Starting Activities

posted on 10/12/10 at 08:03:00 pm by Joel Ross

This is Part 5a of an ongoing series. The rest of the series is always viewable and up to date at http://www.rosscode.com/android. If you're just checking in, the archives are worth a look.

I took a week off, but I'm back with more details on user interfaces. Again, I'm not a designer, nor will I ever claim to be, but I have had a bit of experience with getting a few screens running in the application I'm writing for Pay It Square

I'm going to cover a technique I use to start new actions. So far, I haven't touched on starting actions, but that's a key piece to building a useful application. Most of the examples I've seen show activities starting right in the code from another activity. I don't particularly care for that, as that's giving too much knowledge about the flow of an application to the views. I'd prefer to handle starting new activities through either something under my presenters (like a workflow, for example), or from my presenters.

But that's difficult to do because starting activities requires a reference to the context. And under normal circumstances, your context is attached to your activity. I sought to split those two things. What I came up with works in some cases, but not all. It's great for fire and forget activities, but doesn't work when you need to get a result back from an activity. For me, that solved most of my screen switching needs. If I run across a situation that it doesn't solve, then I'll deal with that then.

So, what does this look like? Well, here's my interface:

   1: public interface IActivityStarter {
   2:   void setClassName(String packageName, String className);
   3:   void putExtra(String name, Serializable value);
   4:   void startActivity();
   5: }

I'm not doing anything too complicated when it comes to starting another activity - a user takes an action on a screen, and that takes them to another screen. I am not calling out to any of Android's system activities either, so my interface is pretty simple. I allow for extra information to be attached to the intent, the class for the intent to target, and a way to actually start the activity.

Let's back up a step. Android uses an Intent to start new activities. Depending on how you specify your intent, the request can either be handled by the OS, or it can be handled by a specific activity in your application. Your application's activities are defined in the AndroidManifest.xml file for your application. Whether you realize it or not, an activity is set up for you when you create a new application. It looks like similar to this:

   1: <activity android:name=".Activities.main" android:label="@string/app_name">
   2:   <intent-filter>
   3:     <action android:name="android.intent.action.MAIN" />
   4:     <category android:name="android.intent.category.LAUNCHER" />
   5:   </intent-filter>
   6: </activity>

When the application launches, Android filters the available activities, looking for one with an action of MAIN and a category of LAUNCHER, and when it finds it, it uses that to start the application.

For this post, we'll be looking at how a user can go from the main screen to the settings screen. Our settings activity is defined in our manifest as follows:

   1: <activity android:name=".Activities.settings" android:label="@string/app_name">
   2:   <intent-filter>
   3:     <action android:name="com.Develomatic.PayItSquare.Activities.SETTINGS" />
   4:   </intent-filter>
   5: </activity>

I've defined my action to be the same as my namespace, since I plan to call this action explicitly from my main activity. If I had a button on my main screen that took you to the settings, without using my ActivityStarter, it would look like this (this code would be inside a click listener for the button in my activity, for example):

   1: Intent intent = new Intent();
   2: intent.setClassName("com.Develomatic.PayItSquare", "com.Develomatic.PayItSquare.Activities.settings");
   3: startActivity(intent);

Pretty straightforward and works, but it presents a major problem: It can't be tested - for two reasons. First, I'm only doing testing on my business layer right now, and this is in my UI layer. Second, it actually starts an activity - something that will be difficult to verify in an automated fashion.

By introducing an abstraction, I can then verify that the presenter intends to start an activity using a mock, but not actually start the activity. Now, instead of having my activity start a new activity directly, it now signals to the presenter that a button was clicked, and the presenter handles starting a new activity, like so:

   1: private final IActivityStarter activityStarter;
   2:  
   3: @Inject
   4: public MainPresenter(IActivityStarter activityStarter) {
   5:   this.activityStarter = activityStarter;
   6: }
   7:  
   8: public void SettingsClicked() {
   9:   activityStarter.setClassName("com.Develomatic.PayItSquare", 
  10:                                "com.Develomatic.PayItSquare.Activities.settings");
  11:   activityStarter.startActivity();
  12: }

I've included the constructor so you can see that the IActivityStarter is injected in through it. I haven't shown it yet, but one other piece of functionality I've allowed is attaching custom data to the intent through the putExtra() method. This allows me to attach any Serializable class to the intent, and allow the activity handling the intent to have access to the data. From the web world, this is roughly the equivalent of clicking a row in a grid and including parameters on the querystring to an edit screen. Implementing Serializable on your objects is pretty simple, and Eclipse handles it nicely for you if you ask nicely (Ctrl-1 is your friend!).

Now that we're working against an interface, verifying that the intended action was taken can now be done easily:

   1: @Mock private IActivityStarter activityStarter;
   2:     
   3: @BeforeSpecification
   4: public void before() {
   5:   SUT = new MainPresenter(activityStarter);
   6: }
   7:  
   8: @Specification
   9: public void shouldStartANewActivityForSettings() {
  10:   expect.that(new Expectations() {
  11:     {
  12:       one(activityStarter).startActivity();
  13:       allowing(activityStarter);
  14:     }
  15:   });
  16:         
  17:   SUT.SettingsClicked();
  18: }

Now for the hard part: the actual implementation of our activity starter:

   1: public class ActivityStarter implements IActivityStarter {
   2:   @Inject protected static Provider<Context> contextProvider;
   3:   private Intent intent;
   4:     
   5:   private Intent getIntent() {
   6:     if(intent == null) {
   7:       intent = new Intent();
   8:     }
   9:     return intent;
  10:   }
  11:     
  12:   @Override
  13:   public void putExtra(String name, Serializable value) {
  14:     getIntent().putExtra(name, value);
  15:   }
  16:  
  17:   @Override
  18:   public void setClassName(String packageName, String className) {
  19:     getIntent().setClassName(packageName, className);
  20:   }
  21:  
  22:   @Override
  23:   public void startActivity() {
  24:     contextProvider.get().startActivity(getIntent());
  25:   }
  26: }

This class resides in our UI project, as it deals directly with Android classes, but it isn't tied to a particular activity, so it's relatively isolated from the rest of our code. The key piece here is the static context provider injected into this. We covered this in part 3 when we talked about requestStaticInjection() with Roboguice. With a reference to the context provider, we're able to get at the current context on demand, which means we can start new activities in a clean way.

I'd planned to cover a couple of other techniques I'm using, but this went longer than expected, so I think I'll stick with just this one for now, and cover the other techniques in part 5b.

Categories: Android