At the last I/O we saw the introduction of Android Architecture Components, which since have been helping us to simplify the way in which we build our apps. Yesterday at this years I/O we saw 2 new additions to this collection of components, one of them being the Navigation component. In this post, I want to dive into this component and take a look at exactly how it works.
This new Navigation component has been added with the intention to ease the process of implementing the correct navigational structure throughout our applications. Using the component, we will be able to have fragment transactions automatically handled for us without the need to continuously write this boilerplate code. The component will also allow us to easily add animations for when we are transitioning between screens. Another advantage of using this component means that any up and back navigation used in our application will be correctly handled by the system, this is a common point that applications can struggle to get right. Finally, deep linking to different fragments within our application becomes much easier to handle when using the component. There are some other points that it will enable us to achieve, but we will cover these as we go through the article.
We can add this to our project by adding the following to our projects build.gradle file:
classpath 'android.arch.navigation:navigation-safe-args-gradle-plugin:1.0.0-alpha01'
Follow by these dependencies in our appplication build.gradle file:
implementation 'android.arch.navigation:navigation-fragment:1.0.0-alpha01'
implementation 'android.arch.navigation:navigation-ui:1.0.0-alpha01'
The navigation graph is used to lay out the navigational routes of our application. Within this graph we define what are known as destinations - these destinations are parts of our application which our user can navigate to from our navigation graph.
We need to start by creating this navigation graph, this is created in the form of an XML resource - don’t worry though, we are able to edit this file using a visual tool from inside of Android Studio (Note, you must be using at least Android Studio 3.2 canary 14 to do so). For this, we’re going to need to right-click on our resources directory and select the option to create a new Android Resource File.
Now that we are presented with the option to create a new resource file - we want to use an appropriate name for our graph and select the resource type Navigation.
We can then create this file and it will be added to a navigation directory within our resources (using the Directory name provided above), this directory is then automatically created by the system. Note: At this point, if you haven’t already added the dependency for navigation then you will be prompted to do so - pretty neat!
If you open up the navigation_graph.xml file you’ll notice that currently it looks something like this:
<?xml version="1.0" encoding="utf-8"?>
<navigation xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
</navigation>
The element is used to house the destinations that we are to define for our graph, so let’s continue and populate our graph with these destinations.
For our graph we need to define a host fragment - this is the fragment component in our view which is to be used as the container for the different fragments within our navigation flow. We’re going to begin by adding this fragment to our activity layout:
<fragment
android:layout_width="match_parent"
android:layout_height="match_parent"
android:id="@+id/fragment_nav_host"
android:name="androidx.navigation.fragment.NavHostFragment"
app:navGraph="@navigation/navigation_graph"
app:defaultNavHost="true" />
You’ll notice a few core attributes here: navGraph - this is used to reference the navigation graph that we previously created. This tells the system that this is the graph to be used when calculating the navigational path for the fragments in use. defaultNavHost - declares that this fragment is the default to be used to host the fragments in use in our navigation controller.
Now that this has been added, if you hop on back over to our navigation graph then you should be able to see that the default host (our main activity) is listed as the default host in the design view:
If you head over to the text representation of our navigation controller then you’ll notice at this point it is still pretty blank, that is because there is no navigational flow currently defined for our graph. We’ll go ahead an add some fragments to the graph so that they can be navigated through.
Now that we have our navigation graph we need to define the fragment which will be shown in the navigations initial state. To begin with, we need to go ahead and add a fragment to our navigation graph. We can do this by selecting the New Destination button in the design view:
Once we select a fragment from the displayed drop-down list, the selected fragment will be shown in the design view. Because we want this fragment to be shown when our navigational controller is first shown we need to select the fragment and hit the Set Start Destination button on the right-hand side. At this point, you’ll notice the home icon is now shown above our fragment to indicate that it is the start destination for our navigation graph.
Now at this point, you’ll notice that our fragment has been added to the XML graph and an app:startDestination attribute has been added to the navigation element, noting that the fragment that we just added is to be used for the starting destination of our navigation graph.
<?xml version="1.0" encoding="utf-8"?>
<navigation xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
app:startDestination="@id/mainFragment2">
<fragment
android:id="@+id/mainFragment2"
android:name="co.joebirch.navvi.ui.main.MainFragment"
android:label="second_fragment"
tools:layout="@layout/second_fragment" />
</navigation>
Now, let’s go ahead and add a second fragment to our graph using the same technique we used above. Once our fragment has been added however, we’re going to go ahead and create a routing between the two.
For this, we can select the anchor point on the first fragment and drag the point to the second fragment, doing so creates a relationship between the two - this is us telling the graph that the first fragment navigates to the second fragment. Now if we go back to the text representation of our XML file we can see that it looks a little like this:
<?xml version="1.0" encoding="utf-8"?>
<navigation xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
app:startDestination="@id/mainFragment2">
<fragment
android:id="@+id/mainFragment2"
android:name="co.joebirch.navvi.ui.main.MainFragment"
android:label="second_fragment"
tools:layout="@layout/second_fragment" >
<action
android:id="@+id/action_main_fragment_to_second_fragment"
app:destination="@id/secondFragment" />
</fragment>
<fragment
android:id="@+id/secondFragment"
android:name="co.joebirch.navvi.ui.main.SecondFragment"
android:label="SecondFragment" />
</navigation>
The key here is the action element that has been added to our first fragment. This action has an auto-generated ID, as well as a destination to our second fragment (automatically added as this is where we dragged the anchor of the first fragment to). That’s all great that it’s been defined, but that action alone isn’t enough to make that navigation happen - we need to programmatically let the system know that we want to perform some navigation. For this we use the createNavigateOnClickListener of the Navigation class to do so:
Navigation.createNavigateOnClickListener(
R.id.action_main_fragment_to_second_fragment)
This function uses the navigation action ID to know the navigation which is to be performed. This makes sense as at this point we don’t need to pass it any form of references to destinations, the ID alone is enough to perform the navigation request.
This alone is enough for us to jump from the first fragment to the second fragment within our graph.
However, as you can see the change kind of just jumps from one view to the second - it would be great if we could make this look a little better. Let’s take a look at how we can assign a transition to this navigation.
Navigation transitions can be assigned in two ways:
- Assigning them from within the navigation graph design interface
- By creating them programmatically and passing them in when the navigation is trigged
Let’s begin by taking a look at the first method. If we hop on over to the design of our navigation graph and select the arrow of the action then we can see the transitions section on the right-hand side:
Upon first select this you will notice the transitions are all marked as none. To assign some transitions, we must first start by adding the animation files to our project. You can use any here really, the ones I’ve used are :
slide_in_right:
<set xmlns:android="http://schemas.android.com/apk/res/android">
<translate android:fromXDelta="100%" android:toXDelta="0%"
android:fromYDelta="0%" android:toYDelta="0%"
android:duration="700"/>
</set>
and slide_out_left:
<set xmlns:android="http://schemas.android.com/apk/res/android">
<translate android:fromXDelta="0%" android:toXDelta="-100%"
android:fromYDelta="0%" android:toYDelta="0%"
android:duration="700"/>
</set>
You can assign these by using the drop-down menu for the transitions section. Once you have added these, you will notice they have been added to the action element from within the XML of our navigation graph:
<action
android:id="@+id/action_main_fragment_to_second_fragment"
app:destination="@id/secondFragment"
app:enterAnim="@anim/slide_in_right"
app:exitAnim="@anim/slide_out_left" />
These transitions can also be defined programmatically by using an instance of the NavOptions.Builder and then passing them in when performing the navigation:
val options = NavOptions.Builder()
.setEnterAnim(R.anim.slide_in_right)
.setExitAnim(R.anim.slide_out_left)
.build()
someButton.setOnClickListener {
findNavController(it).navigate(R.id.some_action, null, options)
}
Now if we head on back over to our activity and perform the navigation, you’ll notice that the transition between the two destinations looks much better!
Now that all looks great, but if you remember we set the app:defaultNavHost attribute on our base fragment - the one used to house all of our destinations where that fragment transactions are taking place. Because of this, fragments will be popped as the back button is pressed - this is so that the transaction manager can provide correct back navigation through our fragment. The thing is, these are also unanimated by default. In the transitions section we looked at previous, we can go ahead and set the transitions to be used for pop-enter and pop-exit navigation events:
You notice here that I’ve set transitions for Pop Enter and Pop Exit - these are the transitions used for when fragments are popped. Now that these are set, we can push the back button to see that the transition in this situation is also animated:
We can also perform the navigation through the navigation components from menus within our application. If not done so yet, we need to add a dependency for the navigation-ui library - this gives us access to some helper functions for navigation:
implementation 'android.arch.navigation:navigation-ui:1.0.0-alpha01'
Next, we simply need to assign the ID of the fragment from our navigation graph to the ID of the item in our menu resource file:
<menu xmlns:android="http://schemas.android.com/apk/res/android">
<item
android:id="@+id/secondFragment"
android:menuCategory="secondary"
android:title="Second fragment" />
</menu>
Now within the onOptionsItemSelected function of our activity, we can make use of the onNavDestinationSelected from the NavigationUI class to trigger the navigation of our fragment. Now, when this is called the onNavDestinationSelected will attempt to use this ID and if a corresponding fragment is found then the navigation will be performed.
override fun onOptionsItemSelected(item: MenuItem): Boolean {
return NavigationUI.onNavDestinationSelected(item,
Navigation.findNavController(this, R.id.host_fragment))
|| super.onOptionsItemSelected(item)
}
This means that now when the menu item from here is pressed, the action destination in our graph that matches this ID will be presented by our navigation component.
In some cases, we may want to pass arguments when performing this navigations across fragments. The navigation component has the functionality to still allow us to do so. Here, we need to begin by adding a new plugin to our application build.gradle file:
apply plugin: 'androidx.navigation.safeargs'
Next, we need to define the possible arguments that can be passed to a fragment, we can define these again in the attributes section on the right-hand side of the navigation graph editor once the desired fragment has been selected. Here you pass an argument ID, value type and default value to be used for when no argument is passed.
Once you’ve added these values you’ll notice in the XML of the navigation graph that the argument element has been nested within our fragment reference:
<fragment
android:id="@+id/secondFragment"
android:name="co.joebirch.navvi.ui.main.SecondFragment"
android:label="SecondFragment" >
<argument
android:name="label_text"
android:defaultValue="Button"
app:type="string" />
</fragment>
Now we can hop on over to our fragment and retrieve the arguments that have been passed to it. We’re going to use a generated class to access these arguments, this class is generated by the navigation graph when we define an arguments element - this generated class will be named [you_fragment_name]Args. For example, in my case, my fragment is called SecondFragment so the generated arguments class is called SecondFragmentArgs. We use the fromBundle function of this class, passing in our fragments arguments, which will give us back a reference to the arguments that we defined in our graph. So here you can see that I am accessing the label_text property that I previously defined in my fragment arguments.
arguments?.let {
val safeArgs = SecondFragmentArgs.fromBundle(it)
button.text = safeArgs.label_text
}
That’s all that we’re going to look at for now around the navigation component. There’s still some other aspects to dive into (such as deep linking and activity destinations) but for now, the scope of this article is enough to get you started with this new component. If you have any thoughts or questions then I’d love to hear them! 🙂