post-photo

The Google I/O 2018 had many interesting topics, many of them got a lot of attention, but the most exciting one for me was the Android Navigation Component. I think it was mostly because this topic is inevitable each time you start a project or refactor some good old apps. There were many approaches, which became popular, but all of them depend on third party libs. But from now on (or from the first stable release in the future), we will have a great tool in our hands, which is maintained by Google???

If you want to use Navigation components with Android Studio, you need a version of 3.2 Canary 14 or a newer release.

Getting Started

To get things working, first we have to make sure to import Navigation Components core to our module:

implementation "android.arch.navigation:navigation-fragment-ktx:1.0.0-alpha02" // skip -ktx for Java

The next step is to plan and create our navigation graph. For the planning part, you can find help in the documentation of the Navigation Components here or in this great article. If you’re ready with the design, the next step is to create a nav_graph.xml resource under res/navigation in your project. This contains three different types of items:

If you created your navigation XML resource, you can start declaring your Destinations (Activities, Fragments).

<?xml version="1.0" encoding="utf-8"?>
<navigation xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    app:startDestination="@id/first_screen">
 
    <fragment
        android:id="@+id/first_screen"
        android:name="com.agta.navigationtest.test1.FirstTestFragment"
        android:label="First"
        tools:layout="@layout/fragment_common"/>
 
    <fragment
        android:id="@+id/first_list_screen"
        android:name="com.agta.navigationtest.test1.FirstListFragment"
        android:label="FirstList"
        tools:layout="@layout/fragment_common"/>
 
    <fragment
        android:id="@+id/first_details_screen"
        android:name="com.agta.navigationtest.test1.FirstDetailFragment"
        android:label="FirstDetails"
        tools:layout="@layout/fragment_common"/>
 
</navigation>

You can do this by writing the XML by hand or using the Navigation Editor bundled in the new Canary versions of Android Studio 3.2. You will notice that the root navigation element also has an attribute indicating the starting Destination. If you have declared most of your destinations, you can specify Arguments and Actions for them. A fully described destination looks something like the example below:

<navigation xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    app:startDestination="@id/first_screen">
    ...
    <fragment
        android:id="@+id/first_list_screen"
        android:name="com.agta.navigationtest.test1.FirstListFragment"
        android:label="FirstList"
        tools:layout="@layout/fragment_common">
 
        <argument
            android:name="groupId"
            app:type="string" />
 
        <argument
            android:name="limit"
            app:type="integer"
            android:defaultValue="50"/>
 
        <action
            android:id="@+id/showItemDetails"
            app:destination="@id/first_details_screen" />
 
    </fragment>
    ...
</navigation>

The last missing part is the Host, which will use our navigation graph. The navigation host is responsible for switching between nodes of the graph (this involves fragment transactions, passing the bundled arguments, back navigation handling, etc.) and provides a NavController interface. This interface can be used to send Actions to the host to change it’s state. To provide a host we need an Activity, which will contain our NavHostFragment. The NavHostFragment is a default NavHost implementation provided by the Navigation component’s core. To use this component you can define you MainActivity’s layout like this:

<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">
 
    <fragment
        android:id="@+id/nav_host"
        android:name="androidx.navigation.fragment.NavHostFragment"
        android:layout_width="0dp"
        android:layout_height="0dp"
        app:defaultNavHost="true"
        app:layout_constraintBottom_toTopOf="@+id/bottom_navigation_view"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        app:navGraph="@navigation/nav_graph" />
 
</android.support.constraint.ConstraintLayout>

As you can see, it’s a simple fragment, but it has two additional attributes: app:defaultNavHost=”true” and app:navGraph=”@navigation/nav_graph”. The app:navGraph is used to associate a navigation graph resource with this NavHost. The app:defaultNavHost indicates whether or not it should intercept the system back button events. If everything goes well, you will have a working base app which displays the starting Destination of your defined navigation graph. To try out the navigation mechanism, you will need to find the NavController provided by the NavHostFragment in your MainActivity instance. This can be achieved by a simple call shown below:

class MainActivity : AppCompatActivity() {
     
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
 
        val navController: NavController = Navigation.findNavController(this, R.id.nav_host)
        ...
    }
    ...
}

But most of the time, you will want to initiate a navigation Action from your graph’s Fragments, therefore to get the Host’s NavController in a Fragment instance you can use the NavHostFragment.findNavController(your_fragment_instance) method.

Integration with Views

One module of the Navigation Component provides built-in functions for configuring common Views used. This functionality can be configured through the NavigationUI class, which can be imported using the following dependency:

implementation "android.arch.navigation:navigation-ui-ktx:1.0.0-alpha02" // skip -ktx for Java

With this class you can connect your NavController with your ActionBar, DrawerLayout, NavigationView or ButtomNavigationView.

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContentView(R.layout.activity_main)
 
    navController = Navigation.findNavController(this, R.id.nav_host)
 
    NavigationUI.setupWithNavController(bottom_navigation_view, navController)
    NavigationUI.setupWithNavController(drawer_navigation_view, navController)
 
    NavigationUI.setupActionBarWithNavController(this, navController)
    NavigationUI.setupActionBarWithNavController(this, navController, drawerLayout)
}

If you connect your NavigationViews to the NavController and the menu XML resources have matching item IDs with the destinations declared in the navigation graph, then this will automatically handle highlighting the item associated with the current destination displayed. It will also handle executing navigation actions based on the NavigationView’s selected item.The behavior and the config method are the same for BottomNavigationViews.

