Jetpack Compose 中的架构思想

Jetpack Compose 中的架构总览

Jetpack Compose 中的架构思想_第1张图片

如果应用打算使用 Jetpack Compose 来开发,那么就可以跟以前的MVC、MVP、MVVM等乱七八糟的架构全部说拜拜,这些名词也将在Android开发当中永远地成为历史。因为 Jetpack Compose 的架构思想非常简单,只有UI层数据层两层,即上图所示(其中 Domain Layer 是可选的层)。它的核心思想是单向数据流,以数据模型驱动界面,遵循关注点分离的原则。

上面的架构图同时也为我们重新定义了到底什么是 UI:

Jetpack Compose 中的架构思想_第2张图片

State和逻辑的分类

  • UI element state: UI 元素提升的状态(如ScaffoldState)
  • Screen or UI state: 需要在屏幕上显示的东西(如CartUiState)
  • UI 行为逻辑: 如何显示状态变化(如导航逻辑或显示snackbar)
  • 业务逻辑: 如何处理状态变化(如发起支付或存储用户preferences)

StateHolder

在前面的架构图中可以看到,UI 层其实细分又包含了两层:UI 元素状态容器(StateHolder)。其中 UI 元素 就是我们常见的各种 Composable 组件,而 状态容器 则是承载这些 UI 元素 所需要的各种状态。同时状态容器也会接受从 UI 元素 中产生的各种事件,因为只要有UI交互就一定会产生事件。StateHolder 的存在避免了界面同时成为界面和状态的管理者。有一句话我觉得可以很好的表达它的职责:“吸收事件,生成状态”。

Jetpack Compose 中的架构思想_第3张图片

可以作为 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来实现同样效果)。

Jetpack Compose 中的架构思想_第4张图片

从某种意义上讲, 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 中可以利用 SavedStateHandle 实现对 UiState 的持久化保存。

注意:viewModel() 是一个 Composable 函数,专门用于在 Composable 中创建或获取对应类型的 ViewModel 对象实例,使用它需要单独添加依赖:androidx.lifecycle:lifecycle-viewmodel-compose:2.5.1

viewModel() 会从最近的 ViewModelStore 中获取ViewModel 实例,这个 ViewModelStore 可能是一个 Activity,也可能是一个 Fragment。如果ViewModel 实例不存在,就会创建一个新的并存入 ViewModelStore 中。只要 ViewModelStore 不销毁,其内部的ViewModel 将一直存活。例如一个 Activity 中的 Composable 通过 viewModel()创建的 ViewModel 被当前的 Activity 持有。在 Activity销毁之前,ViewModel 将一直存在,每次调用viewModel()都会返回同一个实例,所以它可以不用 remember 进行缓存。

UI 元素接收到用户的输入事件之后,会向 ViewModel 通知,ViewModel 在经过业务逻辑的处理之后会更新状态,而被更新的状态则会向 UI 元素反馈(注意这种反馈是UI 元素主动感知的,而非被动,得益于mutableStateOf)。在整个过程当中,事件的处理形成一种单向的流

Jetpack Compose 中的架构思想_第5张图片

ViewModel 作为唯一数据来源的使用总结:

  • 将状态保留在组合之外
  • 访问业务逻辑以及在屏幕上显示的东西 (UI state)
  • 依赖于层次结构的其他层(例如数据和业务层)
  • 推荐用于屏幕级的 Composable 组合

ViewModel 和 普通状态管理类 该如何选择:

  • ViewModel 优于普通状态管理类的地方:
    • 配置更改后操作仍然有效,提供全局唯一单例
    • 可以方便的与Jetpack相关库集成
    • 缓存在导航路由堆栈中,并在目的地弹出时清除
  • 如果这些好处对你的业务场景不适用,则应该更倾向于使用简单的普通状态管理类
  • 普通的状态管理类可以依赖于 ViewModel
  • 当State需要多实例的场景时,建议使用普通的状态管理类

这里值得一提的是,在一些复杂的场景中,ViewModel 和 普通状态管理类 是可以并存的 ,二者并不冲突。Composable 或者 StateHolder 可以依赖 ViewModel 管理 UI 无关的状态及对应的业务逻辑。借助 ViewModel,这些状态可以跨越 Composable 甚至 Activity 的生命周期长期存在。而 ViewModel 依赖于更底层的领域层或者数据层完成相关业务,因为处于底层的业务服务范围往往更广,存活时间也更长

