搜狐技术产品

Compose单Activity架构的设计与实现

前言

随着Compose技术的普及,项目中使用Compose的业务模块也越来越多。我们都知道在“XML时代”,Google提倡使用单Activity架构即:单个Activity+多个Fragment,使用Navigation来管理各页面之间的路由关系。

什么情况下适合

单Activity架构这个“单”是相对的,可以在一个业务模块中做到单Activity,也可以在一个独立功能中做到单Activity。

如果你正在做一个全新的业务模块或者一个全新的功能,并且不仅只有一个页面,建议你考虑使用Compose下的单Activity架构,即单个Activity+多个ComposeView,使用Compose Navigation来管理各页面之间的路由关系。

我们不可能让一个Compose页面对应一个Activity入口或者再用多fragment那一套去包裹一层。

Compose下实现单Activity架构的核心点是Compose Navigation,所以我们需要了解Navigation的用法。

Navigation的基础使用
从AB页面开始

我们以A、B两个Compose页面为例,使用Compose编写UI时,我们都知道这样一段代码:

composeView.setContent {    RainbowTheme() {        Surface(            color = Color.Transparent, modifier = Modifier                .fillMaxSize()        ) {           A()        }    }}

此时页面中展示的就是A Compose页面的内容,在A Compose函数中有一个点击事件跳转到B Compose页面。

@ComposablefunA() {  Button(onClick = {  }) {      Text("跳转到B")  }}
@ComposablefunB() {    Text("B页面")}
定义NavHost

首先我们来定义一个NavHost,代码如下所示:

@ComposablefunNavHostDemo() {    val navController = rememberNavController()    NavHost(navController = navController, startDestination = xxx) {    }}

NavHost 中的startDestination为起始路由地址,设置为A的路由地址就默认显示A页面,设置为B的路由地址就默认显示B页面,与XML中startDestination用法一致。可以根据业务需求来动态修改起始路由。

将所有Compose页面都定义在NavHost中,并且每个页面都持有navController,方便在各自的页面执行跳转事件。

@ComposablefunNavHostDemo() {    val navController = rememberNavController()    NavHost(navController = navController, startDestination = A的路由地址) {        composable(A的路由地址) {            A(navController)        }        composable(B的路由地址) {            B(navController)        }    }}

路由地址是字符串格式的,我们可以定义在单独的配置文件中。我们将定义NavHost的方法放到执行Compose的起始位置,默认显示的就是起始路由地址对应的Compose页面。

composeView.setContent {    RainbowTheme() {        Surface(            color = Color.Transparent, modifier = Modifier                .fillMaxSize()        ) {          A() -> NavHostDemo()        }    }}
NavController的基础使用

此时A、B两个Compose页面都持有一个navController,借助navController我们可以执行跳转事件,这里不详细的介绍navController的用法,我们只关注当前我们能用到的两个核心方法:

  • 跳转到xxx页面:navController.navigate(xxxx)

  • 返回上一页面:navController.popBackStack()

更多使用方法,感兴趣的可以看我很久之前分享过的在Compose中使用Navigation导航,看这一篇就够了。

所以,如果我们现在想在A页面中跳转到B页面,只需要添加这一行代码即可:

@ComposablefunA(navController:NavController) {  Button(onClick = {      navController.navigate(B的路由地址)  }) {      Text("跳转到B")  }}

这样我们就完成了在单Activity架构中从A Compose页面跳转到B Compose页面的功能,但每个Compose函数都持有一个navController并不是最好的方式。

假如我们有A、B、C、D四个Compose页面,A和D页面都需要跳转到B页面,这个时候在A和D的方法中都会有这样一行代码:

navController.navigate(B的路由地址)

如果以后需要添加对应的传参数或者更改跳转到B页面的行为,也需要在每个调用处都修改,并且不同的研发可能会写出相同的功能不同的方法,造成代码重复。

NavController的最佳实践

我们可以将所有的路由方法都封装在Action中,让每个Compose函数都持有一个Action。

以具体需求的实现为例,创建一个Action对象,在Action中定义相关方法,代码如下所示:

class MessengerAction(private val navController: NavController, activity: Activity) {    /**     * 进入sms页面     */    val enterSmsPage: () -> Unit = {        navController.navigate(MessengerRouteConfig.SEND_SMS_MAIN_PAGE)    }    /**     * 退出activity     */    val finishActivity: () -> Unit = {        activity.finish()    }}

在定义NavHost的地方创建Action,同时让所有Compose方法都持有这个Action。

@ComposablefunMessengerNavHost(    startDestination: String    intent: Intent) {    val navController = rememberNavController()    val messengerAction = MessengerAction(navController, this)    NavHost(navController = navController, startDestination) {        //主页面        composable(MESSENGER_MAIN_PAGE) {            MessengerView(messengerAction)        }        //发送短信主页面        composable(SEND_SMS_MAIN_PAGE) {            SmsMainPage(messengerAction)        }    }}

如果在MessengerView中想跳转到SmsMainPage页面只需要执行下列代码:

messengerAction.enterSmsPage()

我们也可以根据具体业务拆分成多个Action,确保职责单一。

扩展

我们已经知道在单独的业务模块中可以定义NavHost来进行路由管理,那如果一个全新的轻量级项目,就一个Activity,或者多个业务模块共享同一个Activity,各个moudle之间的路由怎么关联起来呢?

我们只需要在声明模块NavHost的时候声明为NavGraphBuilder.扩展方法即可。以我很久之前的一个开源项目为例,

在两个module中分别声明各自模块的navhost。

Image
Image

然后在app主模块内集中声明各个模块的navhost即可。

Image
最后

如果一个全新的业务模块使用Compose来实现,我想Compose-单Activity架构是必要的,因为除此之外,我们只能选择一个Activity承载一个Compose页面,或者一个Activity多个Fragment来包裹每个Compose页面。这是与我们使用Compose的初衷是相违背的。