Google is committed to advancing racial equity for Black communities. See how.

Build a graph programmatically using the Kotlin DSL

The Navigation component provides a Kotlin-based domain-specific language, or DSL, that relies on Kotlin's type-safe builders. This API allows you to declaratively compose your graph in your Kotlin code, rather than inside an XML resource. This can be useful if you wish to build your app's navigation dynamically. For example, your app could download and cache a navigation configuration from an external web service and then use that configuration to dynamically build a navigation graph in your activity's onCreate() function.

Dependencies

To use the Kotlin DSL, add the following dependency to your app's build.gradle file:

dependencies {
    def nav_version = "2.3.1"

    api "androidx.navigation:navigation-fragment-ktx:$nav_version"
}

Building a graph

Let's start with a basic example based on the Sunflower app. For this example, we have two destinations: home and plant_detail. The home destination is present when the user first launches the app. This destination displays a list of plants from the user's garden. When the user selects one of the plants, the app navigates to the plant_detail destination.

Figure 1 shows these destinations along with the arguments required by the plant_detail destination and an action, to_plant_detail, that the app uses to navigate from home to plant_detail.

The Sunflower app has two destinations along with an action that
            connects them.
Figure 1. The Sunflower app has two destinations, home and plant_detail, along with an action that connects them.

Create the host for your Kotlin DSL nav graph

Regardless of how you build your graph, you need to host the graph in a NavHost. Sunflower uses fragments, so let's use a NavHostFragment inside of a FragmentContainerView, as shown in the following example:

<!-- activity_garden.xml -->
<FrameLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto">
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <androidx.fragment.app.FragmentContainerView
        android:id="@+id/nav_host"
        android:name="androidx.navigation.fragment.NavHostFragment"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        app:defaultNavHost="true" />

</FrameLayout>

Notice that the app:navGraph attribute is not set in this example, since the graph is built programmatically instead of being defined as an XML resource.

Create constants for your graph

When working with XML-based navigation graphs, the Android build process parses your graph resource file and defines numeric constants for each id attribute defined in the graph. These constants are accessible in your code through a generated resource class, R.id.

For example, the following XML graph snippet declares a fragment destination with an id, home:

<navigation ...>
   <fragment android:id="@+id/home" ... />
   ...
</navigation>

The build process creates a constant value, R.id.home, that is associated with this destination. You can then reference this destination from your code by using this constant value.

This process of parsing and generating constants does not occur when you build a graph programmatically using the Kotlin DSL. Instead, you must define your own constants for each destination, action, and argument that has an id value. Each ID must be unique and consistent across configuration changes.

One organized way to create constants is to create a nested set of Kotlin objects that define the constants statically, as shown in the following example:

object nav_graph {

    const val id = 1 // graph id

    object dest {
        const val home = 2
        const val plant_detail = 3
    }

    object action {
        const val to_plant_detail = 4
    }

    object args {
        const val plant_id = "plantId"
    }
}

With this structure, you can access the ID values in code by chaining the object calls together, as shown in the following examples:

nav_graph.id                     // graph id
nav_graph.dest.home              // home destination id
nav_graph.action.to_plant_detail // action home -> plant_detail id
nav_graph.args.plant_id          // destination argument name

Once you've defined your initial set of IDs, you can build the navigation graph. Use the NavController.createGraph() extension function to create a NavGraph, passing in an id for your graph, an ID value for the startDestination, and a trailing lambda that defines the structure of your graph.

You can build your graph in your activity's onCreate() function. createGraph() returns a Navgraph that you can then assign to the graph property of the NavController that is associated with your NavHost, as shown in the following example:

class GardenActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_garden)

        val navHostFragment = supportFragmentManager
                .findFragmentById(R.id.nav_host) as NavHostFragment

        navHostFragment.navController.apply {
            graph = createGraph(nav_graph.id, nav_graph.dest.home) {
                fragment<HomeViewPagerFragment>(nav_graph.dest.home) {
                    label = getString(R.string.home_title)
                    action(nav_graph.action.to_plant_detail) {
                        destinationId = nav_graph.dest.plant_detail
                    }
                }
                fragment<PlantDetailFragment>(nav_graph.dest.plant_detail) {
                    label = getString(R.string.plant_detail_title)
                    argument(nav_graph.args.plant_id) {
                        type = NavType.StringType
                    }
                }
            }
        }
    }
}

In this example, the trailing lambda defines two fragment destinations using the fragment() DSL builder function. This function requires an ID for the destination. The function also accepts an optional lambda for additional configuration, such as the destination label, as well as embedded builder functions for actions, arguments, and deep links.

The Fragment class that manages the UI of each destination is passed in as a parameterized type inside angle brackets (<>). This has the same effect as setting the android:name attribute on fragment destinations that are defined using XML.

Once you've built and set your graph, you can then navigate from home to plant_detail using NavController.navigate(), as shown in the following example:

private fun navigateToPlant(plantId: String) {

    val args = bundleOf(nav_graph.args.plant_id to plantId)

    findNavController().navigate(nav_graph.action.to_plant_detail, args)
}

Supported destination types

The Kotlin DSL supports Fragment, Activity, and NavGraph destinations, each of which has its own inline extension function available for building and configuring the destination.

Fragment destinations

The fragment() DSL function can be parameterized to the implementing Fragment class. This function takes a unique ID to assign to this destination along with a lambda where you can provide additional configuration.

fragment<FragmentDestination>(nav_graph.dest.fragment_dest_id) {
   label = getString(R.string.fragment_title)
   // arguments, actions, deepLinks...
}

