Yesterday I tweeted something that lead to quite a bit of confusion:

100 replies later ranging from questions to anger and I figured I owe a blog post. The statement above came out of an ASG discussion about how everyone seems to use Presenters differently, particularly when it comes to configuration changes during or after a data fetch. It seems like we all are trying to solve the same problems but in different ways:

Activity/Fragment/View needs to be able to request data

There needs to be a mechanism to cache the data so that on rotation you do not need to hit the network again

Similarly, you should be able to maintain your state after rotation, such as scroll position.

In this article I’ll be going over my preferred way to structure a data driven application.

Some shocking points I’ll be making and explaining along the way:

Presenters should be owned by the Activity/Fragment/View and destroyed/recreated on rotate

Singleton data providers request and own your data

State should be kept minimal and saved/restored on rotation

Let’s setup a common use case that something like a Reddit client would have: MainActivity allows a user to enter a subreddit name. It then calls the Reddit API and shows a list of posts for the particular subreddit. If a particular post in the results is selected, you will go to a CommentActivity which will hit another endpoint and show the comments for the post. To make the example a little more complicated we will have the CommentActivity contain a fragment that shows comments. Some pain points that we should all be aware of:

If a user submits a request for a subreddit and then rotates mid request, you need to be able to keep the request going rather than making a new one, especially if it is an expensive network call like getting 100 posts.

We need to be able to pass data between the 2 Activities. Because our CommentsActivity contains a CommentsFragment, we need to be able to pass data between the activity and fragment or allow the fragment to request its own data.

If we are on the CommentActivity and rotate, we need to be able to restore the proper comments (and maybe the scroll position as well)

Here’s how I would approach the above (with explanations of design decisions)

User enters a subreddit name and then clicks on search:

Activity has an injected Presenter and will call

presenter.getPostsFor(subredditName)

The Presenter has 2 jobs: requesting new data from a data provider/repository/store and then passing that data back to the View (an Activity in this case). Using the simplest approach possible we can inject a network client like Retrofit to our presenter which will act as a data provider and request data from it.

public void getPostsFor(String subredditName) {

subscription = redditApi.getPosts(subredditName)

.subscribe(posts -> getMvpView().showPosts(posts),

throwable -> getMvpView().showError()); }

The activity then implements these:

void showError();

void showPosts(List<Post> posts);

Something that I do that may be different from others is that I make my View own my Presenter. By that I mean the Presenter dies any time an Activity dies. New Activity after rotation means we will have a new Presenter for that instance of the Activity. Think of the Presenter as having the same scope as an instance of an Activity.

What we currently have is a Presenter that acts as the go between our View and our data, the View doesn’t know or care where the posts come from. All it knows is that it needs to request Posts and have callbacks to properly handle when those posts (or an error) are returned. Views (Activity in our case) only care about requesting or showing data while request and subscription implementations live in a Presenter.

Next we need to discuss how we can have a request survive a configuration change. To recap: when a user rotates their device while fetching a new subreddit, we want to show the response after rotation without having to make a second request to the network. There seem to be two schools of thought about how this should work.

The first approach is that we can make our Presenter a singleton which will make it live on a higher scope; or We make a singleton data provider that lives on a higher scope and caches our data

Let’s start by examining the first option. Some suggest that you should make your Presenters singleton or app scoped. This way when you rotate and have a new instance of an activity, you rebind to the same singleton Presenter. The presenter can then own the data and pass it back to the view rather than going to the network a second time.

This adds a bit of complexity, as the Presenter now needs logic for checking if “a request is in flight” before making another request. To me this feels like too much responsibility for a Presenter. In my opinion, Presenters are the middlemen. They request data, they hand it of,f but they shouldn’t be fat or keep data. Another disadvantage to this approach is that many presenters can be holding/retaining the same exact data. What if the MainPresenter and CommentPresenter both want to request a list of Posts, will there be duplicated data within their Singleton Presenters?

