Hello there everyone, welcome to my latest article. Today we’re going to play around with the new Navigation Architecture Components. Ok, maybe not so new 😃 but since it’s almost reaching it’s first stable release feels like a good time to write about it.
This article won’t be part of a series so I created a sample project just for it on GitHub, there’s a link at the bottom of the article.
The Navigation Architecture Components, according to Google, simplifies the implementation of the navigation in an Android app, it helps with implementing the navigation between your app destinations.
It was designed with a Single Activity App concept in mind and is defined by 3 main components:
A Navigation Graph: a file containing a set of destinations and respective actions, defining a navigation architecture for the app. An app can have one or more navigation graphs or sub-graphs.
Destination: a destination on the app, usually a fragment, but a destination can also be an Activity, another navigation graph or sub-graph, and custom destination types are also supported.
Action: the connections between the app destinations.
Ok, now that we know what the Navigation Architecture Components is and what is used for, let’s start playing with it. As usual, dependencies come first, we need to add the following lines to our application build.gradle
file:
def nav_version = "1.0.0-alpha06"
dependencies {
...
implementation "android.arch.navigation:navigation-fragment-ktx:$nav_version"
implementation "android.arch.navigation:navigation-ui-ktx:$nav_version"
}
Next our plan. We’re going to have a very simple example with a single activity, respecting the Navigation Architecture Components principle, and then just 3 fragments. The goal is to demonstrate how to use this to perform a simple navigation without any arguments passed, and another one passing some arguments (primitive type and custom objects) to our destination.
If we wanted to build this kind of flow on a more traditional way, I believe we all know how to do it. We would either have 3 activities and some intents managing the navigation between the activities, so each screen handles the navigation to the next one. Or if using a single activity with 3 fragments, this would need some logic and fragment listeners implementation, so the activity could use a FragmentManager
and swap the fragments accordingly.
All of the above is straightforward as we’ve been doing it forever but to be fair, it’s quite a bit of code to maintain, specially if we want the single activity way, so let’s see how we can achieve the same using the Navigation Architecture Components and find out if it’s a better experience or not.
The first thing we need is a Navigation Graph for our app, you can create one with a right-click in the res
folder followed by:
New -> Android Resource File -> Navigation
Pick a name for your navigation graph file and that’s it, an empty navigation graph file will be generated for you under the right resource directory. This is how our navigation_graph.xml
file looks like after adding all the graph components for our app:
<?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/fragmentA">
<fragment
android:id="@+id/fragmentA"
android:name="com.jcmsalves.navarchcomponentssample.FragmentA"
android:label="Fragment A"
tools:layout="@layout/fragment_a">
<action
android:id="@+id/fragmentAtoB"
app:destination="@id/fragmentB" />
</fragment>
<fragment
android:id="@+id/fragmentB"
android:name="com.jcmsalves.navarchcomponentssample.FragmentB"
android:label="Fragment B"
tools:layout="@layout/fragment_b">
<action
android:id="@+id/fragmentBtoC"
app:destination="@id/fragmentC" />
</fragment>
<fragment
android:id="@+id/fragmentC"
android:name="com.jcmsalves.navarchcomponentssample.FragmentC"
android:label="Fragment C"
tools:layout="@layout/fragment_c">
</fragment>
</navigation>
Quite straightforward, right? We have 3 fragment blocks (our destinations), and one action block (the navigation action) in Fragments A and B. Also notice the app:startDestination
attribute on the navigation
block, this defines which fragment will be the starting destination (in other words the fragment to be rendered in the Activity when this is created).
Ok, this is our navigation graph done, now we just need to wire it up to our activity for everything to work. We need to do 2 things, first edit our activity_main.xml
file and add a NavHostFragment
container to it, 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_fragment"
android:name="androidx.navigation.fragment.NavHostFragment"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:defaultNavHost="true"
app:navGraph="@navigation/navigation_graph" />
</android.support.constraint.ConstraintLayout>
This container will be the host for our navigation graph and where each fragment will be rendered. The 2 important parts are the app:navGraph
attribute that points to our previously created Navigation Graph, and the app:defaultNavHost="true"
that ensures the NavHostFragment
intercepts the system back button and handles it properly.
The second thing we need to do is to go to our Activity class and override the onSupportNavigateUp
to call the same function on the Navigation Controller rather than in the activity:
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
}
override fun onSupportNavigateUp() =
findNavController(nav_host_fragment).navigateUp()
}
And that’s it regarding configuration/setup, pretty clear and easy to understand, right? The only thing missing now is to add some code in the fragments and take advantage of this configuration.
Ok, first a super simple example of navigation from FragmentA
to FragmentB
. All we need is to go to our FragmentA
class and add the following code in a button click listener:
class FragmentA : Fragment() {
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?): View? {
return inflater.inflate(R.layout.fragment_a, container, false)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
buttonA.setOnClickListener {
view.findNavController().navigate(R.id.fragmentAtoB)
}
}
}
Couldn’t be simpler than that, just get the NavigationController
and pass the id of our previously defined action to the navigate()
function. No listeners that need to be implemented by the Activity and no FragmentManager
in the activity and all that code that we always forget to end with .commit()
and wonder why our fragments are not being loaded 😃. Much simpler.
Let’s just see another example, passing a couple of arguments from FragmentB
to FragmentC
including a primitive type argument and a custom object. Here’s how our FragmentB
code looks like:
class FragmentB : Fragment() {
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?): View? {
return inflater.inflate(R.layout.fragment_b, container, false)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
buttonB.setOnClickListener {
val bundle = Bundle()
bundle.putString("primitive_argument", "my argument")
bundle.putParcelable("custom_object", SomeDataClass("field", 99))
view.findNavController().navigate(R.id.fragmentBtoC, bundle)
}
}
}
Very similar to FragmentA
before, we’re just creating a Bundle
object and passing it as a second parameter in the NavigationController
navigate()
function. And to read the arguments on FragmentC
you can probably guess what we need to do, but here it goes:
class FragmentC : Fragment() {
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?): View? {
return inflater.inflate(R.layout.fragment_c, container, false)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
textResult.text = "received argument: " + arguments?.getString("primitive_argument")
val someDataClass: SomeDataClass? = arguments?.getParcelable("custom_object")
someDataClass?.let {
customObjectField.text = it.someField
customObjectNumber.text = it.anotherField.toString()
}
}
}
Again, super simple. To add to the fact we don’t need listeners and deal with Fragment Managers anymore, we also don’t need to write those static newInstance()
functions and then create the Bundle onCreate()
to get the Fragment arguments.
So, what can we say about this experience with the Navigation Architecture Components? For this particular example, I really enjoyed using it, it reduces a lot of boilerplate code and we get to keep all our navigation structure in a single centralised XML file that is much easier to manage.
Hard to say with precision how this scales for bigger projects but definitely looks promising and maybe I should have tried it earlier 😃. Please share some feedback if you’ve used in more complex projects, would be nice to hear some stories about that.
And that’s it, we arrived at our destination today. Hope you enjoyed the ride and don’t forget your belongings and to give some 👏. Feedback is also more than welcome as usual. See you in the next one 👋.