On Navigation In Android-MVP

Nikita Barishok
8 min readApr 5, 2016

--

Photo credit: http://netdna.webdesignerdepot.com/

Although MVP approach for Android seems to be covered thoroughly on the web, there are still some dark places in it. And one of them is navigation mechanism. On the one hand, there are proven concepts about navigation, but on the other hand, there is harsh reality in the face of Android platform with all it’s niceties + non-trivial task of app’s architecture flexibility maintenance. So here is an attempt to sort things out.

UPD: this article contains of some sort of theoretical and practical parts. And the practical one is based on my previous story On Communication Between V and P in Android-MVP(link to story).

And as far as I can see stats(and there is no surprise, I’d better done it earlier), there are readers who are more interested in exploring code itself and may be reading articles after that to better grasp things, so here is the project containting the whole code of all the stories and even more (link to project). You are welcome to investigate it.

Talks About Navigation In Android-MVP

Opinion exists that navigation is the responsibility of presenters. And as far as navigation commonly has Android-specific dependencies, like Context/Activity/FragmentManager/etc., navigation logic should be wrapped in NavigationCommand. So the it looks like this:

Usage of this NavigationCommand is rather straightforward:

Everything seems valid, perfectly decoupled and smooth.

…So What’s The Deal?

When working with such an abstract example, there seems to be no problems. The code definitely works. But while abstracting away navigation in NavigationCommand it good thing to do, using it in presenters leads to some questions.

At first, let’s recall what are the responsibilities of presenter? Here it is: handle signals from user + transform data from model into view model + update the view. Then, what are the responsibilities of view? To be passive, delegate user input to presenter and to handle visual behavior.. Hmm.. To handle visual behavior. And the thing is navigation is something visual, isn’t it? I totally can see transitions from one screen to another, for example.

At this point the doubt appears. What if view is responsible for doing the navigation stuff?..

Another Truth

The reasons for doubts don’t stop here. But before moving further, let’s think about what is the navigation actually?

It is valid to consider these cases as navigation:

#1. starting activity

#2. if you are the-more-struggle-the-better kind of guy, working with fragments

#3. even #1 and #2 aren’t enough for you, then showing dialogs (using DialogFragment, of course). Yes, why not? It is a valid (not that acceptable in many scenarios though) navigation case. And basically it is #2 under the hood.

Interrupting the user with a pop-up notification(1) #uxreactions

#1 is operated on app context so it can be done without any limitations using aforementioned NavigationCommand objects. But navigation using #2 or #3 changes state of the current view(it could be acitivity or fragment), this type of navigation is bound to the lifecycle. What I mean is the fact that even if you wrap this logic in NavigationCommand, invoking this command after onSaveInstanceState() will make your app crash. The recipe of such a crash is simple: navigation as a result of some async operation in presenter + user [minimizing app]/[switching to another app]/[rotating device] while operation is in progress.

So, here is the picture for now:

  • navigation is all about changing UI
  • all navigation should be lifecycle-aware (NavigationCommands should be executed between onResume() and onPause() of view). That’s necessary, as far as you want your app to continue working after changes like “moving to activity” → “adding a fragment”(2) for NavigationCommand or “sync navigation invocation” → “navigation invocation after network request”(3);
  • presenter by its’ nature should be unaware of such niceties of an Android platform like intervals when navigation can be done;

Things getting complicated…

OK Everything Is Bad, Give Me The REAL Solution

In the approach that I want to introduce it is view’s responsibility to handle navigation. For me it seems natural and here is why. Two types of navigation can be distinguished: one is like changing text in TextView in current view= moving view to another state = state-based navigation, and another type — is like opening dialogs = moving from current view to some another view = view-based navigation. So as there are no questions for the chain

→[view] userClicksButton()

→ [presenter] handleClick()

→ [view]updateTextView(),

there should be no questions for the chain

→[view]userClicksAnotherButton()

→ [presenter]handleAnotherClick()

→ [view]showInfoDialog().

Both updateTextView() and showInfoDialog() change the UI = change the state of UI = navigate to another state of UI, so treating all of these events as navigation is logically valid thing. More than that, when treating everything in a single manner, communication between view and presenter becomes consistent as it should be, thus easy to write and understand.

But there is another tricky question to answer. And it is related to the fact that presenters are not supposed to be aware of view lifecycle, but the navigation should be lifecycle-aware. The only useful information presenters have is whether the view is attached (and it is true between onResume() and onPause()). So the next solution would work:

But while this approach is safe in terms of app crashes, the cost of such an achievment is too high: ViewState loss. If result comes after onPause(), user will not see the dialog afterwards.