I think we can do better. It sounds like all we need to do is cache our data and in-flight requests somewhere that is not scoped to a single Activity. If we can have an abstraction that survives configuration changes, we can recreate our Presenter and then request the same data a second time.

Instead, I’d propose using data stores which abstract away data loading. Rather than the presenter needing to cache the data, we can leverage the Repository pattern and create Data Providers/Stores that can get data from the network and cache it for when we need it. For one thing, this approach makes it easier to persist your data. Another advantage is that we have a single source of truth for our data. Finally, wew can eliminate things like Interactors and just have your Presenter subscribe to your Stores whenever they need something for a view.

so

redditApi.get(subredditName)

becomes

redditStore.get(subredditName)

where the store is a singleton (preferably managed by Dagger) and takes care of requesting new data, caching that data and dealing with in flight request management.

Now when you rotate, the Presenter can simply inject the same singleton Store and re-request data which can piggyback on an inflight request or get the cached data from the Store. From the Views/Presenters perspective it shouldn’t matter if the data is being requested for the first time or coming from a cache; all it needs is the data to pass to the view.

As for an implementation of this pattern, I suggest wrapping your network calls in Stores that take care of caching and in flight logic. We just open sourced our implementation of a library using this pattern at NY Times https://github.com/NYTimes/Store.

Let’s examine this a bit more. Views get recreated, Presenters get recreated too. The only reason I’ve seen to not recreate Presenters is because they contain data that need to survive rotation. I believe that as long as you have an identifier for your data you only need to persist/retain/maintain a singleton provider of your data rather than the entire Presenter. Having a singleton Store that can provide pre and post rotation data gives you all of the advantages of a singleton Presenter without the added weight. Thin views and presenters which talk to single responsibility stores is my preferred setup for MVP. As mentioned above, Presenters now can worry about what the View needs and getting access to it rather than worrying about things like caching and request management (let your store do that).

Rotation logic works like this:

OnSaveInstanceState will save the request params if there are any.

OnCreate will inflate a layout, create a presenter and checks if there are any request params in the saved bundle. If there are, it will request data from the Presenter which will request from the Store. Since data has already been cached by the Store, you should be receiving a cached representation.

If an Application gets backgrounded for a long time and needs to recreate, it will work exactly the same as in a rotation. There’s no need to save your entire presenter or data model in save/restore instance state eliminating parcel too large exceptions. Since the Store owns the data, you can use much better persistence techniques like flat files or sql lite which are not available at the UI level. Both of these storage techniques have less size constraints than an activities save restore bundle mechanism.

OK, back to our example which is now powered by a Presenter and a Store. The user enters a subreddit name, clicks on search which delegates the call to the Presenter who made a Subscription to the Store and passed back the results to the Activity. Now that the user sees posts, he clicks on one to see the comments.

We then open CommentActivity and pass it a postID within an Intent. Activity B creates a Fragment and passes the Intent Extra to it. Fragment inflates a Layout, creates/injects a CommentsPresenter and asks the Presenter for the comments for a particular post. Similar to our last Activity/Presenter, the CommentPresenter will get a reference to a singleton CommentStore which it will pass the postID to and ask for a list of comments. As in our MainActivity example if a rotation occurs the fragment will save the current PostID and restore it when the fragment gets recreated.

The CommentFragment has 2 starting states: frag args containing postID or saved state contains postID, in either case the Fragment creates a Presenter and passes the postID to it for retrieval of a list of comments. As you can see by having a Store abstraction we can keep our logic for fetching pre or post rotation exactly the same. If any other state needs to be maintained it should be minimal and saved/restored same as the request params. Since you are only ever saving/restoring states that should be just a few flags.

In conclusion I feel that the simplest way to work with data is to have an Activity/Fragment/View which owns a Presenter. That Presenter can request data from an app scoped Store which can either fetch new data or return cached data. This setup works for me and I hope it will work for you too.

Feedback is loved and appreciated.