Android Development: Part 2: Presenters and Activities
posted on 09/08/10 at 04:18:22 am by Joel Ross
This is part 2 of an on-going series on Android development (or at least my warped interpretation of it). If you missed the introduction of the application or a discussion of the tools, you might want to check those out first.
I mentioned earlier that I'm using an MVP pattern, but didn't go into much more detail than that. Well, that's about to change, as that's exactly what this post is all about.
In the Android world, Activities are the equivalent of views in MVP. The way Android works is that Intents are responsible for activating activities. If you're not familiar with Android development, that probably didn't make a lot of sense, but the bottom line is that Android is view-first rather than presenter-first. This has some implications in how the presenter and activity interact, so it's important to note that.
I've picked a pretty simple screen in my application to demonstrate how my activities and presenters work together. It's a screen where you set your user name and password to be used to connect to Pay It Square. Note that, for now, I?ll ignore how the data is actually stored and retrieved, and just leave that logic hidden behind an interface.
While Android starts with the activity, I like to start with my tests. I am not testing my Activities right now, since I plan to keep them relatively thin. The presenter will essentially be responsible for two things: populating the activity with the existing settings, and saving those settings back to the data store when the activity says it's time. So let's write a couple of specs to indicate that.
As I said in part 1, I'm using Instinct as my testing framework, so that?s what I'm going to show. The basic test spec looks like this:
1: @RunWith(InstinctRunner.class)
2: public class SettingsPresenterSpecs {
3: @Subject private SettingsPresenter SUT;
4:
5: @BeforeSpecification
6: public void before() {
7: SUT = new SettingsPresenter(preferenceRepository);
8: }
9:
10: @Specification
11: void shouldSetUserAndPasswordOnView() {
12:
13: }
14: }
This doesn't do anything except ?new up? the presenter. I've defined a spec, but there's no expectations set up yet. I've also defined my presenter as the subject of the test, and marked it to run with the Instinct test runner. But essentially, this spec doesn't do anything. So let's fix that. I know I'll have a data store to hold the user's current username and password, as well as the activity. Both will be interfaces, and we'll talk about wiring that up in a bit. For now, we'll assume the presenter will be constructor injected. This changes things a bit, and I now need two mocks: one for IPreferenceRepository and one for ISettingsActivity.
Side note: I'm trying to get into the ?Java way? of naming things, but I'm still using "I" to prefix my interfaces. I'm showing my C# bias, but it is what it is.
Now, before we update the spec to reflect our changes, there's one thing that isn't quite right. I intend to use an IoC container to create my presenters, so the container will be responsible for instantiating any dependencies the presenter needs. But we said earlier that the Activity is where the process starts in an Android app, so our container won't be activating the activity for us. As a result, it can't be constructor injected. It will have to be passed in later. I've settled on a base presenter that handles that for me through a call to a method called InitializeWith(activity);. Updating our spec, we end up with this:
1: @RunWith(InstinctRunner.class)
2: public class SettingsPresenterSpecs {
3: @Subject private SettingsPresenter SUT;
4: @Mock private ISettingsActivity activity;
5: @Mock private IPreferenceRepository preferenceRepository;
6:
7: @BeforeSpecification
8: public void before() {
9: SUT = new SettingsPresenter(preferenceRepository);
10: }
11:
12: @Specification
13: void shouldSetUserNameAndPasswordOnView() {
14: SUT.InitializeWith(activity);
15: }
16: }
You can see how the repository is injected in the constructor, and since the presenter begins its interaction with the activity once InitializeWith is called, I've added that to the spec. Notice that Instinct provides simple ways to create mocks by adding @Mock onto the declaration.
The only thing missing is what we expect to happen. Instinct provides a way to set up expectations on my mocks, so that's what we'll use. First, the presenter should get the current user preferences and then it should set those on the activity. To do that, we update our spec to include those expectations:
1: @Specification
2: void shouldSetUserNameAndPasswordOnView() {
3: expect.that(new Expectations() {
4: {
5: one(preferenceRepository).get(); will(returnValue(new Preference("jross", "test1234");));
6: one(activity).setUserName("jross");
7: one(activity).setPassword("test1234");
8: }
9: });
10:
11: SUT.InitializeWith(activity);
12: }
We're stubbing the call to preferenceRepository.get() to return a preference, and then we're expecting it to be set on the view. Next, we need a spec to save the updated username and password. That's very similar:
1: @Specification
2: void shouldSaveUserNameAndPassword() {
3: expect.that(new Expectations() {
4: {
5: one(preferenceRepository).save("jross2", "test4321");
6: }
7: });
8:
9: SUT.Save("jross2", "test4321");
10: }
Now, let's move onto the presenter, and get these tests to pass. Like I said, I have a base class for my presenter. It doesn't do much, but it helps with the interaction with the activity:
1: public class BasePresenter<T extends IActivity> {
2: private T activity;
3:
4: protected T getActivity(){
5: return activity;
6: }
7:
8: protected BasePresenter() { }
9:
10: public void InitializeWith(T activity) {
11: this.activity = activity;
12: }
13: }
Nothing too fancy here, but it cleans up some repetitive code in my presenters. Here's my SettingsPresenter that satisfies those specs we created:
1: public class SettingsPresenter extends BasePresenter<ISettingsActivity> {
2: private IPreferenceRepository preferenceRepository;
3:
4: @Inject
5: public SettingsPresenter(IPreferenceRepository preferenceRepository) {
6: this.preferenceRepository = preferenceRepository;
7: }
8:
9: @Override
10: public void InitializeWith(ISettingsActivity activity) {
11: super.InitializeWith(activity);
12:
13: Preference preference = preferenceRepository.get();
14: getActivity().setUserName(preference.getUserName());
15: getActivity().setPassword(preference.getPassword());
16: }
17:
18: public void Save(String userName, String password) {
19: preferenceRepository.save(userName, password);
20: }
21: }
The implementation is pretty simple. About the only thing to note is the @Inject on the constructor. This is what signals Roboguice to use that constructor when an instance of the SettingsPresenter is requested.
This brings us to the activity. Remember, this is activated by Android when an Intent is fired that this activity matches. I'll talk about firing intents in a future part, but for now, it's enough to know that the activity will be created for us.
Whether you realized it or not, we defined an interface for our activity:
1: public interface ISettingsActivity extends IActivity {
2: void setUserName(String userName);
3: void setPassword(String password);
4: }
We also know that the activity will have a couple of text boxes and a button, and when the button is clicked, it should call Save() on our presenter. With that knowledge, we can implement our activity. To get injection working for Roboguice, our activities will extend GuiceActivity. And of course, we'll need to implement our ISettingsActivity interface.
Before we dive into the code, another nice thing that Roboguice provides for you is injection of screen elements. Normally, you would have a field to represent the screen element like this:
1: private TextView getUserName() {
2: (TextView) findViewById(R.id.user_name);
3: }
With Roboguice, this becomes:
1: @InjectView(R.id.user_name) protected TextView userName;
It's not a huge savings, but it looks much cleaner when viewing an activity with a lot of UI elements. Anyway, onto the activity.
1: public class settings extends GuiceActivity implements ISettingsActivity {
2: @Inject protected SettingsPresenter presenter;
3: @InjectView(R.id.save) protected Button saveButton;
4: @InjectView(R.id.user_name) protected TextView userName;
5: @InjectView(R.id.password) protected TextView password;
6:
7: @Override
8: public void onCreate(Bundle savedInstanceState) {
9: super.onCreate(savedInstanceState);
10: setContentView(R.layout.settings);
11: saveButton.setOnClickListener(saveClick);
12: presenter.InitializeWith(this);
13: }
14:
15: public void setUserName(String userName) {
16: this.userName.setText(userName);
17: }
18:
19: public void setPassword(String password) {
20: this.password.setText(password);
21: }
22:
23: private OnClickListener saveClick = new OnClickListener() {
24: @Override
25: public void onClick(View v) {
26: presenter.Save(userName.getText().toString();, password.getText().toString());
27: finish();
28: }
29: };
30: }
I have fields for the three elements: userName, password and the saveButton. I marked my presenter with @Inject so that it gets instantiated for me, and then I'm calling presenter.InitializeWith(this) in the onCreate method. I'm also wiring up the save button to call presenter.Save() when clicked.
At this point, we now have an activity that injects itself into the presenter, a presenter that handles setting up the activity, and then processes actions taken on the activity.
I've purposely left out a couple of things from this: how Roboguice is actually wired up and the different XML files that Android needs to layout the UI. I do plan to cover those in future parts, starting next time with more information about Roboguice.
Categories: Android