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:

  1. navigation resources
  2. A Kotlin domain-specific language (DSL) for use with fragments and similar destinations
  3. 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:

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:

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:

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.