Navigation with Compose

1,798 阅读4分钟

上篇文章介绍了 Navigation 的集成与基本使用。可以看出来Navigation的引入使我们对我们项目中的fragment的管理更加的井井有条,提高了开发效率。而本文则着重讲一下机遇ComposeNavigation的使用。

Compose下的页面跳转

Compose的显示是需要载体的,传统的Activity也好,Fragment也好,如果基于传统的页面形式来做Compose下的页面跳转的话,最终也还是回到了传统的Activity跳转以及Fragment的管理上来。那么有没有一种针对Compose的量身定做的而不依赖传统Activity以及Fragment的页面管理方式呢。答案是有,一直都有。前有Google推出的Navigation,后有大佬制作的号称 “Easier to complete Jetpack Compose navigation”compose-navigator。本质上都是为了解决 Compose 的页面管理问题,本期文章我们来看一下Google推出的navigation-compose

集成

dependencies {
    implementation "androidx.navigation:navigation-compose:2.4.0-rc01"
}

组成

上篇文章 基于 FragmentNavigation是由三部分组成,分别是导航图NavHostNavController,而基于ComposeNavigation是由NavHostNavController组成,相比前者少了导航图的资源构建。

使用

NavController

NavControllerNavigation的核心api,他维护了Navigation内部关于页面的堆栈以及状态信息,甚至,前文讲的导航图其实也是在NavController中所维护。在Compose中需要通过以下方式获取一个有状态的实例:

val navController = rememberNavController()

NavHost

上篇文章中讲到了NavHostFragmentNavHostFragment其实是作为Navigationfragment的显示载体来出现的。而在compose-navigation的体系中,因为没了Fragment的出现,因此相应的,NavHostFragment也由更加抽象的NavHost来实现。其实就是个容器,内部通过持有NavController的引用,在“页面切换”时,渲染当前 Compose UI。那么NavHost的初始化参数我可大致猜出一些端倪,肯定会有:

  • NavController 刚才讲到在compose-navigation体系中导航图是不需要单独在资源文件中定义的,是由NavHost来维护的,因此参考上篇文章中导航图所包含的关键内容来看,NavHost也应该包含:
  • startDestination路由起点
  • NavGraph导航图详细

那么一个简单的实践如下:

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            val navController = rememberNavController()
            NavHost(navController = navController, startDestination = "A") {
                composable("A") {
                    ScreenA()
                }
                composable("B") {
                    ScreenB()
                }
                composable("C") {
                    ScreenC()
                }
            }
        }
    }
}

跳转

NavController小节中,我们讲到了初始化,对于这一点,官方的描述如下:

您应该在可组合项层次结构中的适当位置创建 NavController,使所有需要引用它的可组合项都可以访问它

其本意是指,身处于NavHost中的Compose界面如果要使用Navigation跳转的话,需要使用这个NavHost自身绑定那一个NavControllerNavControllerNavHost可以有多个,但是不同的NavHost之间的NavController是不能混用的,概括起来可以这么理解 "每一个NavHost,对应一个NavController",栗子🌰(来源自亲爱的好工友 hanbing 好哥哥):

wecom-temp-2466471577c50ef847ec27173345747b.png

wecom-temp-b0c261a7a38de89f670fb724fd68b76a.png

那么如何进行跳转?

先看下NavHost初始化源码:

@Composable
public fun NavHost(
    navController: NavHostController,
    startDestination: String,
    modifier: Modifier = Modifier,
    route: String? = null,
    builder: NavGraphBuilder.() -> Unit
) {
    NavHost(
        navController,
        remember(route, startDestination, builder) {
            navController.createGraph(startDestination, route, builder)
        },
        modifier
    )
}

看下关键的参数

  • navController
  • startDestination
  • route
  • builder navController上文已经提过,用来绑定NavController

startDestination的作用与上篇文章导航图中的 startDestination如出一辙,都是定义了初始界面。

route的作用我的理解是用于不同的NavHost之间的跳转使用的。

最重要的,builderbuilder的类型是NavGraphBuilder.() -> Unit,那么 回到NavHost的初始化这里来

NavHost(navController = navController, startDestination = "A") {
    composable("A") {
        ScreenA("A")
    }
    composable("B") {
        ScreenB("B")
    }
    composable("C") {
        ScreenC("C")
    }
}

显而易见lambda内部即是对导航图的定义,包含了当前NavHost的所有页面集合,并且定义了页面属性destination,看一下NavGraphBuilder.composable()方法本身:

public fun NavGraphBuilder.composable(
    route: String,
    arguments: List<NamedNavArgument> = emptyList(),
    deepLinks: List<NavDeepLink> = emptyList(),
    content: @Composable (NavBackStackEntry) -> Unit
) {
    addDestination(
        ComposeNavigator.Destination(provider[ComposeNavigator::class], content).apply {
            this.route = route
            arguments.forEach { (argumentName, argument) ->
                addArgument(argumentName, argument)
            }
            deepLinks.forEach { deepLink ->
                addDeepLink(deepLink)
            }
        }
    )
}

可以看到composable函数本身是NavGraphBuilder的扩展函数,本质是调用了NavGraphBulderaddDestination函数。可以通过这个函数来传递子级NavHost

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            val parentNavController = rememberNavController()
            val navController1 = rememberNavController()
            NavHost(navController = navController1, startDestination = "Stack1A", route = "Stack1") {
                composable("Stack1A") {
                    Greeting(parentNavController, navController1, "A", "Stack1")
                }
                composable("Stack1B") {
                    Greeting(parentNavController, navController1, "B", "Stack1")
                }
                composable("Stack1C") {
                    Greeting(parentNavController, navController1, "C", "Stack1")
                }
            }
            val navController2 = rememberNavController()
            NavHost(navController = navController2, startDestination = "Stack2A", route = "Stack2") {
                composable("Stack2A") {
                    Greeting(parentNavController, navController2, "A", "Stack2")
                }
                composable("Stack2B") {
                    Greeting(parentNavController, navController2, "B", "Stack2")
                }
                composable("Stack2C") {
                    Greeting(parentNavController, navController2, "C", "Stack2")
                }
            }
            NavHost(navController = parentNavController, startDestination = "Stack1") {
                addDestination(navController1.graph)
                addDestination(navController2.graph)
            }
        }
    }
}

具体的跳转方式跟前文一致:

navController.navigate("destination")

destination既可以传递定义单个NavHost节点是传入的composable名也可以传递deepLink等链接

image.png

参数传递

本质上是在routes参数中添加占位符,

NavHost(startDestination = "profile/{userId}") {
    ...
    composable(
        "profile/{userId}",
        arguments = listOf(navArgument("userId") { type = NavType.StringType })
    ) {...}
}

跳转时,调用navController.navigate()传递具体的参数值比如profile/123

总结

compose-navigation的引入让compose的使用更加灵活,不需要依赖的fragment以及Activity即可自行完成页面的跳转与管理维护。而且新版本的compose-navigation已经对 Parcelable类型的argument进行了支持(通过currentBackStackEntry获取参数)

// In the source screen...
navController.currentBackStackEntry?.arguments = 
    Bundle().apply {
        putParcelable("argument", argument)
    }
navController.navigate("newPage")