Android Jetpack Navigation From iOS Developer Perspective

Marco Muccinelli
The Startup
Published in
10 min readMar 14, 2020

--

From an iOS developer perspective, how screens are presented in an Android is quite strange. The first time I developed an Android app, I was taught to create an Activity per screenful of content. Then, they have introduced me to Fragments, at the same time warning me about their cumbersome behavior and the lack of a proper constructor.

I was so confused and, searching the web for a simple alternative to navigate from a list to a detail screen, I encountered Conductor project. Basically, they embrace a single Activity structure of the project and they introduce Controller objects able to present Views. It sounds a lot like UIViewController, isn’t it?

More recently, I googled again, and I discovered Android team has introduced (many months ago) a Jetpack component, called Navigation. At a first glance it’s very similar to Apple’s storyboards and I would like to try it from an iOS developer perspective.

Installation

I create a new project with Android Studio (version higher that 3.3), selecting Empty Activity template, with Kotlin, AndroidX support, and API 19 as minimum deployment target.

To include Navigation support in your project, add the following dependencies to your app’s build.gradle file (check latest available version on Jetpack page):

Add also this option to android group:

To enable SafeArgs, you need to add dependency to your project’s build.gradle file:

At this point your project should launch properly on emulator with a white Hello World screen.

Push and Pop

Let’s push our first screen on navigation stack. I know, push is not the proper verb in Material Design world, but I am trying to translate from my iOS mindset.

First of all, I’ll create a MasterFragment and a DetailFragment using Android Studio wizard. I choose a blank fragment, with XML layout, but without any method or interface.

Then, I add a navigation graph to project: right-click on the res directory and select New > Android Resource File. Resource type must be Navigation. I have chosen to call it main, because it reminds me about Main.storyboard file.

Click on Add Destination button and add our two destinations.

Little house icon denotes masterFragment is start destination.

Select masterFragment and drag from it, to detailFragment to create an action (a segue) from these two destinations.

Last step is to tell where this graph should be displayed. Open activity_main.xml layout file, and insert a NavHostFragment. Its XML will be like this:

Please note app:defaultNavHost="true" attribute ensures that NavHostFragment intercepts the system Back button. Only one NavHost can be the default.

I omitted I had changed master fragment layout a little bit respect default one

Navigating to a destination is done using a NavController, an object that manages app navigation within a NavHost. Each NavHost has its own corresponding NavController. You can retrieve a NavController by using one of the following methods:

So, it’s trivial to push detail fragment responding to a button tap:

Now, what about navigation bar? I know, it doesn’t exist in Android. But I want top bar to behave like that! First of all, we disable global action bar by changing adding these attributes to theme in res/values/styles.xml.

Then, add a Toolbar inside activity_main.xml.

This is much more flexible than iOS counterpart, because you could place bar wherever you like. You could also insert toolbars inside fragment, if you don’t need a fixed navigation bar on top. In MainActivity you need to configure navigation controller to hook to this toolbar:

Please note that title is specified in label field of destination, inside navigation graph.

You can animate transitions by selecting action in navigation graph, and picking you animation type from right sidebar. You can even design custom transitions.

Passing Parameters

When you develop for Android, you have to forget the pass-the-baton approach you would use on other platforms. Fragment initializers are a nightmare — an we are avoiding them completely by using Jetpack Navigation, thumbs up — and activity are recreated from scratch often, even when user rotates its device.

Navigation controller navigate() method can be called with a Directions object. SafeArgs package is able to sythentesize direction objects from navigation graph. If you select a destination in your graph, you can add an argument from editor right sidebar.

Then, if you select the action, you can set default values for each argument. If an argument type supports null values, you can declare a default value of @null.

You can pass and receive almost every primitive type that can be serialized inside a bundle. In general, you should strongly prefer passing only the minimal amount of data between destinations. For example, you should pass a key to retrieve an object rather than passing the object itself, as the total space for all saved states is limited: your app will crash if available space finishes.

A class is created for each destination where an action originates. The name of this class is the name of the originating destination, appended with the word Directions. Click on Build > Rebuild Project, if necessary to see the autogenerated class. This class has a method for each action defined in the originating destination.

A class is created for the receiving destination. The name of this class is the name of the destination, appended with the word Args. You can use Kotlin delegation to grab arguments:

Then, you will use them in your code:

This technique could be used to get custom titles for screens. Insert {title} as label for destination. Then, you can use it as a parameter in direction class

Passing Back Results

Because we cannot keep references of fragments, we cannot even think about delegation or to set callback to communicate results from detail to master.

First option you have is to pass back an elementary value inside the backstack entry. You should not abuse this feature, because space is limited — but it could be handy. In master fragment you start observing a key:

Detail fragment can update result value when it desires:

Contact! Please note that observer is invoked when master fragment is hidden.

