使用 Kotlin DSL 以编程方式构建图形

导航组件提供一种基于 Kotlin 的领域特定语言 (DSL),该语言依赖于 Kotlin 的类型安全构建器。借助该 API,您可以在 Kotlin 代码中(而不是在 XML 资源内部)以声明方式构建图形。如果您希望为应用动态构建导航,该方法会非常有用。例如,您的应用可以从外部网络服务下载并缓存导航配置,然后使用该配置在 Activity 的 onCreate() 函数中动态构建导航图。

依赖项

如需使用 Kotlin DSL,请将以下依赖项添加到应用的 build.gradle 文件中:

dependencies {
    def nav_version = "2.3.0-rc01"

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

构建图形

首先我们来看一个基于 Sunflower 应用的基本示例。在该示例中,我们有两个目的地:homeplant_detail。用户首次启动应用时,系统会显示 home 目的地。该目标地会显示用户花园中的植物列表。当用户选择其中一种植物时,应用会导航到 plant_detail 目的地。

图 1 显示了这些目的地以及 plant_detail 目的地所需的参数和应用用于从 home 导航到 plant_detail 的操作 to_plant_detail

Sunflower 应用有两个目的地以及一个连接这两个目的地的操作。
图 1. Sunflower 应用有两个目的地(homeplant_detail)以及一个连接这两个目的地的操作。

为 Kotlin DSL 导航图创建主机

无论如何构建图形,都需要将图托管在 NavHost 中。Sunflower 使用 Fragment,因此我们在 FragmentContainerView 中使用 NavHostFragment,如以下示例所示:

<!-- 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>

请注意,该示例中未设置 app:navGraph 属性,因为该图形是用编程方式构建的,而不是以 XML 资源的形式定义的。

为图形创建常量

使用基于 XML 的导航图时,Android 构建流程会解析图形资源文件,并为图形中定义的每个 id 属性指定数字常量。您代码中的这些常量可以通过生成的资源类 R.id 来获取。

例如,以下 XML 图代码段使用 idhome 声明了一个 Fragment 目的地:

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

构建流程会创建一个与该目的地关联的常量值 R.id.home,然后,您可以使用该常量值从代码中引用该目的地。

使用 Kotlin DSL 以编程方式构建图形时,不会发生这种解析和生成常量的过程。相反,您必须为具有 id 值的每个目的地、操作和参数定义自己的常量。每个 ID 在配置更改中必须是唯一且一致的。

创建常量的一种有序方法是创建一组用于静态定义常量的嵌套 Kotlin object,如以下示例所示:

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"
    }
}

使用该结构,可以将对象调用串联起来,以获取代码中的 ID 值,如以下示例所示:

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

定义一组初始 ID 后,即可构建导航图。使用 NavController.createGraph() 扩展函数创建一个 NavGraph,并传入图形的 idstartDestination 的 ID 值以及用于定义图形结构的尾随 lambda。

您可以在 Activity 的 onCreate() 函数中构建图形。createGraph() 会返回一个 Navgraph,然后您可以将其分配给与 NavHost 关联的 NavControllergraph 属性,如以下示例所示:

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
                    }
                }
            }
        }
    }
}

在该示例中,尾随 lambda 使用 fragment() DSL 构建器函数定义了两个 Fragment 目的地。该函数需要目的地的 ID。该函数还接受用于其他配置的可选 lambda(如目的地 label)以及用于操作、参数和深层链接的嵌入式构建器函数。

管理每个目的地界面的 Fragment 类将作为放在尖括号 (<>) 中的参数化类型传入。这与在使用 XML 定义的 Fragment 目的地上设置 android:name 属性具有相同的效果。

构建并设置图形后,即可使用 NavController.navigate()home 导航到 plant_detail,如以下示例所示:

private fun navigateToPlant(plantId: String) {

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

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

支持的目的地类型

Kotlin DSL 支持 FragmentActivityNavGraph 目的地,每个目的地都有自己的内嵌扩展函数,可用于构建和配置目的地。

Fragment 目的地

可以将 fragment() DSL 函数参数化为实现 Fragment 类。该函数使用分配给该目的地的唯一 ID 以及您可以在其中提供其他配置的 lambda。

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

Activity 目的地

activity() DSL 函数使用分配给该目的地的唯一 ID,但未将其参数化为任何实现 Activity 类。相反,您可以在尾随 lambda 中设置可选 activityClass。这种灵活性可以让您为从隐式 intent 启动的 Activity 定义 Activity 目的地,在这种情况下,显式 Activity 类将毫无意义。与 Fragment 目的地一样,您还可以定义并配置标签和任何参数。

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

    activityClass = ActivityDestination::class
}

您可以使用 navigation() DSL 函数构建嵌套导航图。与其他目的地类型一样,该 DSL 函数使用三个参数:分配给图形的 ID、图形的起始目的地 ID 以及用于进一步配置图形的 lambda。lambda 的有效元素包括参数、操作、其他目的地、深层链接和标签。

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

支持自定义目的地

您可以使用 addDestination() 将默认不直接支持的自定义目的地类型添加到 Kotlin DSL,如以下示例所示:

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

您还可以使用一元加号运算符 (+) 将新构造的目的地直接添加到图形中:

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

提供目的地参数

您可以为任何目的地类型定义可选或必需参数。如需定义参数,请针对所有目的地构建器类型的基类 NavDestinationBuilder 调用 argument()。该函数将参数的名称作为可用于构造和配置 NavArgumentString 和 lambda。在 lambda 中,您可以指定参数数据类型、默认值(如果适用)以及参数值是否可以为 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
    }
}

如果指定了 defaultValue,则 type 是可选的。在这种情况下,如果未指定 type,则从 defaultValue 推断类型。如果同时提供了 defaultValuetype,这些类型必须匹配。如需查看参数类型的完整列表,请参阅 NavType

操作

您可以定义任何目的地内的操作,包括根导航图中的全局操作。如需定义操作,请使用 NavDestinationBuilder.action() 函数,为该函数提供一个 ID 并提供一个 lambda 以提供其他配置。

以下示例构建了一项具有 destinationId、过渡动画以及弹出行为和单一顶级行为的操作。

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
    }
}

深层链接

您可以将深层链接添加到任何目的地,就像使用基于 XML 的导航图一样。为目的地创建深层链接中定义的相同程序也适用于使用 Kotlin DSL 创建显式深层链接的过程。

但是,在创建隐式深层链接时,您没有可以针对 <deepLink> 元素进行分析的 XML 导航资源。因此,您不能依赖于在 AndroidManifest.xml 文件中放置 <nav-graph> 元素,而是必须手动向 Activity 添加 Intent 过滤器。您提供的 Intent 过滤器应与应用深层链接的基本网址格式匹配。

对于每个单独的深层链接目的地,您可以使用 deepLink() DSL 函数提供更具体的 URI 模式。该函数接受 String 作为 URI 模式,如以下示例所示:

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

您可以添加的深层链接 URI 数量没有限制。每次调用 deepLink() 时,都会将新的深层链接附加到特定于该目的地的内部列表。

以下是更复杂的隐式深层链接场景,该场景还定义了基于路径和基于查询的参数:

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
    }
}

请注意,可以使用字符串插值简化定义。

创建 ID

导航库要求用于图形元素的 ID 值是唯一整数,这些整数在配置更改时保持不变。创建这些 ID 的一种方法是将其定义为静态常量,如为图形创建常量中所示。您还可以将 XML 中的静态资源 ID 定义为资源。或者,您也可以动态构造 ID。例如,您可以创建一个在每次引用时都会递增的序列计数器。

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"
    }
}

限制

  • Safe Args 插件与 Kotlin DSL 不兼容,因为该插件会查找 XML 资源文件以生成 DirectionsArguments 类。