A problem like Navigation — Part 2

Maria Neumayer
A problem like Maria
5 min readMay 24, 2018

Last week I wrote about my initial experience using the new Navigation Architecture Component. But after my first few days of working with it I still had many questions. How do I pass data back to the origin? How does conditional navigation actually work in practice? How can I start navigation for a common flow and get back to the origin? So I got stuck in and tried to figure them out.

Passing data back to the origin

When working with activities there’s a simple way of getting a response back from a started Activity using startActivityForResult and onActivityResult. For fragments that doesn’t exist, and as the navigation component mainly works with fragments I had to find an alternative. When navigating from one fragment to another all you need is an action — you don’t have to know what Fragment is being started, so trying to couple that with a callback seemed like the wrong approach. However we can make use of another component: The ViewModel. Most of my ViewModels are scoped to a Fragment. However they can also be scoped to an Activity. This means we can use it to communicate between fragments. So how does this work?

To get a ViewModel we have to use the ViewModelProvider:

ViewModelProviders.of(this).get(HomeViewModel::class.java)

So how do we get an Activity scoped one? The trick lies in the this. All we have to pass in here is requireActivity() and you get an Activity scoped ViewModel. But how can we use that to pass data between the fragments? The answer is LiveData. Say you want to find out which item was selected in the next Fragment. You can define a LiveData event in your ViewModel:

val itemSelectedEvent = MutableLiveData<Event<Item>>()

When selecting the item set the value of the data like this:

itemSelectedEvent.value = Event(item)

On the origin side you have to observe this event:

viewModel.itemSelectedEvent.observe(this, EventObserver { ... })

That’s all! Now you get an event when navigating back to the origin Fragment. There’s a few things in here that I didn’t explain yet— where did Event and EventObserver come from?LiveData will fire an event when resubscribing. This is great when you want to make sure your UI always has the latest state, however in this case we only want to get the event once. Jose wrote a great article about how to solve that problem and I am using his suggestion here, so check it out if you want to learn more.

With this it’s very simple to pass data back to the origin without tightly coupling them, which is great. I do wish, though that there was a way to integrate that more into the Navigation component.

Conditional Navigation

Sometimes to access a screen you have to be in a certain state to be able to access it. A common case for this would be being logged in or having gone through some setup flow. The navigation component supports that, but the documentation for it is a bit light right now, so it took me a while to figure out how to do it right. So let’s take a look at how we could handle that. Let’s start with checking if you’re logged in when opening a screen and going to a login screen if not.

if (!isLoggedIn()) {
findNavController().navigate(R.id.login)
} else {
...
}

Note: There’s currently a crash when you instantly try to navigate to a new place — you can work around that by using a post for now.

That seems quite simple. But what happens if you then hit back? You’ll end up on the same screen, which you shouldn’t be able to access without being logged in. We can run the same condition as above again, however then you just end up on the login screen again. So how do we know that you actually came back from the login screen? The answer again is: LiveData and an Activity scoped ViewModel. When starting the login flow we can set the LiveData value to something like LOGIN_STARTED and once we’re done we can change it to LOGIN_FINISHED. On the origin side we can observe the state and know that you either have to pop back to the previous screen or, if logged in, show the content. The flow is similar to startActivityForResult — there you have RESULT_OK or RESULT_CANCELLED to define if a flow was finished or not.

Starting the login flow can be quite a common use case within your app. A navigation action is defined per screen — will I have to repeat the action everywhere? The answer is no, thanks to global actions.

Global actions

A global action is defined just like any other option — it is just within the navigation element instead of the fragment:

<navigation
android:id="@+id/main">
<action
android:id="@+id/start_login"
app:destination="@id/login"/>
</navigation>

You might’ve noticed that the navigation element has an id. This is not added by default, but for global actions to work we need to define one. This is especially needed for safe args as a NavDirections class will have to be generated and this takes the id for the class name.

To navigate back all we have to do is call popBackStack() and we’re back on the origin. But what if the login flow consists of multiple screens? Maybe a user will actually sign up instead of logging in — how do you get back to the origin screen when completed?

First: I recommend to keep the login flow as a nested flow. The navigation graph supports nesting multiple flow in the xml. Just add another navigation element inside like this:

<navigation
android:id="@+id/main">

<action
android:id="@+id/start_login"
app:destination="@id/login"/>

<navigation
android:id="@+id/login"
app:startDestination="@id/login_fragment">
<fragment
android:id="@+id/login_fragment">
<action
android:id="@+id/signup"
app:destination="@id/signup_fragment"
</fragment>
<fragment
android:id="@+id/signup_fragment">
</navigation>

</navigation>

This way the login flow is defined separate — the first screen of it will always be the same, no matter where you start it from. If you want to change that all you have to do is change the startDestination.

But how do you get back to the origin when finishing signup? Just calling popBackStack will just take us back to login. There’s two options:

First: we can call popBackStack with an id:

findNavController().popBackStack(R.id.login, true)

What we’re doing here is popping back beyond the login destination. As above we defined login to be the start of the login graph — no matter which screen goes first we will always exit the flow. Also because we haven’t defined the actual origin this will work great for global actions as you land back on the screen the action was started from. The true means that we’re including the login destination, meaning it will also be popped. If you just want to go back to the beginning of the login flow just pass false here.

The second option is to define a separate action in your navigation flow:

<action
android:id="@+id/finish_login"
app:popUpTo="@id/login"
app:popUpToInclusive="true"/>

This is basically the same as option 1, but defined in xml. To navigate back we can just navigate to this destination:

findNavController().navigate(R.id.finish_login)

I prefer this option because what actually happens is defined in your navigation graph. What if you actually want to show another screen post login? You just have to update the action and everywhere you navigate to finish_login it’ll actually show that screen.

That’s it for now — there’s still a lot to uncover. As I continue working on my project I’m sure I will discover more interesting things along the way. I want to thank everybody who helped me — especially Ian Lake who answered all of my many questions I had.

So what is your favourite feature in the Navigation Architecture Component? What have you found that I haven’t mentioned yet?

--

--

Maria Neumayer
A problem like Maria

European living in London. Principal Engineer at @SkyscannerEng. Tweeting at @marianeum.