Suggested option is to create a shared ViewModel. I’m not too excited about this architecture, because it smells like glorified global state to me. Not to mention the waste of memory in a deep navigation tree. But, if you structure you data properly, this pattern could be quite powerful.

First of all let’s create a ViewModel. I will adopt the more recent variant, the one with SavedStateHandle in order to support process death, not only configuration changes (like device rotation). Because you are writing inside this handle, you still need to be aware of bundle size when you pass around state. If you don’t save to bundle, say for volatile data, you have no limit except RAM size. I want to expose only immutable LiveData and to provide methods to update values: this will help sooner than you think.

For simplicity, let’s create view model in our single activity scope:

Then master fragment could subscribe to view model in order to update an example text view:

Default value is 0: correct!

Also detail fragment should be completely driven by view model observation, even for first EditText field content:

Whenever text field is edited, view model has to remain the single source of truth.

Since master fragment is observing a shared view model instance, label value is automatically correct as soon as we come back.

Modal flow

Even if modal presentation of screens is not a Material Design concept, we can simulate a modal flow with nested navigation graphs.

Say we have a wizard: it could be a checkout flow, a login flow. Suppose we want to collect name and surname of a user. I create two fragments in main navigation graph.

Then, I group them in a nested graph using the little button on Android Studio editor: I’ll call this nested graph wizard.

If you double click this nested graph, you will see inside, and you can connect destinations with an action.

More interesting is the action you can create to bring the wizard on screen: I want a global action, because I would like to display wizard both from master and detail fragments.

I don’t want to enter too much inside specific code implementation, I only want to cover main aspects. First of all, wizard needs a view model. It will have name and surname live data fields, with associated methods to mutate contents. This view model will be shared inside sub-navigation graph to cover all wizard steps. Once navigation inside nested graph will be finished, view model will be disposed. This is good not to waste resources.

Wizard results will be delivered by activity shared view model, like before.

So, coming back to navigation, you present this dedicated flow by firing global action:

First wizard step can navigate to second step in the classic way. More interesting is the final step of navigation: back stack is popped until the head of the nested graph, including last screen.

Tab Bar

Tab bar in Material Design is called bottom navigation. First of all, you create a menu that will be used to populate bar of tabs. You can specify secondary menu category for item, if you want to navigate back in the reverse order in which you have clicked the icons.

You specify items of menu, then you insert a BottomNavigationView in activity layout, and associate it with menu_bottom_nav.

Then, you insert destinations inside navigation graph. It’s key to use same identifiers both for destinations and menu items.

When you configure top bar, you need to specify destinations reachable via global navigation UI, not to show back button icon. Note start destination is always considered a top-level destination.

If you execute app, you will see navigation is automatically handled horizontally between tabs:

If you navigate to detail, you switch to another tab, and you return to the first one, you will notice navigation stack state is lost. If you read to Material Design guidelines, it seems it is a desired behaviour:

When you select a bottom navigation item (one that’s not currently selected), […] the app navigates to a destination’s top-level screen. Any prior user interactions and temporary screen states are reset, such as scroll position, tab selection, and in-line search.

If you want to circumvent this silly behavior, you just need to use a different NavHostFragment. This way, each tab would have a different NavigationController, a separated navigation graph and a different toolbar to show. Outer navigation controller would be in charge only to orchestrate tab changes. Anyway, I don’t want to implement this, because it’s a bit off this article intent.

Side menu

Discouraged by Apple, hamburger menu is widely embraced by Material Design.

First of all, you need to change activity layout, in order to have a root DrawerLayout. Remember this layout expects to have two children: first one is content; second one is a NavigationView. I’ll use same menu resource created for bottom navigation:

Now we just need to hook drawer to navigation controller:

You launch app, et voilà: drawer displays if you tap hamburger menu, items in drawer drives you to correct destination, and hamburger menu mutates to a back arrow if you push a destination on stack.

Having both tab bar and hamburger menu at the same time, it’s kind of dumb. It would be smart to display bottom navigation bar only in portrait orientation. It’s possible by moving things a little bit around. First of all, I isolate top bar and navigation host in a separate layout file. Then, I create a separate layout for activity in landscape orientation.

In normal activity_main.xml I have left an <include> to content, plus the BottomNavigationView, everything contained inside a root ConstraintLayout. In landscape variant, I’ve kept the root DrawerLayout. Activity code now produces two warnings, since bottomNavigationView and drawerNavigationView are now optional. This is easy fixed using ? syntax.

Alerts

To show alert popups, you use DialogFragment destinations. It’s just like another push:

To come back, user can tap on shadowed space or on a custom OK button:

This presentation is much more similar to iOS modal view controller: nothing stands on your way if you would like to implement a wizard inside a dialog.

Bottom sheets

To show bottom sheets, you need to create a subclass of BottomSheetDialogFragment. You reference it in navigation graph as a normal destination. Every piece of code is identical to alert case.

Source code

You can see Android Studio test project checking out this Git repo.

--

--