在以前不使用 Compose 的时候,我们通常使用 Activity 作为一个单独的页面,页面跳转只需要调用 Context.startActivity(),页面关闭调用 Activity.finish(),非常的简单。
但在 Jetpack Compose 中,推荐使用单 Actiivty 模式,即所有的页面都在同一个 Activity 中,所以会用到路由框架 Jetpack Navigation。不得不说这个 Navigation 真的用的不是很习惯,踩了很多坑。下面是踩坑两个小技巧让写 Compose 路由更舒服。
图

技巧一:NavHostController 的 “本地化”

为什么我们在所有的Compose函数中都可以拿到 Context、Density 呢?因为 Compose 框架默认给它们做了 “本地化”处理。“本地化”处理之后,我们可以在任意 Compose 函数中使用 Localxxx.current 获取到它。我们来试着依葫芦画瓢实现一个 LocalNavHostController。
我们点开 LocalContext 的源码。

1
2
3
val LocalContext = staticCompositionLocalOf<Context> {
noLocalProvidedFor("LocalContext")
}

我们要先把照着这个 LocalNavHostController 定义出来。

1
2
3
4
5
6
7
8
val LocalNavHostController = staticCompositionLocalOf<NavHostController> {
noLocalProvidedFor("LocalNavHostController")
}

// 把用到的私有函数也 Copy 一份
private fun noLocalProvidedFor(name: String): Nothing {
error("CompositionLocal $name not present")
}

这样我们就可以在任意 Compose 函数中调用 LocalNavHostController.current 拿到 NavHostController 了吗?肯定不行啊,因为我们还没有把它给实例化出来啊,这硬拿肯定空指针啊。那怎么做呢?

1
2
3
4
5
6
7
8
9
10
11
12
13
val navHostController = rememberAnimatedNavController()
CompositionLocalProvider(LocalNavHostController provides navHostController) {
AnimatedNavHost(navController = navHostController, startDestination = startDestination){
composable(xxx){
xxxScreen()
}

composable(xxx){
xxxScreen()
}
// xxx
}
}

这样子的话,我们所有的页面都可以通过 LocalNavHostController.current,相比在每个页面的 Compose 函数里面都传这样一个参数,是不是方便多了?

技巧二:使用扩展函数让 LocalNavHostController 传参数更方便

Jetpack Navigation for Compose 的参数传递真的非常非常坑!!!按照 Google 官方文档,所有的参数都需要声明到 composable 函数那里,看起来非常合理,非常的规范。但是,我要说但是,按照官方文档的写法,当你的参数是 Parcelable 类型的你会发现根本传不过去。好嘛,你不让我传我转 JOSN 传总可以吧?然后你就会被参数的编码问题折磨🉐️死去活去。那有什么好办法呢?我们来翻开 NavController 的代码,发现有如下函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public fun navigate(route: String, builder: NavOptionsBuilder.() -> Unit) {
navigate(route, navOptions(builder))
}

@JvmOverloads
public fun navigate(route: String, navOptions: NavOptions? = null, navigatorExtras: Navigator.Extras? = null ) {
// ***
}

@MainThread
public open fun navigate(@IdRes resId: Int) {
navigate(resId, null)
}

@MainThread
public open fun navigate(@IdRes resId: Int, args: Bundle?) {
// ***
}

啥意思啥意思,这不是欺负老实人吗?传route就不给传 args 参数是吧?

欺负人

诶,等等等等,这个 resId 是什么鬼?以前好像没见过你啊。😯这个 resId 是给 Fragment 用的。。。但是但是,不管是 Int 类型的 resId 还是 String 类型的 route ,最后都会构建成 NavDeepLinkRequest 对象,然后我就发现了一个大宝贝。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
@MainThread
public open fun navigate(
request: NavDeepLinkRequest,
navOptions: NavOptions?,
navigatorExtras: Navigator.Extras?
) {
val deepLinkMatch = _graph!!.matchDeepLink(request)
if (deepLinkMatch != null) {
val destination = deepLinkMatch.destination
val args = destination.addInDefaultArgs(deepLinkMatch.matchingArgs) ?: Bundle()
val node = deepLinkMatch.destination
val intent = Intent().apply {
setDataAndType(request.uri, request.mimeType)
action = request.action
}

// 宝贝在这里
args.putParcelable(KEY_DEEP_LINK_INTENT, intent)
navigate(node, args, navOptions, navigatorExtras)
} else {
throw IllegalArgumentException(
"Navigation destination that matches request $request cannot be found in the " +
"navigation graph $_graph"
)
}
}

啥啥啥,它这里居然 put 了一个 Parcelable 类型的参数,那是不是意味着我们自己构建这个 NavDeepLinkRequest 对象,我们就可以将任意参数传递到下一个页面呢?说干就干。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
fun NavController.navigate(
route: String,
args: Bundle,
navOptions: NavOptions? = null,
navigatorExtras: Navigator.Extras? = null
) {
val routeLink = NavDeepLinkRequest
.Builder
.fromUri(NavDestination.createRoute(route).toUri())
.build()

val deepLinkMatch = graph.matchDeepLink(routeLink)
if (deepLinkMatch != null) {
val destination = deepLinkMatch.destination
val id = destination.id
navigate(id, args, navOptions, navigatorExtras)
} else {
navigate(route, navOptions, navigatorExtras)
}
}

这样子,我们直接使用这个既带 route 又支持 Bundle 类型参数的 NavController 拓展函数,页面间的传参就可以为所以为了,嘿嘿。