The Next Wave: Kotlin DSL
The Navigation component started out focused on navigation
XML resources. Navigation has now evolved into three supported ways of defining a navigation graph:
-
navigation
resources - A Kotlin domain-specific language (DSL) for use with fragments and similar destinations
- A related Kotlin DSL for use with Jetpack Compose and composables
So, let’s take a look at what the Kotlin DSL looks and works like.
What We Gain, What We Lose
On the plus side, using a Kotlin DSL means that you do not have to fuss with Yet Another Resource Type. Some developers love the separation imposed by the split between resources and programming languages; other developers dislike it. If you fall in the latter camp, the Kotlin DSL is likely to be something that you prefer.
Also, it is far simpler for the team behind the Navigation component to extend the DSL, as that is just standard Kotlin code from a Kotlin library. So, it is easier for the Navigation team to add new features or fix awkward APIs.
However, what we lose is tooling:
- There is no drag-and-drop editor for the Kotlin DSL — you are writing Kotlin code the same way as you would any other Kotlin code
- There is no support for automatic generation of “SafeArgs”-style code, though creating your own is not that difficult
- The original Kotlin DSL, that we will explore here, requires the use of unique numeric identifiers, requiring you to write a bit of extra code to define what those identifiers are
Defining the Identifiers
The FragmentNavDSL
sample module in the Sampler
contains a clone of FragmentNav
that uses the Kotlin DSL to define the nav graph. There is no SamplerJ
edition, as the Kotlin DSL requires Kotlin to be practical.
We need unique Int
identifiers for:
- The nav graph itself
- Each destination
- Each action
We also need unique String
identifiers for any arguments that we want to pass to a destination via an action.
To that end, we have a NavGraph
object that holds those constants… and a little bit more:
package com.commonsware.jetpack.sampler.nav
import androidx.core.os.bundleOf
import androidx.navigation.NavController
object NavGraph {
const val Id = 1
object Screen {
const val List = 2
const val Display = 3
}
object Action {
object DisplayModel {
const val Id = 4
fun NavController.displayModel(modelId: String) {
navigate(Id, bundleOf(Args.ModelId to modelId))
}
val DisplayFragment.modelId
get() = arguments?.getString(Args.ModelId)
}
}
object Args {
const val ModelId = "ModelId"
}
}
We will look at those two extension functions defined in DisplayModel
a bit later. The rest, though, are just a namespaced set of constants, following some organizing suggestions from Google.
Defining the Graph
In FragmentNav
, our nav graph is set up as a navigation
resource. Our activity_main
layout refers to that nav graph from the FragmentContainerView
. As a result, MainActivity
gets the nav graph automatically and can just start configuring how Navigation works.
In FragmentNavDSL
, we no longer have a navigation
resource. While we still have the FragmentContainerView
, it no longer has an app:navGraph
attribute. Which means that MainActivity
has the honor and privilege of setting up the nav graph itself, using the Kotlin DSL:
package com.commonsware.jetpack.sampler.nav
import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import androidx.navigation.NavType
import androidx.navigation.createGraph
import androidx.navigation.fragment.findNavController
import androidx.navigation.fragment.fragment
import androidx.navigation.ui.AppBarConfiguration
import androidx.navigation.ui.setupWithNavController
import com.commonsware.jetpack.sampler.nav.databinding.ActivityMainBinding
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
supportFragmentManager.findFragmentById(R.id.nav_host)?.findNavController()
?.let { nav ->
nav.graph = nav.createGraph(NavGraph.Id, NavGraph.Screen.List) {
fragment<ListFragment>(NavGraph.Screen.List) {
label = getString(R.string.list_title)
action(NavGraph.Action.DisplayModel.Id) {
destinationId = NavGraph.Screen.Display
}
}
fragment<DisplayFragment>(NavGraph.Screen.Display) {
label = getString(R.string.display_title)
argument(NavGraph.Args.ModelId) {
type = NavType.StringType
}
}
}
binding.toolbar.setupWithNavController(
nav,
AppBarConfiguration(nav.graph)
)
}
}
}
After obtaining our NavController
, we use createGraph()
on that NavController
to build a NavGraph
object that we assign to the graph
property on the NavController
. createGraph()
is the root of the DSL — it takes a lambda expression or other function type that actually builds up the nav graph. The receiver (this
) for that lambda expression is a NavGraphBuilder
, which actually implements the functions that make up the DSL itself.
createGraph()
takes two other parameters besides that lambda expression:
- The unique
Int
ID of this nav graph - The ID of the root destination
That is equivalent to the android:id
and app:startDestination
attributes on the root <navigation>
element of the navigation
resource that we started with.
From there, we call fragment()
twice. fragment()
is the DSL equivalent of the <fragment>
element in the navigation
resource, defining a destination in the form of a fragment. fragment()
takes a type declaration, which supplies the Fragment
subclass that should be displayed when we navigate to this destination, filling the role of android:name
in the resource. fragment()
also takes the ID of the destination (equivalent to android:id
) and another lambda expression or other function type to specify details of the destination. There, we set a label
property (equivalent to android:label
).
For the first fragment()
call, we call action()
to define an action that we can take from this fragment. action()
serves as the <action>
equivalent, and there we provide IDs for the action itself and the destination that this action should go to. The second fragment()
call has an argument()
nested DSL call, replacing the <argument>
element, where we provide the name of the argument and its type
.
After that, we can call setupWithNavController()
as before to tie the Toolbar
into the Navigation component.
On the whole, there is a fairly straightforward mapping of navigation
resource contents into the Kotlin DSL, with functions and parameters taking the place of elements and attributes.
Navigating Using the Graph
In the FragmentNav
sample, to actually navigate, we used the NavController
, coupled with the code-generated “directions” and “args” classes that the SafeArgs plugin created for us. Without SafeArgs, the Kotlin DSL dumps most of the problem of properly passing arguments on our lap.
But, we can make it appear a bit more seamless, using those extension functions in NavGraph
:
fun NavController.displayModel(modelId: String) {
navigate(Id, bundleOf(Args.ModelId to modelId))
}
val DisplayFragment.modelId
get() = arguments?.getString(Args.ModelId)
To navigate from ListFragment
to DisplayFragment
, we had this in the FragmentNav
sample:
private fun navTo(model: ToDoModel) {
findNavController().navigate(ListFragmentDirections.displayModel(model.id))
}
That turns into:
private fun navTo(model: ToDoModel) {
findNavController().displayModel(model.id)
}
displayModel()
comes from NavGraph
. Like displayModel()
on ListFragmentDirections
, displayModel()
takes our ToDoModel
ID as a parameter. The FragmentNavDSL
edition of displayModel()
wraps up that ID in a Bundle
, keyed by our ModelId
identifier, and passes that to navigate()
. Effectively, that is all ListFragmentDirections.displayModel()
is doing — we just had to write the code ourselves. And, along the way, by making displayModel()
be an extension function on NavController
, the navTo()
function becomes a bit less verbose.
On the DisplayFragment
side, in FragmentNav
, we could use the navArgs()
property delegate and the generated DisplayFragmentArgs
class:
private val args: DisplayFragmentArgs by navArgs()
We could then refer to a modelId
property on args
:
val model = vm.getModel(args.modelId)
That line turns into:
val model = modelId?.let { vm.getModel(it) }
modelId
is now an extension property on DisplayFragment
, defined in NavGraph
. It gets the arguments Bundle
and pulls out our ModelId
argument. DisplayFragment
can reference it the same as it would any other property. Once again, while we had to write the extension property ourselves, we can tweak it to make using that code a bit simpler than what we had with SafeArgs.
Prev Table of Contents Next
This book is licensed under the Creative Commons Attribution-ShareAlike 4.0 International license.