Jetpack Compose 中的架构思想_第6张图片

Jetpack Compose 中的架构思想_第7张图片

Stateful 组件与 Stateless 组件

只依赖参数的 Composable 组件被称为 Stateless 组件,例如以下代码:

@Composable
fun Greeting(name: String) {
	Text(text= "Hello $name")
}

Greeting除了依赖参数以外,不依赖任何其他状态。

相对的,在 Composable 内部持有或者访问某些状态的组件是 Stateful 组件。

Stateless 的 Composable 的重组只能来自其上层调用的 Composable ,而 Stateful 的 Composable 的重组来自其依赖的状态的变化。 Stateless 的 Composable 是一个 “纯函数”,也就是完全没有副作用的函数,参数是其变化的唯一来源,只要参数不变UI就不会变化。因此 Compose 编译器对其进行了优化,当Stateless 的参数没有变化时会对其跳过重组,重组范围局限在Stateless的外部。

Jetpack Compose 中的架构思想_第8张图片

如果有了解过 Flutter 开发可能更加容易理解这个概念,在 Flutter 中,直接提供了两种显示的定义 Widget 组件的方式:即 StatefulWidget 和 StatelessWidget。

State Hoisting (状态提升)

Jetpack Compose 中的架构思想_第9张图片

简单的来说,状态提升就是将 Stateful 的组件改造成 Stateless 的组件。状态提升可以使 Composable 组件提高复用性,同时由于 Stateless 组件不耦合任何业务逻辑,功能更加纯粹,因此也提供了其可测试性。状态提升使 Composable 更加容易遵循单一数据源的原则,降低出现bug的风险。

状态提升通常的做法就是将内部状态移除,然后通过参数来传入内部需要UI显示的状态以及需要处理的回调事件,即将状态提升到调用者那一层,由调用者负责为子级组件提供状态和处理事件的方法,Stateless 是相对于调用者而言的。

Jetpack Compose 中的常规状态提升模式是将状态变量替换为两个参数:

  • value: T:要显示的当前值
  • onValueChange: (T) -> Unit:请求更改值的事件,其中 T 是建议的新值

不过,事件处理的回调并不局限于 onValueChange。你可以使用 lambda 定义任何更加具体的事件。

例如,以下代码中 CartItem 组件将其依赖状态和事件处理向外部公开,这样 CartItem 组件就变成了一个 Stateless 组件:

Jetpack Compose 中的架构思想_第10张图片

然后在调用 CartItem 的地方为其提供所需状态和处理事件的方法:

Jetpack Compose 中的架构思想_第11张图片

注意这里 Cart 组件并没有直接给出事件处理的具体实现,而是将其委托给 CartViewModel , 因为具体的事件处理属于业务逻辑的范畴。

注意:使用viewModel() 方法的Composable无法进行预览,因此通过状态提升将需要预览的部分提取为 Stateless 组件更加有助于调试。

另外前面提到 ViewModel 作为正牌的状态容器,它可以向 UI 提供状态,但是有一点需要注意的是它不能持有由 Composable 创建的状态,也就是说 ViewModel 的内部可以创建并暴露一些 UI stateUI elements,而 UI elements 中的创建的 state 不能传入 ViewModel 中。因为 ViewModel 不属于组合树的一部分,它的生命周期比组合更长,因此它不应该接受组合作用域内的状态(假如你这样做了,那么要谨慎了,因为这可能导致内存泄漏)。

相应的,调用者如果是通过 ViewModelStateless 组件提供状态必须是在 ViewModel 内部创建且必须保证这些创建的 State 必须是可追踪/可观察的(通过mutableStateOf 实现):

Jetpack Compose 中的架构思想_第12张图片

如果 ViewModel 内部创建的 State 不是通过mutableStateOf 实现的(例如LiveDataFlow或者RxJava等,这可能是之前旧项目中留下来的),那么此时调用者应该使用 Compose 中提供的相应的转换方法来转换成可被 Composable 观察的状态。例如针对 Flow可使用 collectAsState 函数转换:

Jetpack Compose 中的架构思想_第13张图片

(其他库的转换可参考之前在Jetpack Compose中的副作用中提到的关于第三方库适配的部分。)