Activity destination

The activity() DSL function takes a unique ID to assign to this destination but is not parameterized to any implementing activity class. Instead, you can set an optional activityClass in a trailing lambda. This flexibility allows you to define an activity destination for an activity that is launched from an implicit intent, where an explicit activity class would not make sense. As with fragment destinations, you can also define and configure a label and any arguments.

activity(nav_graph.dest.activity_dest_id) {
    label = getString(R.string.activity_title)
    // arguments, actions, deepLinks...

    activityClass = ActivityDestination::class
}

You can use the navigation() DSL function to build a nested navigation graph. As with the other destination types, this DSL function takes three arguments: an ID to assign to the graph, a starting destination ID for the graph, and a lambda to further configure the graph. Valid elements for the lambda include arguments, actions, other destinations, deep links, and a label.

navigation(nav_graph.dest.nav_graph_dest, nav_graph.dest.start_dest) {
   // label, arguments, actions, other destinations, deep links
}

Supporting custom destinations

You can use addDestination() to add custom destination types to your Kotlin DSL that are not directly supported by default, as shown in the following example:

// The NavigatorProvider is retrieved from the NavController
val customDestination = navigatorProvider[CustomNavigator::class].createDestination().apply {
    id = nav_graph.dest.custom_dest_id
}
addDestination(customDestination)

You can also use the unary plus operator (+) to add a newly-constructed destination directly to the graph:

// The NavigatorProvider is retrieved from the NavController
+navigatorProvider[CustomNavigator::class].createDestination().apply {
    id = nav_graph.dest.custom_dest_id
}

Providing destination arguments

You can define optional or required arguments for any destination type. To define an argument, call the argument() function on NavDestinationBuilder, the base class for all destination builder types. This function takes the argument's name as a String and a lambda that you can use to construct and configure a NavArgument. Inside the lambda, you can specify the argument data type, a default value if applicable, and whether the argument value can be null.

fragment<PlantDetailFragment>(nav_graph.dest.plant_detail) {
    label = getString(R.string.plant_details_title)
    argument(nav_graph.args.plant_name) {
        type = NavType.StringType
        defaultValue = getString(R.string.default_plant_name)
        nullable = true  // default false
    }
}

If a defaultValue is given, type is optional. In this case, if no type is specified the type is inferred from defaultValue. If both a defaultValue and type are provided, the types must match. For a complete list of argument types, see NavType.

Actions

You can define actions within any destination, including global actions within the root navigation graph. To define an action, use the NavDestinationBuilder.action() function, supplying an ID to the function and a lambda to provide additional configuration.

The following example builds an action with a destinationId, transition animations, and pop and single-top behavior.

action(nav_graph.action.to_plant_detail) {
    destinationId = nav_graph.dest.plant_detail
    navOptions {
        anim {
            enter = R.anim.nav_default_enter_anim
            exit = R.anim.nav_default_exit_anim
            popEnter = R.anim.nav_default_pop_enter_anim
            popExit = R.anim.nav_default_pop_exit_anim
        }
        popUpTo(nav_graph.dest.start_dest) {
            inclusive = true // default false
        }
        // if popping exclusively, you can specify popUpTo as
        // a property. e.g. popUpTo = nav_graph.dest.start_dest
        launchSingleTop = true // default false
    }
}

Deep links

You can add deep links to any destination, just as with an XML-based navigation graph. The same procedures defined in Creating a deep link for a destination apply to the process of creating an explicit deep link using the Kotlin DSL.

When creating an implicit deep link, however, you don't have an XML navigation resource that can be analyzed for <deepLink> elements. Therefore, you can't rely on placing a <nav-graph> element in your AndroidManifest.xml file and must instead manually add intent filters to your activity. The intent filter you supply should match the base URL pattern of your app's deep links.

For each individually-deep-linked destination, you can supply a more specific URI pattern using the deepLink() DSL function. This function accepts a String for the URI pattern, as shown in the following example:

deepLink("http://www.example.com/plants/")

There is no limit to the number of deep link URIs you can add. Each call to deepLink() appends a new deep link to an internal list specific to that destination.

Here's a more complex implicit deep link scenario that also defines path- and query-based parameters:

val baseUri = "http://www.example.com/plants"

fragment<PlantDetailFragment>(nav_graph.dest.plant_detail) {
    label = getString(R.string.plant_details_title)
    deepLink("${baseUri}/{id}")
    deepLink("${baseUri}/{id}?name={plant_name}")
    argument(nav_graph.args.plant_id) {
       type = NavType.IntType
    }
    argument(nav_graph.args.plant_name) {
        type = NavType.StringType
        nullable = true
    }
}

Note that string interpolation can be used to simplify the definition.

Creating IDs

The Navigation library requires that the ID values used for graph elements be unique integers that remain constant through configuration changes. One way to create these IDs is to define them as static constants as shown in Create constants for your graph. You can also define static resource IDs in XML as a resource. Alternatively, you can dynamically construct IDs. For example, you could create a sequence counter that is incremented each time you reference it.

object nav_graph {
    // Counter for id's. First ID will be 1.
    var id_counter = 1

    val id = id_counter++

    object dest {
       val home = id_counter++
       val plant_detail = id_counter++
    }

    object action {
       val to_plant_detail = id_counter++
    }

    object args {
       const val plant_id = "plantId"
    }
}

Limitations

  • The Safe Args plugin is incompatible with the Kotlin DSL, as the plugin looks for XML resource files to generate Directions and Arguments classes.