如果应用打算使用 Jetpack Compose 来开发,那么就可以跟以前的MVC、MVP、MVVM等乱七八糟的架构全部说拜拜,这些名词也将在Android开发当中永远地成为历史。因为 Jetpack Compose 的架构思想非常简单,只有UI层和数据层两层,即上图所示(其中 Domain Layer 是可选的层)。它的核心思想是单向数据流,以数据模型驱动界面,遵循关注点分离的原则。
上面的架构图同时也为我们重新定义了到底什么是 UI:
在前面的架构图中可以看到,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 中,所以有以下特点:
因此 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
)。在整个过程当中,事件的处理形成一种单向的流:
ViewModel 作为唯一数据来源的使用总结:
ViewModel 和 普通状态管理类 该如何选择:
这里值得一提的是,在一些复杂的场景中,ViewModel 和 普通状态管理类 是可以并存的 ,二者并不冲突。Composable 或者 StateHolder 可以依赖 ViewModel 管理 UI 无关的状态及对应的业务逻辑。借助 ViewModel,这些状态可以跨越 Composable 甚至 Activity 的生命周期长期存在。而 ViewModel 依赖于更底层的领域层或者数据层完成相关业务,因为处于底层的业务服务范围往往更广,存活时间也更长。
只依赖参数的 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的外部。
如果有了解过 Flutter 开发可能更加容易理解这个概念,在 Flutter 中,直接提供了两种显示的定义 Widget 组件的方式:即 StatefulWidget 和 StatelessWidget。
简单的来说,状态提升就是将 Stateful 的组件改造成 Stateless 的组件。状态提升可以使 Composable 组件提高复用性,同时由于 Stateless 组件不耦合任何业务逻辑,功能更加纯粹,因此也提供了其可测试性。状态提升使 Composable 更加容易遵循单一数据源的原则,降低出现bug的风险。
状态提升通常的做法就是将内部状态移除,然后通过参数来传入内部需要UI显示的状态以及需要处理的回调事件,即将状态提升到调用者那一层,由调用者负责为子级组件提供状态和处理事件的方法,Stateless 是相对于调用者而言的。
Jetpack Compose 中的常规状态提升模式是将状态变量替换为两个参数:
不过,事件处理的回调并不局限于 onValueChange
。你可以使用 lambda 定义任何更加具体的事件。
例如,以下代码中 CartItem
组件将其依赖状态和事件处理向外部公开,这样 CartItem
组件就变成了一个 Stateless 组件:
然后在调用 CartItem
的地方为其提供所需状态和处理事件的方法:
注意这里 Cart
组件并没有直接给出事件处理的具体实现,而是将其委托给 CartViewModel
, 因为具体的事件处理属于业务逻辑的范畴。
注意:使用
viewModel()
方法的Composable无法进行预览,因此通过状态提升将需要预览的部分提取为 Stateless 组件更加有助于调试。
另外前面提到 ViewModel 作为正牌的状态容器,它可以向 UI 提供状态,但是有一点需要注意的是它不能持有由 Composable 创建的状态,也就是说 ViewModel 的内部可以创建并暴露一些 UI state 给 UI elements,而 UI elements 中的创建的 state 不能传入 ViewModel 中。因为 ViewModel 不属于组合树的一部分,它的生命周期比组合更长,因此它不应该接受组合作用域内的状态(假如你这样做了,那么要谨慎了,因为这可能导致内存泄漏)。
相应的,调用者如果是通过 ViewModel 向 Stateless 组件提供状态必须是在 ViewModel 内部创建且必须保证这些创建的 State 必须是可追踪/可观察的(通过mutableStateOf
实现):
如果 ViewModel 内部创建的 State 不是通过mutableStateOf
实现的(例如LiveData、Flow或者RxJava等,这可能是之前旧项目中留下来的),那么此时调用者应该使用 Compose 中提供的相应的转换方法来转换成可被 Composable 观察的状态。例如针对 Flow可使用 collectAsState
函数转换:
(其他库的转换可参考之前在Jetpack Compose中的副作用中提到的关于第三方库适配的部分。)
总而言之, Composable 的状态提升是不能无限提升的,最高可提升至的层次应该是某个路由导航目的地的屏幕级 Composable 中,因为再往上没有了,也就是到达了根路由在 NavHost 中的配置。
但并不是 UI elements 中的每一个状态和事件处理方法都需要提升至最顶层的屏幕级可组合项中的,那么状态提升到底要提升到哪一个层次较为合适呢?关于这一点,官方也给出了两条建议:
开头提到过,Domain Layer 是一个可选的层,也就说它是可有可无的一层,至于其是否真的有存在的必要性,只能说仁者见仁智者见智了。但是,如果应用中存在了 Domain Layer 这一层,那么至少需要保证的是该层应该只包含纯业务逻辑,而不应该包含UI状态。
其中,Domain Layer 中的 UseCase 可以横向依赖该层的其他 UseCase ,也可以向下依赖 Data Layer 层中的不同的 Repository 。但是它不能向上依赖 UI Layer(即它不能包含 UI状态)层,相反的, UI Layer 应该依赖 Domain Layer 中的 UseCase 。 UseCase 可以选择向 UI 层暴露挂起函数、Flow或者是回调方法等。
关于 UseCase 的实现方式,官方给出的一点建议是使 UseCase 可调用(重载invoke
操作符),这样的好处是可以让别人明显的知道你在调用领域层的类。
关于线程方面,官方给出了两条建议:
最常见的场景可能是在 UI 层调用的一些工具类:
注意 UI 层包括 UI elements 和 SateHolder ,所以这里的提到的 Util Classes 多半存在于 ViewModel 这样的 SateHolder 当中。
此时,可以将这些 Util Classes 提取并下沉到领域层作为 UseCase 实现,以达到复用性目的。
如果 UI 层的 ViewModel 中需要同时访问多个来自数据层的 Repository,然后将它们获得的结果进行合并后为 UI elements 提供显示状态。
此时就可以将访问多个 Repository 合并数据的逻辑下沉到领域层作为 UseCase 来实现。
同时,需要注意在 UseCase 的代码实现时不应该阻塞调用者的线程(因为调用者来自UI层),可以选择将invoke
函数声明为挂起函数,将请求数据合并的逻辑移动到子线程调度器的上下文中执行,并将协程调度器作为公开参数以便调用者可以自行决定在哪个线程中执行。这种做法也可以同时提高可测试性。
Domain Layer 总结:
Data Layer 位于分层架构的最底层,它向其他层提供数据,同时其他层只能通过 Data Layer 来获取数据和修改数据。
Data Layer 是应用中真正提供数据源的地方,它由 3 个关键角色组成:Repositories(存储仓库)、Data Sources(数据源)、Data Models(数据模型)
数据源比较简单,一般情况下,它分为两种类型:本地数据库和网络数据
对于本地数据,我们可以使用Room数据库来管理,对于网络数据一般是单独创建管理网络请求的业务类来管理。
Data Layer 中使用 Repository 存储仓库和数据源进行打交道,每个存储仓库可以与0个、1个或者多个数据源进行交互。其中数据源可以是网络、本地数据库、文件、DataStore甚至是内存数据等。
Repository 存储仓库的主要职责:
每一个 Repository 存储仓库应该对应一种数据类型,而每一个 Data Source 中通常应该只负责一种业务类型。例如,电影相关的和支付相关的数据处理应该分别创建一个 Repository 来管理。
存储仓库应该负责解决本地数据缓存和远程服务器数据之间存在的冲突:
请记住,应用中的其他所有层都不能与数据源直接进行交互,访问数据源的入口应当始终都是 Repository 类。Repository 类的一种常用模式是执行一次性调用操作,例如创建、读取、更新和删除。这些可通过 Kotlin 中的 suspend
函数来实现。
但是也可以通过公开数据流(例如使用Flow)来获得数据随时间变化的通知:
在一个Repository中处理多个数据源可能比较棘手,你需要选择一个可靠的来源并确保它始终处于一致的状态。
例如,新闻业务依赖本地和网络两个数据源,现在有两个新闻数据源,其中LocalNewsDataSource
依赖一个本地 room
数据库,而RemoteNewsDataSource
依赖于一个远程 API 客户端(例如Retrofit
):
然后,向其他层提供新闻列表的 NewsRepository
同时包含上面两个数据源:
在 fectNews()
方法中,首先从远程数据源中获取,如果成功就会更新本地数据源,如果失败或更新过程中出现异常,会打印日志,或者在这里提醒用户。但是无论如何,最终返回的都是从本地数据源中查询的结果,因为它是我们可靠的来源。
这种情况非常简单,因为我们使用的是用户无法修改的数据。但是有些情况下可能会更加复杂一些,例如一个日历应用,如果有两个用户同时修改了其中的某个会议,这时就需要考虑的更多一些以确保用户的体验。
数据同步
总之在存在多个数据源的情况下,情况比较复杂,我们需要在不同数据源之间进行数据同步。
数据同步可以分为三种类型:
存储仓库可能依赖于多个数据源,在某些情况下,你可能希望有多个级别的存储仓库。
例如,用户存储仓库可能需要来自于日志记录存储仓库和注册存储仓库的数据:
同时,一个数据源可能被多个存储仓库共用。这种分层的设计只是一种推荐的做法,在实践中你可以有不同的实现。
关于 Data Model 的核心关键是不可变性(immutable),也就是说其它层不能直接修改 Data Model 的属性,如果其它层想要做出业务数据的修改或更新,它们必须通过 Data Layer 发起请求交给 Repository 去处理。
建议在定义数据模型时,用一个数据模型表达一种业务模式。无论一种业务模式在内部有多少种业务数据模型,对外只暴露一个数据模型。
数据层公开的数据应该确保不可变性,任何类都不能对其进行篡改,篡改数据可能造成数据不一致问题。不可变性的另一个好处是,它可以被多线程安全地处理。实现不可变性的最好办法就是使用 Kotlin 的 data class
:
这里还需要考虑的一点是数据库或远程API返回的实体类模型可能并不是其他层所需要的模型,所以最好是单独创建一个实体模型,确保只提供其他层所需要的那些数据而不是所有数据:
这样不仅代码更简洁而且还能更好地隔离潜在的问题。
同样地,调用数据源和存储仓库也不应该阻塞主线程,对于长时间的获取数据源的方法,存储仓库应该负责将其执行移至其他线程中:
关于在请求数据源时可能发生的异常处理,一种方式是不处理,向外抛出异常,然后在 Domain Layer 或 UI Layer 层中调用处通过常规的 try-catch 捕获异常进行处理:
或者如果是使用的 Kotlin Flow,可以使用 catch 运算符:
另外一种异常处理的方式是在数据层的内部处理,然后以一种更容易理解的方式,向外公开包含成功或者失败的结果数据,但不要忘记错误处理。
应用中会涉及到许多的 Entity 实体类,根据所处的架构层次,可将 Entity 做如下分类:
更多关于 Data Layer 构建范例请参考官方的 Codelab:Building a Data Layer
对应以前 View 系统的状态持久化方式在 Jetpack Compose 中都有相类似的解决方案:
例如,对于以前 View 系统中的onSaveInstanceState
API,现在 Jetpack Compose 中则有对应的 rememberSaveable
API来对应:
对于 Jetpack Compose 中的状态持久化,主要有两种方式:
Activity/Fragment
的 ViewModelStore
中,在Activity因为横竖屏切换等配置变更导致页面销毁重建之后,仍然可以获取到之前的 ViewModel
实例。前面提到过,它实际上是一种全局共享状态,因此 Composable
可以借助 ViewModel
单例来实现状态的持久化。这里可持久化的状态比较适合于那些在 ViewModel
内部创建的公开给 Composable
的 UiState
,而对于 Composable
内创建的状态不太适合通过 ViewModel
持久化保存(虽然也可以通过ViewModel
中的SavedStateHandle
来实现,可参考这里)。remember
的封装容器,可在 Bundle
中存储数据。 remember
缓存的状态可以跨越重组,但不能跨越Activity重建或进程重建。而 rememberSaveable
不仅能让状态在重组后保留下来,还能让状态在重新创建 activity 和系统发起的进程终止后继续留存。它可以像 Activity 的 onSaveInstanceState()
那样在进程被杀死时自动保存状态,同时像 onRestoreInstanceState()
一样随进程重建而自动恢复。ViewModel 在前面也提到了它是官方推荐的正牌状态容器,你可以在其中存储纯 UI 元素相关的状态,也可以存储一些基于业务逻辑(如处理或显示UI元素的行为)相关的 UI 状态,也可以接收来自下层 Data Layer 层读取的数据,并对其进行一些业务逻辑处理之后生成 UI 元素需要的状态。
它的最大特点就是可以跨越配置变更而存活(如横竖屏切换),具体关于ViewModel
的详细使用请参考官方文档或者我之前的文章 - Jetpack架构组件库:Lifecycle、LiveData、ViewModel 中的 ViewModel
部分。这里不再赘述。
SavedStateHandle
其实是ViewModel
的一种高级用法,因为一般用法是在ViewModel
的构造函数中添加一个SavedStateHandle
参数,前面也提到它也可以用于保存Composable
组件内部需要临时存储的UI元素状态,我们可以借助它来存储一些来自UI元素的轻量级的键值对信息。
通常,SavedStateHandle
适合保存的数据都是一些临时状态,根据用户的输入或导航而定。例如:列表的滚动位置、详细页面对应的 Item 的 ID、用户正在进行的偏好设置选择或者正在输入的文本字段等等。
SavedStateHandle
具有用于存储界面元素状态的不同类型 API :
SavedStateHandle
的 saveable
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,则可以通过SavedStateHandle
的 getStateFlow()
来存储界面元素状态,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
才会保存写入其中的数据。
这个就是用来对标以前 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
重建之后,依然能够在Text
和OutlinedTextField
中保持用之前输入的文本(而常规的remember
方式则不行):
下面的代码中,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
,其中包含 LazyColumn
或 LazyRow
的滚动状态。该 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 应用,此问题尤其明显。
Jetpack Compose 中的状态持久化,除了前面提到的 ViewModel
与 rememberSaveable
以外,当然你还可以使用持久化存储,即将数据存储到本地,这样做的好处也很明显:可以跨越任意形式的进程终止而永久存在,超越一切生死。
目前官方推荐的持久化存储方案主要有以下两种:
SharedPreferences
,主要用于存储一些轻量级的简单、较小的数据结构,具体详细使用请参考官方文档或者我之前的文章 - Jetpack架构组件库:DataStore当然,除了以上官方推荐的两种,你也可以采用其他第三方的存储库,如MMKV等,甚至你也可以直接使用文件来存储,只要你喜欢。
注,上表中:
rememberSaveable
(Jetpack Compose) 和 onSaveInstanceState
(View system),以及ViewModel中的SavedStateHandle
。