总而言之, Composable 的状态提升是不能无限提升的,最高可提升至的层次应该是某个路由导航目的地的屏幕级 Composable 中,因为再往上没有了,也就是到达了根路由在 NavHost 中的配置。

Jetpack Compose 中的架构思想_第14张图片

但并不是 UI elements 中的每一个状态和事件处理方法都需要提升至最顶层的屏幕级可组合项中的,那么状态提升到底要提升到哪一个层次较为合适呢?关于这一点,官方也给出了两条建议:

  • 状态应该提升至所有消费该状态的父组件中的最低公共父组件中
  • Composable 向调用者公开的参数应该仅传递它需要的参数,减少不必要的多余参数

Domain Layer (领域层)

开头提到过,Domain Layer 是一个可选的层,也就说它是可有可无的一层,至于其是否真的有存在的必要性,只能说仁者见仁智者见智了。但是,如果应用中存在了 Domain Layer 这一层,那么至少需要保证的是该层应该只包含纯业务逻辑,而不应该包含UI状态。

Jetpack Compose 中的架构思想_第15张图片

其中,Domain Layer 中的 UseCase 可以横向依赖该层的其他 UseCase ,也可以向下依赖 Data Layer 层中的不同的 Repository 。但是它不能向上依赖 UI Layer(即它不能包含 UI状态)层,相反的, UI Layer 应该依赖 Domain Layer 中的 UseCaseUseCase 可以选择向 UI 层暴露挂起函数、Flow或者是回调方法等。

Jetpack Compose 中的架构思想_第16张图片

Jetpack Compose 中的架构思想_第17张图片

UseCase 的创建

关于 UseCase 的实现方式,官方给出的一点建议是使 UseCase 可调用(重载invoke操作符),这样的好处是可以让别人明显的知道你在调用领域层的类。

Jetpack Compose 中的架构思想_第18张图片

UseCase 的线程安全

关于线程方面,官方给出了两条建议:

  1. 确保主线程安全, 如果一个 UseCase 中执行耗时过长的任务,那么应该让它脱离主线程,把它移动到子线程中执行。
  2. 如果一个任务执行的结果可以被缓存,那么可以将该任务移动到 Data Layer 中。

封装和提取可重用的业务逻辑

最常见的场景可能是在 UI 层调用的一些工具类:

Jetpack Compose 中的架构思想_第19张图片

注意 UI 层包括 UI elementsSateHolder ,所以这里的提到的 Util Classes 多半存在于 ViewModel 这样的 SateHolder 当中。

此时,可以将这些 Util Classes 提取并下沉到领域层作为 UseCase 实现,以达到复用性目的。

Jetpack Compose 中的架构思想_第20张图片

合并来自多个Repository的数据

如果 UI 层的 ViewModel 中需要同时访问多个来自数据层的 Repository,然后将它们获得的结果进行合并后为 UI elements 提供显示状态。

Jetpack Compose 中的架构思想_第21张图片

此时就可以将访问多个 Repository 合并数据的逻辑下沉到领域层作为 UseCase 来实现。

Jetpack Compose 中的架构思想_第22张图片

同时,需要注意在 UseCase 的代码实现时不应该阻塞调用者的线程(因为调用者来自UI层),可以选择将invoke函数声明为挂起函数,将请求数据合并的逻辑移动到子线程调度器的上下文中执行,并将协程调度器作为公开参数以便调用者可以自行决定在哪个线程中执行。这种做法也可以同时提高可测试性。

Jetpack Compose 中的架构思想_第23张图片

Domain Layer 总结:

  • 减少 UI Layer 的复杂度
  • 避免重复性工作,提高可复用性
  • 提高可测试性

Data Layer (数据层)

Data Layer 位于分层架构的最底层,它向其他层提供数据,同时其他层只能通过 Data Layer 来获取数据和修改数据。

Jetpack Compose 中的架构思想_第24张图片

Data Layer 是应用中真正提供数据源的地方,它由 3 个关键角色组成:Repositories(存储仓库)、Data Sources(数据源)、Data Models(数据模型)

Jetpack Compose 中的架构思想_第25张图片

Data Source

数据源比较简单,一般情况下,它分为两种类型:本地数据库和网络数据

Jetpack Compose 中的架构思想_第26张图片

