Jetpack Compose 中的架构总览
如果应用打算使用 Jetpack Compose 来开发,那么就可以跟以前的MVC、MVP、MVVM等乱七八糟的架构全部说拜拜,这些名词也将在Android开发当中永远地成为历史。因为 Jetpack Compose 的架构思想非常简单,只有UI层和数据层两层,即上图所示(其中 Domain Layer 是可选的层)。它的核心思想是单向数据流,以数据模型驱动界面,遵循关注点分离的原则。
上面的架构图同时也为我们重新定义了到底什么是 UI:
State和逻辑的分类
- UI element state: UI 元素提升的状态(如ScaffoldState)
- Screen or UI state: 需要在屏幕上显示的东西(如CartUiState)
- UI 行为逻辑: 如何显示状态变化(如导航逻辑或显示snackbar)
- 业务逻辑: 如何处理状态变化(如发起支付或存储用户preferences)
StateHolder
在前面的架构图中可以看到,UI 层其实细分又包含了两层:UI 元素和状态容器(StateHolder)。其中 UI 元素 就是我们常见的各种 Composable 组件,而 状态容器 则是承载这些 UI 元素 所需要的各种状态。同时状态容器也会接受从 UI 元素 中产生的各种事件,因为只要有UI交互就一定会产生事件。StateHolder 的存在避免了界面同时成为界面和状态的管理者。有一句话我觉得可以很好的表达它的职责:“吸收事件,生成状态”。
可以作为 StateHolder 状态容器的一般有两种,一种是使用持有 UI 状态的普通的类来管理,另一种是使用 ViewModel 来管理。
下面是一个使用普通的类来管理 UI 状态的示例:
class MyAppState(
val scaffoldState: ScaffoldState,
val navController: NavController,
private val resources: Resources,
...
) {
val bottomBarTabs = /* State */
val shouldShowBottomBar: Boolean // 决定什么什么时候显示BottomBar的逻辑代码
get() = /* ... */
fun navigateToBottomBarRoute(route: String) {
/* ... */} // 导航逻辑,是UI逻辑的一种类型
fun showSnackBar(message: String) {
/* ... */} // 显示SnackBar的逻辑
}
@Composable
fun rememberMyAppState(
scaffoldState: ScaffoldState = rememberScaffoldState(),
navController: NavController = rememberNavController(),
resources: Resources = LocalContext.current.resources,
...
) = remember(scaffoldState, navController, resources, ...) {
MyAppState(scaffoldState, navController, resources, /* ... */)
}
然后在 Composable 中使用自定义的 StateHolder 来读取各种状态和执行UI跳转逻辑等。
@Composable
fun MyApp() {
MyApplicationTheme {
val myAppState = rememberMyAppState()
Scaffold(
scaffoldState = myAppState.scaffoldState,
bottomBar = {
if (myAppState.shouldShowBottomBar) {
BottomBar(
tabs = myAppState.bottomBarTabs,
navigateToRoute = {
myAppState.navigateToBottomBarRoute(it)
}
)
}
}
) {
NavHost(navController = myAppState.navController, "WelcomeScreen") {
/* ... */}
}
}
}
注意:凡是要在
Composable
中使用的状态一定要使用remember
,因为Composable
是会被重复执行的(重组),所以对于自定义的状态容器类对象也要使用remember
来创建,最好是提供一个配套的remember
函数。
官方推荐的可以作为 StateHolder 的正牌状态容器其实是 ViewModel, 因为普通的状态管理类无法做到像 ViewModel
那样在横竖屏切换等配置发生改变的场景时自动恢复(但依然可通过rememberSavable
来实现同样效果)。
从某种意义上讲, ViewModel 只是一种特殊的 StateHolder ,但因为它保存在 ViewModelStore 中,所以有以下特点:
- 存活范围大:可以脱离 Composition 存在,被所有的 Composable 共享访问。
- 存活时间长:不会因为横竖屏切换后进程被杀死等情况丢失状态。
因此 ViewModel 适合管理应用级别或者屏幕级别的全局状态,各个 Composable 可以通过 viewModel()
获取 ViewModel 单例达到 “全局共享” 的效果,而且 ViewModel 更倾向于管理那些非 UI 的业务状态,业务状态中的数据往往需要脱离 UI 长期保存。
例如:
data class ExampleUiState(
val dataToDisplayOnScreen: List<Example> = emptyList(),
val userMessages: List<Message> = emptyList(),
val loading: Boolean = false
)
class ExampleViewModel(
private val repository: UserPreferRepository,
private val savedStateHandle: SavedStateHandle
): ViewModel() {
var uiState by mutableStateOf(ExampleUiState())
private set // 私有化set操作,只有当前ViewModel内部可以修改,对外部来说不可修改
fun someBusinessLogic() {
// 执行业务逻辑
// savedStateHandle.set("key", uiState)
}
}
@Composable
fun ExampleScreen(viewModel: ExampleViewModel = viewModel()) {
val uiState = viewModel.uiState
// ... 使用 uiState
Button(onClick = {
viewModel.someBusinessLogic() }) {
Text("Do Something")
}
}
在上面代码中,ExampleUiState
中包含了 userMessages
这样的领域层数据,以及 loading
这样的代表数据加载状态的数据,这些都与 UI 无关,适合用 ViewModel 进行管理。此外, ViewModel 中