Configuring the ActionBar with the NavController will result in displaying the destination labels as title, and it will also provide the hamburger icon and the back arrow according to the current destination’s position in the graph. It also handles the opening and closing of the Drawer if the DrawerLayout is also provided for the config.

DeepLinking

The Navigation Component has built-in support for DeepLinks. You can specify deepLink Uri-s in your navigation graph’s XML for your destinations. It’s simple as below:

<deepLink app:uri="https://someth.ing/you/wanna/see"/>

If you want your app’s Activity to respond for the Uri specified in your nav_graph.xml, you must add it in your manifest, like an intent-filter, for your Activity.

...
<activity name=".MainActivity"
    ...>
    ...
    <nav-graph android:value="@navigation/nav_graph" />
</activity>
...

Simple as that. But we can even get arguments from our Uri by including placeholder is the path.

<deepLink app:uri=" https://someth.ing/you/wanna/read/page/{page_number} "/>
Or
<deepLink app:uri=" https://someth.ing/you/wanna/read/page/{page_number}/paragraph/{paragraph} "/>

In this case our destination will receive a Bundle containing the placeholder value associated with a „page_number” key. We can have multiple placeholders in one Uri and we can also inclue wildcards („.*” indicating zero or more characters).

SafeArgs Plugin

This is my most favourite tool of the whole Navigation Component, because it makes parameterized navigations so easy. Having lots of screens in one app (or lots of apps in progress with some screens) can easily make you forget some of the mandatory arguments, which will lead to unexpected runtime crashes during development.

Thanks to the SafeArgs plugin; if you declare your arguments in the navigation graph, you will always see the parameters on the generated NavDirection builders.

<fragment
    android:id="@+id/first_list_screen"
    android:name="com.agta.navigationtest.test1.FirstListFragment"
    android:label="FirstList"
    tools:layout="@layout/fragment_common">
 
    <argument
        android:name="groupId"
        app:type="string" />
 
    <argument
        android:name="limit"
        app:type="integer"
        android:defaultValue="50"/>
 
    <action
        android:id="@+id/showItemDetails"
        app:destination="@id/first_details_screen" />
 
</fragment>

The example above shows a destination having two arguments. A groupId describing which list should be requested and a limit describing the max count of the request’s result. If you investigate it closely, you can see that the limit has a defaultValue specified.

The generated Directions code will look something like this.

public class FirstTestFragmentDirections {
  public static ShowFirstList showFirstList(String groupId) {
    return new ShowFirstList(groupId);
  }
 
  public static class ShowFirstList implements NavDirections {
    private String groupId;
    private int limit = 50;
 
    public ShowFirstList(String groupId) {
      this.groupId = groupId;
    }
    ...
  }
...
}

As you can see, the plugin used the default value for the limit and generated a NavDirections implementation with a constructor, which has the mandatory arguments (Which don’t have default values, so you must provide them.). You can construct a navigation to this destination without using this helper class, but I strongly recommend using it, because it has an implementation on the receiving side also, which will extract your passed arguments from the Bundle, and validate the passed data. The next snippet shows the generated class which can be used to unwrap the bundled arguments.

public class FirstListFragmentArgs {
  private String groupId;
 
  private int limit = 50;
 
  private FirstListFragmentArgs() {
  }
 
  public static FirstListFragmentArgs fromBundle(Bundle bundle) {
    FirstListFragmentArgs result = new FirstListFragmentArgs();
    if (bundle.containsKey("groupId")) {
      result.groupId = bundle.getString("groupId");
    } else {
      throw new IllegalArgumentException("Required argument \"groupId\" is missing and does not have an android:defaultValue");
    }
    if (bundle.containsKey("limit")) {
      result.limit = bundle.getInt("limit");
    }
    return result;
  }
  ...
}

As we can see, if we target this destination without suppling the declared arguments, then the fromBundle method will throw an IllegalArgumentException indicating that there was a missing argument. This can only occur with arguments that don’t have default values.

To wrap things up, the SafeArgs plugin can make our life easier when we need to pass information to destinations.

Current limitations:

Only application modules are supported by the plugin, which means if you have your project divided into submodules that invole Fragments outside the application module, the SafeArgs plugin won’t be able to generate the classes. Hopefully this will be fixed soon. ([I really enjoyed diving into this new tool. The basics can be understood by walking through the provided tutorials on Android Developers (https://developer.android.com/topic/libraries/architecture/navigation/). There are a few limitations that will probably make some of the developers hesitate about using this in the near future, but hey, it’s only in the alpha stage, so there’s no need to worry. I’m really looking forward to using it in future projects, but I will also wait at least for the first beta version. 😉](https://issuetracker.google.com/issues/80036553, https://issuetracker.google.com/issues/110011752))

Also in modularized projects, you will probably want to create navigation sub graphs, which will be „included” in a top level navigation graph. This can be a pain if you haven’t gone through some SO searches, due to the lack of documentation.

Overview

I really enjoyed diving into this new tool. The basics can be understood by walking through the provided tutorials on Android Developers (https://developer.android.com/topic/libraries/architecture/navigation/)). There are a few limitations that will probably make some of the developers hesitate about using this in the near future, but hey, it’s only in the alpha stage, so there’s no need to worry. I’m really looking forward to using it in future projects, but I will also wait at least for the first beta version. 😉

Wanari is an 18-year-old custom software development company, based in downtown Budapest, Hungary. We aim at making the world a better place by creating software even our picky selves would use. For more, visit us at: https://www.wanari.com/.

member photo

He is an Android developer, loves to work with Rx Java and CustomWidgets. Not surprisingly, he codes not only at work, but also in his free time.

Latest post by Tamás Agócs

Life after Google Analytics