对于本地数据,我们可以使用Room数据库来管理,对于网络数据一般是单独创建管理网络请求的业务类来管理。

Repository

Data Layer 中使用 Repository 存储仓库和数据源进行打交道,每个存储仓库可以与0个、1个或者多个数据源进行交互。其中数据源可以是网络、本地数据库、文件、DataStore甚至是内存数据等。

Jetpack Compose 中的架构思想_第27张图片

Repository 存储仓库的主要职责:

  1. 向应用其余层公开数据访问
  2. 集中处理对数据的更改
  3. 解决多个数据源之间的冲突
  4. 容纳业务逻辑

每一个 Repository 存储仓库应该对应一种数据类型,而每一个 Data Source 中通常应该只负责一种业务类型。例如,电影相关的和支付相关的数据处理应该分别创建一个 Repository 来管理。

Jetpack Compose 中的架构思想_第28张图片

存储仓库应该负责解决本地数据缓存和远程服务器数据之间存在的冲突:

Jetpack Compose 中的架构思想_第29张图片

请记住,应用中的其他所有层都不能与数据源直接进行交互,访问数据源的入口应当始终都是 Repository 类。Repository 类的一种常用模式是执行一次性调用操作,例如创建、读取、更新和删除。这些可通过 Kotlin 中的 suspend 函数来实现。

Jetpack Compose 中的架构思想_第30张图片

但是也可以通过公开数据流(例如使用Flow)来获得数据随时间变化的通知:

Jetpack Compose 中的架构思想_第31张图片

处理多个数据源

在一个Repository中处理多个数据源可能比较棘手,你需要选择一个可靠的来源并确保它始终处于一致的状态。

例如,新闻业务依赖本地和网络两个数据源,现在有两个新闻数据源,其中LocalNewsDataSource 依赖一个本地 room 数据库,而RemoteNewsDataSource依赖于一个远程 API 客户端(例如Retrofit):

Jetpack Compose 中的架构思想_第32张图片

然后,向其他层提供新闻列表的 NewsRepository 同时包含上面两个数据源:

Jetpack Compose 中的架构思想_第33张图片

fectNews() 方法中,首先从远程数据源中获取,如果成功就会更新本地数据源,如果失败或更新过程中出现异常,会打印日志,或者在这里提醒用户。但是无论如何,最终返回的都是从本地数据源中查询的结果,因为它是我们可靠的来源。

Jetpack Compose 中的架构思想_第34张图片
这种情况非常简单,因为我们使用的是用户无法修改的数据。但是有些情况下可能会更加复杂一些,例如一个日历应用,如果有两个用户同时修改了其中的某个会议,这时就需要考虑的更多一些以确保用户的体验。

数据同步

总之在存在多个数据源的情况下,情况比较复杂,我们需要在不同数据源之间进行数据同步。

Jetpack Compose 中的架构思想_第35张图片

数据同步可以分为三种类型:

Jetpack Compose 中的架构思想_第36张图片

Jetpack Compose 中的架构思想_第37张图片
Jetpack Compose 中的架构思想_第38张图片

多个层级的 Repository

存储仓库可能依赖于多个数据源,在某些情况下,你可能希望有多个级别的存储仓库。

例如,用户存储仓库可能需要来自于日志记录存储仓库和注册存储仓库的数据:

Jetpack Compose 中的架构思想_第39张图片

同时,一个数据源可能被多个存储仓库共用。这种分层的设计只是一种推荐的做法,在实践中你可以有不同的实现。

Data Model

关于 Data Model 的核心关键是不可变性(immutable),也就是说其它层不能直接修改 Data Model 的属性,如果其它层想要做出业务数据的修改或更新,它们必须通过 Data Layer 发起请求交给 Repository 去处理

Jetpack Compose 中的架构思想_第40张图片

建议在定义数据模型时,用一个数据模型表达一种业务模式。无论一种业务模式在内部有多少种业务数据模型,对外只暴露一个数据模型。

公开的数据应该确保不可变性

数据层公开的数据应该确保不可变性,任何类都不能对其进行篡改,篡改数据可能造成数据不一致问题。不可变性的另一个好处是,它可以被多线程安全地处理。实现不可变性的最好办法就是使用 Kotlin 的 data class

Jetpack Compose 中的架构思想_第41张图片

