在以前不使用 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") }
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() } } }
|
这样子的话,我们所有的页面都可以通过 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 拓展函数,页面间的传参就可以为所以为了,嘿嘿。