MVP - Presenters That Survive Configuration Changes (Part 1)

This is somewhat of a continuation from my last blog post, where I ended with a requirement of having Presenter classes that would pair with views but be free from the lifecycle burdens that come with Android components.

I'll start by mentioning Mortar. Mortar is a library by Square that is designed to solve this problem, but in my experience it doesn't help very much at all. Not only does the library seem unnecessarily complicated, it requires a lot of manual work anyway (e.g. building and destroying MortarScopes at the correct times). When paired with Flow, some of this pain is alleviated. Despite this, the Flow/Mortar samples I have seen are still quite complicated, and even I find myself not quite understanding how things are put together at times. My personal preference is to continue using Fragments and the pattern described in this article.

Note: I will post a GitHub link at the end of this article that uses Mortar + the technique described in the article for reference. If anyone reading this endorses Mortar and feels I have miss-represented it - please leave a comment - my experience with the library is minimal.


First thing's first: what are we trying to achieve?

I would like Presenter classes that persist until the view is "finished". By finished, I mean that the user has exited the view. This might happen when the press the back button, or when they perform some action that explicitly finishes it's containing activity, for instance.

To give an example, say we have a view called A. When A is created, a Presenter is created with it. When we change configurations, view A' (the new instance) will continue to use the same presenter instance. If the user exits the view by pressing the back key, the presenter will be destroyed along with the view. Now when the user starts a new instance of A, a brand new presenter instance will be created with it too.

Let's add another view into the mix and call it B. If A starts B (adding it to the back stack), then the back stack will be A, B. In this instance, A's presenter instance should be retained, because A is still part of the current flow.


So now that we know what we're trying to achieve, we need to figure out how to determine if a view is "finished" for good, or just finished temporarily (due to a config change, or low memory conditions, or remaining in the back stack, etc).

Note: for this article I will solve this problem using Fragments, although the same process could easily be applied to Activities.

Turns out we can gain this information from combining information from two lifecycle methods: Fragment#onSaveInstanceState(Bundle) and Fragment#onDestroy().

Let's start by focusing on when Fragment#onDestroy() gets called because it's the easiest. Fragment#onDestroy() gets called when the Fragment instance is being destroyed. This doesn't mean that the Fragment won't be created again though. The Framework will re-instantiate Fragments when needed (e.g. when returning to it from the back stack, or after a configuration change, etc). Therefore this callback alone doesn't provide enough information.

Now for the tricky one, Fragment#onSaveInstanceState(Bundle). The system will call this method if it knows that the view will likely be re-created at some point (either immediately after a configuration change, or at some later date). Take extra care when utilising this method for information though, because the documentation is not clear on when it is called. The following quote is from the documentation:

Do not confuse this method with activity lifecycle callbacks such as onPause(), which is always called when an activity is being placed in the background or on its way to destruction, or onStop() which is called before destruction. One example of when onPause() and onStop() is called and not this method is when a user navigates back from activity B to activity A: there is no need to call onSaveInstanceState(Bundle) on B because that particular instance will never be restored, so the system avoids calling it. An example when onPause() is called and not onSaveInstanceState(Bundle) is when activity B is launched in front of activity A: the system may avoid calling onSaveInstanceState(Bundle) on activity A if it isn't killed during the lifetime of B since the state of the user interface of A will stay intact.

This states that onSaveInstanceState(Bundle) is not always called when a view is pushed to the background. It will only be called when that view is about to be destroyed and if the system knows that it might come back. Therefore, if we use the knowledge that this method was called within Fragment#onDestroy() then that is all the information we need. We can now determine if the fragment is being destroyed and it will never come back.


Now let's see some code!

PresenterCache.java: This is a singleton that we can store all of the presenters in.

public class PresenterCache {
    private static PresenterCache instance = null;

    private SimpleArrayMap<String, Presenter> presenters;

    private PresenterCache() {}

    public static PresenterCache getInstance() {
        if (instance == null) {
            instance = new PresenterCache();
        }
        return instance;
    }

    /**
     * Returns a presenter instance that will be stored and 
     * survive configuration changes
     *
     * @param who A unique tag to identify the presenter
     * @param presenterFactory A factory to create the presenter
     *        if it doesn't exist yet
     * @param <T> The presenter type
     * @return The presenter
     */
     @SuppressWarnings("unchecked") // Handled internally
     protected final <T extends Presenter> T getPresenter(
         String who, PresenterFactory<T> presenterFactory) {
        if (presenters == null) {
            presenters = new SimpleArrayMap<>();
        }
        T p = null;
        try {
            p = (T) presenters.get(who);
        } catch (ClassCastException e) {
            Log.w("PresenterActivity", "Duplicate Presenter " +
                  "tag identified: " + who + ". This could " +
                  "cause issues with state.");
        }
        if (p == null) {
            p = presenterFactory.createPresenter();
            presenters.put(who, p);
        }
        return p;
    }

    /**
     * Remove the presenter associated with the given tag
     *
     * @param who A unique tag to identify the presenter
     */
    public final void removePresenter(String who) {
        if (presenters != null) {
            presenters.remove(who);
        }
    }
}

PresenterFactory.java: An interface for a class that will create new presenter instances

public interface PresenterFactory<T extends Presenter> {

    /**
     * Create a new instance of a Presenter
     *
     * @return The Presenter instance
     */
    @NonNull T createPresenter();
}

SomeFragment.java: A view implementation

public class SomeFragment extends Fragment implements SomeView {

    private static final String TAG =  SomeActivity.class.getName();

    private PresenterCache presenterCache = 
                      PresenterCache.getInstance();
    private boolean isDestroyedBySystem;
    private SomePresenter presenter;

    @Override 
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        presenter = presenterCache.getPresenter(TAG,
                          presenterFactory);
    }

    @Nullable @Override
    public View onCreateView(LayoutInflater inflater,
           ViewGroup container, Bundle savedInstanceState) {
        return inflater.inflate(R.layout.fragment_example, 
                  container, false);
    }

    @Override 
    public void onViewCreated(View view, Bundle savedState) {
        super.onViewCreated(view, savedInstanceState);
        presenter.bindView(this);
    }

    @Override public void onResume() {
        super.onResume();
        isDestroyedBySystem = false;
    }

    @Override 
    public void onSaveInstanceState(Bundle outState) {
        super.onSaveInstanceState(outState);
        isDestroyedBySystem = true;
    }

    @Override public void onDestroyView() {
        super.onDestroyView();
        presenter.unbindView();
    }

    @Override public void onDestroy() {
        super.onDestroy();
        if (!isDestroyedBySystem) {
            presenterCache.removePresenter(TAG);
        }
    }

    private PresenterFactory<SomePresenter> presenterFactory = 
    new PresenterFactory<SomePresenter>() {
        @NonNull @Override 
        public SomePresenter createPresenter() {
            return new SomePresenter();
        }
    };
}

I have put up a sample project here:
https://github.com/grandstaish/hello-mvp

For comparison, I have done the same application using mortar:
https://github.com/grandstaish/hello-mvp-mortar


This code achieves our goal, but it can be greatly improved from here:

  • Wouldn't it be great to inject our presenters using a DI library like Dagger 2?
  • That static cache is a bit ugly
  • Those Fragments are duplicating a lot of code, it would be nice to push some of the configuration into a base class

Part 2 of this article will address these issues.

Brad Campbell

Android application developer, currently working as the lead on the ANZ goMoney NZ applications. All of the opinions and code on this blog are my own, and not that of ANZ.