这里还需要考虑的一点是数据库或远程API返回的实体类模型可能并不是其他层所需要的模型,所以最好是单独创建一个实体模型,确保只提供其他层所需要的那些数据而不是所有数据:

这样不仅代码更简洁而且还能更好地隔离潜在的问题。

线程安全

同样地,调用数据源和存储仓库也不应该阻塞主线程,对于长时间的获取数据源的方法,存储仓库应该负责将其执行移至其他线程中:

Jetpack Compose 中的架构思想_第42张图片

异常处理

关于在请求数据源时可能发生的异常处理,一种方式是不处理,向外抛出异常,然后在 Domain LayerUI Layer 层中调用处通过常规的 try-catch 捕获异常进行处理:

Jetpack Compose 中的架构思想_第43张图片

或者如果是使用的 Kotlin Flow,可以使用 catch 运算符:

在这里插入图片描述

另外一种异常处理的方式是在数据层的内部处理,然后以一种更容易理解的方式,向外公开包含成功或者失败的结果数据,但不要忘记错误处理。

Entities划分

应用中会涉及到许多的 Entity 实体类,根据所处的架构层次,可将 Entity 做如下分类:

  • Remote Entities:远程查询请求对应的实体类(Data Layer)
  • Database Entities:数据库查询对应的实体类(Data Layer)
  • Domain Entities:领域层使用的实体类
  • UiState: UI层使用的实体类


Jetpack Compose 中的架构思想_第44张图片

更多关于 Data Layer 构建范例请参考官方的 Codelab:Building a Data Layer

Compose 中的状态持久化与恢复

对应以前 View 系统的状态持久化方式在 Jetpack Compose 中都有相类似的解决方案:

Jetpack Compose 中的架构思想_第45张图片

例如,对于以前 View 系统中的onSaveInstanceState API,现在 Jetpack Compose 中则有对应的 rememberSaveable API来对应:

Jetpack Compose 中的架构思想_第46张图片
Jetpack Compose 中的架构思想_第47张图片

对于 Jetpack Compose 中的状态持久化,主要有两种方式:

  • ViewModel:它保存在 Activity/FragmentViewModelStore 中,在Activity因为横竖屏切换等配置变更导致页面销毁重建之后,仍然可以获取到之前的 ViewModel 实例。前面提到过,它实际上是一种全局共享状态,因此 Composable 可以借助 ViewModel 单例来实现状态的持久化。这里可持久化的状态比较适合于那些在 ViewModel 内部创建的公开给 ComposableUiState,而对于 Composable 内创建的状态不太适合通过 ViewModel 持久化保存(虽然也可以通过ViewModel中的SavedStateHandle来实现,可参考这里)。
  • rememberSaveable: 这个 API 是对 remember 的封装容器,可在 Bundle 中存储数据。 remember 缓存的状态可以跨越重组,但不能跨越Activity重建或进程重建。而 rememberSaveable 不仅能让状态在重组后保留下来,还能让状态在重新创建 activity 和系统发起的进程终止后继续留存。它可以像 Activity 的 onSaveInstanceState() 那样在进程被杀死时自动保存状态,同时像 onRestoreInstanceState() 一样随进程重建而自动恢复。

ViewModel

ViewModel 在前面也提到了它是官方推荐的正牌状态容器,你可以在其中存储纯 UI 元素相关的状态,也可以存储一些基于业务逻辑(如处理或显示UI元素的行为)相关的 UI 状态,也可以接收来自下层 Data Layer 层读取的数据,并对其进行一些业务逻辑处理之后生成 UI 元素需要的状态。

它的最大特点就是可以跨越配置变更而存活(如横竖屏切换),具体关于ViewModel的详细使用请参考官方文档或者我之前的文章 - Jetpack架构组件库:Lifecycle、LiveData、ViewModel 中的 ViewModel 部分。这里不再赘述。

SavedStateHandle

SavedStateHandle其实是ViewModel的一种高级用法,因为一般用法是在ViewModel的构造函数中添加一个SavedStateHandle参数,前面也提到它也可以用于保存Composable组件内部需要临时存储的UI元素状态,我们可以借助它来存储一些来自UI元素的轻量级的键值对信息。

通常,SavedStateHandle 适合保存的数据都是一些临时状态,根据用户的输入或导航而定。例如:列表的滚动位置、详细页面对应的 Item 的 ID、用户正在进行的偏好设置选择或者正在输入的文本字段等等。