Therefore, before moving to the solution some improvement to the basic Android-MVP approach should be made. It touches neither presenter, nor view in [view] → [presenter] →[view] chain, but “→” themselves. This improvement is connected to introducing a transparent layer between view and presenter that is responsible for the following:

  • Managing view lifecycle, so presenter can talk to view at any desired moment without null-checks;
  • Tracking ViewState, so no matter what is done to the view (rotation, other config changes, low-memory), it’s state stays up-to-date and can be restored;

So the idea is really great, it helps presenter and view to do what they are supposed to do and nothing more, all the complexities of managing lifecycle and keeping view up-to-date are left for that transparent layer (it acts like some sort of Sticky-Communication-Bus between view and presenter). I described implementation details in the post about communication between V and P. And the code for the story is here, you might explore it as well.

The most important about introducing CommunicationBus layer for us now is how communication is handled in direction from presenter to view:

  • CommunicationBus acts transparently, so presenter thinks it sends events to the view, but actually it sends events to the CommunicationBus (example of code). CommunictionBus tracks ViewState and dispatches events to the view if it is available.
  • View thinks it talks to presenter, but actually it talks to CommunicationBus, that dispatches events to presenter and restores actual state of the viewusing ViewState when view binds itself to presenter by calling attachView(View view) (example of code).
  • The ViewState concept is introduced. ViewState is a model of actual state of the view.
  • CommunicationBus is a POJO and tracks ViewState all the time, it even survives configuration changes, backs ViewState up on low-memory, so that view can be restored in its’ actual state after rotation/being vanished away on low-memory etc.

It means, that if user initiated, for example, loading, thus making view show loading indicator, then rotated the device during operation, the view after being recreated will continue to show loading indicator and then will show data loaded by the same operation. That’s good because of better UX and more optimized resources usage (results from the same operation are delivered)(4).

But, while the approach plays well in cases like one above with loading indicator, it fails to handle navigation. Why? Lets look at how CommunicationBus generally handles communication from presenter to view:

And restore process (when view thinks it attaches to presenter, but in fact to communication bus):

And the thing is operations like showing loading indicator, or updating view with content... They all are changing current ViewState. And opening an acitivity or showing a dialog is a moving to a new ViewState, it doesn’t affect the current ViewState.

So for navigation the next scheme should be used:

It looks like the previous one, but here we are not saving operation if it can be performed at the moment, because it is not something that we want to restore after rotation. If view-based navigation can be done at the moment, just do and forget about it. And if can’t be done, then save it as pending operation. All the pending operations are stored as a separate class member, because state-based and view-based navigations live separately and overall state is their combination. So basically ViewState will look like this:

And the restore process will change accordingly:

OK, so ViewState, along with tracking state of the view itself, is now able to track possible pending navigations and restore all of it. I used a list for storage, because it’s unlikely but still possible to have several pending navigations.

Doing so navigation will be handled correctly even if it is invoked after async processing while view is being recreated and even if navigation operation has some data as parameter (view-based operations can be persisted along with their parameters). CommunicationBus will manage all the logic related to keeping ViewState up-to-date and restoring it and all of its’ pending navigations. Nice!

Wrap up

That is how the skeleton for reliable navigation in Android-MVP can look like. In this approach the view is responsible for all of the navigation, both view-based and state-based. And have no problems using this approach. It actually could be done at the CommunicationBus level, but I can’t see any benefits of doing so. But there is no way for handling navigation in presenter itself, it is unacceptable, because of view-based navigation constraints in terms of lifecycle.

You can find the actual code for this approach along with other modifications and improvements here in repository.

It is WIP, but for now I just want to share the idea behind navigation in Adnroid-MVP and steps for making it reliable.

Hope you’ve found this story helpful. Thank you:)

(1) it doesn’t mean dialogs are totally evil. There are cases when they are appropriate

(2) consider master-detail example: navigation to another activity on phones and replacing a detail-fragment on tablet.

(3) example: login fragment. User taps on “login” button, chain of network requests launches and if it appears that such a mail is already exisits, dialog should be shown + if everything is fine, then replace login fragment with welcome fragment.Here if user minimizes app while network requests are processing, there is a crash when processing is done and it’s time for either of navigations.

(4) The operation scheme of CommunicationBus for the example is the next:

save SHOW_LOADING state of the view, when presenter says “showLoading()” and then propogating the signal to view. Then, if device is rotated, the new intance of view is going to be created and attached to the same CommunicationBus, which has it’s actual state, so it can be restored in onResume() of the new view. If, while view was recreating, the result was deveivered to presenter, it will invoke “showContent()” on CommunicationBus, and CommunicationBus will update view state, but will not propogate that signal to view (it is in recreation process, thus null), but then will restore actual SHOW_CONTENT state on the new recreated view when it attaches.

--

--