SavedStateHandle 具有用于存储界面元素状态的不同类型 API :

Jetpack Compose 中的架构思想_第48张图片

SavedStateHandlesaveable API 以支持 Compose 的 MutableState 形式读取和写入界面元素状态,以便只需极少的代码设置就能在重新创建 activity 和进程后继续保留状态。saveable API 开箱就支持基础类型,并会接受一个 stateSaver 参数,以便使用自定义 Saver(就像 rememberSaveable() 一样)。

例如,在以下代码段中,message 会将用户输入类型存储在 TextField 中:

@OptIn(SavedStateHandleSaveableApi::class)
class ConversationViewModel(
   private val savedStateHandle: SavedStateHandle
) : ViewModel() {

   var message by savedStateHandle.saveable(stateSaver = TextFieldValue.Saver) {
      mutableStateOf(TextFieldValue(""))
   }
       private set
       
   fun update(newMessage: TextFieldValue) {
       message = newMessage
   }
   // …
}

@Composable
fun UserInput() {
   TextField(
       value = viewModel.message,
       onValueChange = { viewModel.update(it) }
   )
}

另外,如果你更喜欢使用StateFlow,则可以通过SavedStateHandlegetStateFlow() 来存储界面元素状态,StateFlow 是只读的,该 API 会要求您指定一个Key,以便替换数据流以发出新值。您可以使用配置的Key来向SavedStateHandle 检索 StateFlow 并收集最新值。

例如,在以下代码段中,savedFilterType 是一个 StateFlow 变量,用于存储应用于聊天应用中的聊天频道列表的过滤器类型:

private const val CHANNEL_FILTER_SAVED_STATE_KEY = "ChannelFilterKey"

class ChannelViewModel(
   private val channelsRepository: ChannelsRepository,
   private val savedStateHandle: SavedStateHandle
) : ViewModel() {

   private val savedFilterType: StateFlow<ChannelsFilterType> =
       savedStateHandle.getStateFlow(key = CHANNEL_FILTER_SAVED_STATE_KEY, initialValue = ALL_CHANNELS)

   private val filteredChannels: Flow<List<Channel>> =
       combine(channelsRepository.getAll(), savedFilterType) { channels, type -> filter(channels, type)}
   	  .onStart { emit(emptyList<List<Channel>>()) }

   fun setFiltering(requestType: ChannelsFilterType) {
       savedStateHandle[CHANNEL_FILTER_SAVED_STATE_KEY] = requestType
   }
   // ...
}

enum class ChannelsFilterType {
   ALL_CHANNELS,
   RECENT_CHANNELS,
   ARCHIVED_CHANNELS
}

每次用户选择新的过滤器类型时,系统都会调用 setFiltering。这会在 SavedStateHandle 中保存使用键 CHANNEL_FILTER_SAVED_STATE_KEY 存储的新值。savedFilterType 是发送存储到Key中的最新值的数据流。filteredChannels 已订阅该数据流以执行频道过滤。

更多具体关于 SavedStateHandle 的详细使用请参考官方文档或者我之前的文章 - Jetpack架构组件库:Lifecycle、LiveData、ViewModel 中的 ViewModel 的进阶用法部分。这里不再赘述。

ViewModel 中使用 SavedStateHandle 相当于使得原本跨越配置变更而存活提升到了跨越进程而存活的层次,但是这种跨越进程而存活是有条件的,你只需要记住比较重要的一点是,SavedStateHandle 只有在系统发起的界面状态解除情景中,才会恢复,而在用户发起的界面状态解除情景中,不会恢复保存的状态(因为这是正常终止)。并且,仅当 Activity 停止时(onStop),SavedStateHandle 才会保存写入其中的数据。

rememberSaveable

这个就是用来对标以前 View 系统中的onSaveInstanceState API。

rememberSaveable 中的数据会以 Bundle 的形式通过 LocalSaveableStateRegistry 存储在内部的 Map 当中,并在进程或者 Activity 重建时根据 key 恢复到对应的 Composable 中,这个 key 就是 Composable 在编译期被确定的唯一标识。注意,在由系统发起的进程终止时,状态会保存,而用户手动退出应用时,状态才会被清空。

以下是使用rememberSaveable的一个简单示例:

@Composable
fun HelloScreen() { 
    var name by rememberSaveable { mutableStateOf("") } // 在配置更改(如旋转屏幕)后保持状态 
    // var name by remember { mutableStateOf("") } // 这种方式不能在配置更改后保持状态 
    HelloContent(name = name, onNameChange = { name = it })
}


@Composable
fun HelloContent(name: String, onNameChange: (String) -> Unit) {
    Column(modifier = Modifier.padding(16.dp)) {
        Text(
            text = "Hello, $name",
            modifier = Modifier.padding(bottom = 8.dp),
            style = MaterialTheme.typography.titleSmall
        )
        OutlinedTextField(
            value = name,
            onValueChange = onNameChange,
            label = { Text("Name") }
        )
    }
}

上面代码在屏幕旋转Activity重建之后,依然能够在TextOutlinedTextField中保持用之前输入的文本(而常规的remember 方式则不行):

Jetpack Compose 中的架构思想_第49张图片

下面的代码中,showDetails 用于存储聊天气泡是收起还是展开:

@Composable
fun ChatBubble(
    message: Message
) {
    var showDetails by rememberSaveable { mutableStateOf(false) }
    ClickableText(
        text = message.content,
        onClick = { showDetails = !showDetails }
    )
    if (showDetails) {
        Text(message.timestamp)
    }
}

而在以下代码段中,rememberLazyListState Compose API 会使用 rememberSaveable 存储 LazyListState,其中包含 LazyColumnLazyRow 的滚动状态。该 API 使用 LazyListState.Saver,这是能够存储和恢复滚动状态的自定义 Saver。在重新创建 activity 或进程后(例如,在设备屏幕方向等配置发生更改后),滚动状态将得以保留。

@Composable
fun rememberLazyListState(
    initialFirstVisibleItemIndex: Int = 0,
    initialFirstVisibleItemScrollOffset: Int = 0
): LazyListState {
    return rememberSaveable(saver = LazyListState.Saver) {
        LazyListState(
            initialFirstVisibleItemIndex,
            initialFirstVisibleItemScrollOffset
        )
    }
}

rememberSaveable 可以支持所有 Bundle支持的数据类型,如果要保存的内容无法被 Bundle 支持,例如对象类型,可以通过以下几种方式解决:

1. 向对象添加 @Parcelize 注解,例如,以下代码将 City 类变为一个 Parcelable 对象,由于 MutableState 本身也是一个 Parcelable 对象,因此可以直接保存到 rememberSaveable 中。

@Parcelize
data class City(val name: String, val country: String) : Parcelable

@Composable
fun CityScreen() {
    var selectedCity = rememberSaveable {
        mutableStateOf(City("Madrid", "Spain"))
    }
}

使用@Parcelize注解需要在gradle中添加 kotlin-parcelize 插件: plugins { id 'kotlin-parcelize' },当一个类实现 Parcelable 接口且添加@Parcelize注解时,Kotlin 编译器会自动为其添加 Parcelable 的相关实现。

2. 自定义 Saver,如果某种原因导致 @Parcelize 不合适,比如定义在第三方库中的类,此时可以通过实现 Saver 接口来自定义保存和恢复数据的逻辑,然后在调用rememberSaveable时传入此 Saver 实例即可。

data class City(val name: String, val country: String)

object CitySaver: Saver<City, Bundle> {
    val key1 = "name"
    val key2 = "country"
    
    override fun restore(value: Bundle): City? {
        return value.getString(key1)?.let { name ->
            value.getString(key2)?.let { country ->
                City(name, country)
            }
        }
    }

    override fun SaverScope.save(value: City): Bundle? {
        return Bundle().apply { 
            putString(key1, value.name)
            putString(key2, value.country)
        } 
    }
}
@Composable
fun CityScreen() {
    var selectedCity = rememberSaveable(stateSaver = CitySaver) {
        mutableStateOf(City("Madrid", "Spain"))
    }
}

3. MapSaver,类似 Saver 接口,mapSaver使用更加简单,同样只需实现保存和恢复的逻辑即可。

data class City(val name: String, val country: String)

val CitySaver = run {
    val nameKey = "Name"
    val countryKey = "Country"
    mapSaver(
        save = { mapOf(nameKey to it.name, countryKey to it.country) },
        restore = { City(it[nameKey] as String, it[countryKey] as String) }
    )
}

@Composable
fun CityScreen() {
    var selectedCity = rememberSaveable(stateSaver = CitySaver) {
        mutableStateOf(City("Madrid", "Spain"))
    }
}

4. ListSaver,使用 listSaver 可以将其索引作为key,避免自己写key

data class City(val name: String, val country: String)

val CitySaver = listSaver<City, Any>(
    save = { listOf(it.name, it.country) },
    restore = { City(it[0] as String, it[1] as String) }
)

@Composable
fun CityScreen() {
    var selectedCity = rememberSaveable(stateSaver = CitySaver) {
        mutableStateOf(City("Madrid", "Spain"))
    }
}

rememberSaveable 还可以接收一个 inputs 可变参数,其作用与 remember 接收 keys 的作用相同,即当输入参数发生更改时,缓存就会失效。下次函数重组时,rememberSaveable 会对 lambda 块重新执行计算。例如下面的示例中,rememberSaveable 会存储 userTypedQuery,直到 typedQuery 发生变化:

var typedQuery by remember { mutableStateOf("") }
var userTypedQuery by rememberSaveable(typedQuery, stateSaver = TextFieldValue.Saver) {
    mutableStateOf(
        TextFieldValue(text = typedQuery, selection = TextRange(typedQuery.length))
    )
}

如果状态只是需要跨越 ConfigurationChanged 存在,而不需要跨越进程恢复,那么可以在 AndroidManifest.xml 中设置 android:configChanges 属性,然后使用普通的 remember 即可。因为 Compose 能够在所有 ConfigurationChanged 发生时做出响应,理论上一个纯 Compose 项目不再需要因为 ConfigurationChanged 重建 Activity

最后需要注意的一点是:rememberSaveable 中的数据最终是基于 Bundle 存储的,所以不能用于存储较大的对象或大量列表对象,它只适合用于存储ID、key等一些轻量级对象。这是因为 Bundle 的大小是有限的,如果存储大型对象,可能会导致运行时出现 TransactionTooLarge 异常。对于在整个应用中使用同一 Bundle 的单个 Activity 应用,此问题尤其明显。

Persistent Storage(持久化存储)

Jetpack Compose 中的状态持久化,除了前面提到的 ViewModelrememberSaveable 以外,当然你还可以使用持久化存储,即将数据存储到本地,这样做的好处也很明显:可以跨越任意形式的进程终止而永久存在,超越一切生死

目前官方推荐的持久化存储方案主要有以下两种:

  1. Data Store:它是官方推荐的新型轻量级键值对存储方案,旨在平替以前的SharedPreferences,主要用于存储一些轻量级的简单、较小的数据结构,具体详细使用请参考官方文档或者我之前的文章 - Jetpack架构组件库:DataStore
  2. Room:它是官方推荐的新型数据库架构组件,旨在优化以前的SQLite使用方式和体验,可以用于存储大型、复杂的数据对象,支持数据的部分更新查询、参照完整性,具体详细使用请参考官方文档或者我之前的文章 - Jetpack架构组件库:Room

当然,除了以上官方推荐的两种,你也可以采用其他第三方的存储库,如MMKV等,甚至你也可以直接使用文件来存储,只要你喜欢。

持久化方案对比


Jetpack Compose 中的架构思想_第50张图片

注,上表中:

  1. Saved State APIs 主要是指 rememberSaveable (Jetpack Compose) 和 onSaveInstanceState (View system),以及ViewModel中的SavedStateHandle
  2. Configuration changes 主要是指系统配置变化,如语言、主题变化,横竖屏切换等等,这会导致应用的Activity重建。
  3. 系统内存不足导致的进程终止 主要是指App被用户切到后台,系统因为内存不足需要回收内存而将App进程杀死的情况。
  4. App进程意外终止 可能包括用户从最近应用列表中手动结束进程或者应用中出现未捕获的异常而导致进程崩溃终止等。
  5. 导航或用户输入的临时状态 主要是指如滚动列表的位置、详细页面的ID、用户正在文本框输入的文本…等等
  6. 轻量级键值对存储方案 主要是指 DataStore复杂数据库存储方案 主要是指 Room

你可能感兴趣的:(架构设计,Jetpack,Compose,架构,android,Jetpack,Compose,StateHolder,单向数据流)