Jetpack Compose(十)-状态管理

一、什么是状态

大到一个页面的切换,小到一个字符的增删,这些看得见的变化,本质上都是内部数据的变化,这些不断变化的数据就是UI的"状态"。

在传统视图体系中,状态大多以View的成员变量形式存在,例如当想要更新TextView的文字时,通常先要设法获取TextView的实例,然后调用setText方法对文本内容进行更新。随着各处关于TextView的需求增多,相似的代码也会增多,我们也需要一遍遍的编写相似的代码,且随着业务逻辑变复杂setText的逻辑也会变得很复杂。在这里TextView倾向于持有自己的状态,如果没有ViewModel、LiveData等框架的支持,很难写出符合单向数据流架构的代码。

Compose在设计之初就贯彻了单向数据流的设计思想:首先Composable只是一个函数,不会像View那样轻易封装私有状态,状态随处定义的情况得到抑制;其次Compose的状态像LiveData一样能够被观察,当状态变化后,相关联的UI会自动刷新,不需要像传统视图那样命令式地逐个通知。因此即使没有ViewModelLiveData的加持,也能轻松写出符合单向数据流架构的代码。

接下来就看看Compose是如何实现这一切的,首先深入了解一下Compose的"状态"。

二、Stateless与Stateful

传统视图中通过获取组件对象句柄来更新组件状态,而ComposeComposable只是一个函数,且调用后不返回任何实例,那么Composable是如何实现UI刷新的呢?

重新执行@Composable函数来更新UI的过程被称为重组,所以Composable是通过重组来刷新UI的,而重组正是由于Composable的状态变化所触发的。

只依赖函数传参来构建不同UI界面的Composable被称为Stateless Composable(无状态的可组合项)。相对的,有的Composable内部持有或者访问了某些状态,我们称为Stateful Composable(有状态的可组合项)Stateless Composable的重组只能来自上层Composable的调用,而Stateful Composable的重组来自其依赖状态的变化。

Stateless的参数没有变化时不会参与调用方的重组,重组范围局限在Stateless外部,如图所示:

三、状态的定义

看下面的一个简单的例子:

kotlin 复制代码
@Composable
fun Greeting() {
    var countNumber by remember {
        mutableStateOf(0)   //1
    }

    Column {
        Text(text = "当前计数: $countNumber")   //重组刷新计数

        Button(onClick = { countNumber++ }) {  //增加以修改数值
            Text(text = "增加计数")
        }
    }

}

UI效果

上述代码中依赖对countNumber的读写,因此它是一个Stateful ComposablecountNumberInt类型,那是如何修改countNumber来实现状态改变并发生重组的呢?我们先来看看注释1处的mutableStateOf(0),点进去跟踪源码如下:

kotlin 复制代码
fun <T> mutableStateOf(
    value: T,
    policy: SnapshotMutationPolicy<T> = structuralEqualityPolicy()  //快照机制,定义了如何比较特定类型的值 (equivalent) 以及如何解决冲突 (merge)
): MutableState<T> = createSnapshotMutableState(value, policy)
kotlin 复制代码
@Stable
interface MutableState<T> : State<T> {
    override var value: T              //持有值
    operator fun component1(): T
    operator fun component2(): (T) -> Unit
}
kotlin 复制代码
@Stable
interface State<out T> {
    val value: T       //持有值
}

本来想分析一下源码,发现插入在这里太长了,初学阶段还是先讲究实用吧(^o^)/~。

总结就是快照系统会把所有订阅了StateRecomposeScope记录下来,当State的值value发生变化时,会通知所有的RecomposeScope进行重组。

创建MutableState的三种方式:

kotlin 复制代码
//返回T类型的value
var countNumber = remember {
    mutableStateOf(0)
}

//通过解构返回T类型的value以及(T)→Unit类型的set方法
var (countNumber,setCountNumber) = remember {
    mutableStateOf(0)
}

//使用属性代理。
//对countNumber的读写会通过getValue和setValue这两个运算符的↩️
//重写最终代理为对value的操作,通过by关键字,可以像访问一个普通的Int变量一样对状态进行读写。
var countNumber by remember {
    mutableStateOf(0)
}

//委托给了下面二个函数
@Suppress("NOTHING_TO_INLINE")
inline operator fun <T> State<T>.getValue(thisObj: Any?, property: KProperty<*>): T = value

@Suppress("NOTHING_TO_INLINE")
inline operator fun <T> MutableState<T>.setValue(thisObj: Any?, property: KProperty<*>, value: T) {
    this.value = value
}

当使用by代理创建State时,需要额外引入以下扩展方法:

kotlin 复制代码
import androidx.compose.runtime.getValue
import androidx.compose.runtime.setValue

有时IDE无法自动import上面的依赖,如果发现编译报错,可以手动添加依赖。代理的方式在后续使用中最为简单,所以也是实际项目中的首选。

上述三种方式都使用到了remember,那remember的作用是什么? 看下源码:

kotlin 复制代码
@Composable
inline fun <T> remember(crossinline calculation: @DisallowComposableCalls () -> T): T =
    currentComposer.cache(false, calculation)   //对值做缓存
kotlin 复制代码
@ComposeCompilerApi
inline fun <T> Composer.cache(invalid: Boolean, block: @DisallowComposableCalls () -> T): T {
    @Suppress("UNCHECKED_CAST")
    return rememberedValue().let {
        if (invalid || it === Composer.Empty) {
            val value = block()      //对mutableStateOf的调用获取新的值
            updateRememberedValue(value)   //记录新的值做缓存
            value
        } else it
    } as T
}

mutableStateOf创建可变状态变量mutableStateOf 函数用于创建可变状态(mutable state)。它接受一个初始值作为参数,并返回一个包含此值的可变状态变量。可变状态变量的值可以更改,在 Compose 中需要在 Composable 函数内部使用。

remember记录可变状态变量并保证使用时的统一和重建时状态的恢复remember 函数用于将值保留在 Composable 函数内。它可以用来在 Composable 函数的多次调用之间保留不变的值,以避免因组件重组而导致的数据混乱和丢失。remember 函数接受一个 lambda 表达式作为参数,这个 lambda 表达式通常包含对 mutableStateOf 的调用,用于创建和保留一个可变状态的变量。

务必记住,mutabeStateOf的调用一定要出现在remember中,不然每次重组都会创建新的状态。

四、状态的持久化与恢复

前面说到,remember可以缓存创建的状态,避免因为重组而丢失。使用remember缓存的状态虽然可以跨越重组,但是不能跨越Activity或者跨越进程存在。比如当横竖屏等ConfigurationChanged事件发生时,状态会发生丢失。如果想要更长久地保存状态,就需要使用到rememberSavable了,它可以像ActivityonSaveInstanceState那样在进程被杀死时自动保存状态,同时像onRestoreInstanceState一样随进程重建而自动恢复。

rememberSavable中的数据会随onSaveInstanceState进行保存,并在进程或者Activity重建时根据key恢复到对应的Composable中,这个key就是Composable在编译期被确定的唯一标识。因此当用户手动退出应用时,rememberSavable中的数据才会被清空。

rememberSavable实现原理实际上就是将数据以Bundle的形式保存,所以凡是Bundle支持的基本数据类型都可以自动保存。对于一个对象类型,则可以通过添加@Parcelize变为一个Parcelable对象进行保存。比如下面的代码中City就是一个Parcelable类,而MutableState本身也是一个Parcelable对象,因此可以直接保存进rememberSavable

有的数据结构可能无法添加Parcelable接口,比如定义在三方库的类等,此时可以通过自定义Saver为其实现保存和恢复的逻辑。只需要在调用rememberSavable时传入此Saver即可:

kotlin 复制代码
object CitySaver : Saver<City, Bundle> {
    override fun restore(value: Bundle): City? {
        //恢复
        return value.getStrinq("name")?.let { name ->
            value.getString("country")?.let { country ->
                City(name, country)
            }
        }
    }

    override fun SaverScope.save(value: City): Bundle? {
        //存值
        return Bundle().apply {
            putString("name", value.name)
            putString("country", value.country)
        }
    }
}

//使用
@Composable
fun CityScreen() {
    var selectedCity = rememberSavable(stateSaver = CitySaver) {   //传入Saver
        mutableStateof(City("Madrid", "Spain"))
    }
}

除了自定义Saver外,Compose也提供了MapSaverListSaver供开发者使用。

五、使用ViewModel管理状态

前面学习了rememberSavable,它可以在屏幕旋转时甚至进程被杀死时保存状态,理论上可以替代ViewModel的存在。但是一个真实的项目,业务逻辑不会只是对countNumber的加加减减这样简单,往往要复杂得多,如果这些代码都放在Stateful Composable中,会导致UI组件的职责不清,毕竟Composable的主要职责是负责UI的显示。所以当Stateful的业务逻辑变得越发复杂时,可以将Stateful的状态提到ViewModel管理,Stateful也就变为了一个Stateless,通过参数传入不同ViewModel即可替换具体业务逻辑,可复用性和可测试性也大大提高。

Compose中使用ViewModel需要导包,如下:

kotlin 复制代码
 implementation "androidx.lifecycle:lifecycle-viewmodel-compose:2.6.2"

下面是潦草的示例代码

kotlin 复制代码
class MainViewModel : ViewModel() {

    private val _countNumber = mutableStateOf(0)
    //对外暴露不可改变的State
    val countNumber: State<Int> = _countNumber

    fun add() {
        _countNumber.value = _countNumber.value + 1
    }
}

//使用
@Composable
fun Greeting() {
    val mainViewModel: MainViewModel = viewModel()     //注意这里的viewModel()
    CounterComponent(mainViewModel.countNumber,mainViewModel::add)
}

viewModel()是一个@Composable方法,用于在Composable中创建ViewModelGreeting()函数中通过viewModel()创建了一个MainViewModel对象, MainViewModel持有可变状态值countNumber。看下viewModel()的源码如下:

kotlin 复制代码
@Suppress("MissingJvmstatic")
@Composable
public inline fun <reified VM : ViewModel> viewModel(...略...): VM = viewModel(VM::class.java, viewModelStoreOwner, key, factory, extras)

有没有注意到ViewModel中的mutableStateOf并没有被remember{}包裹,这是为什么呢?

答:这是因为在Activity销毁之前,ViewModel将一直存在,viewModel()每次调用将返回同一个实例,所以此时可以不使用remember{}进行缓存。

六、LiveData、RxJava、Flow转State

MVVM架构中,View通过LiveData等观察ViewModel的状态,当LiveData的数据变化时,会以数据流的形式通知View,因此LiveData这类工具也被称为流式数据框架或响应式数据框架。同类框架还有RxJavaFlow等。在Compose中同样的功能由State负责完成,可以将上述这些流式数据转换为ComposableState,当LiveData等数据变化时,可以驱动Composable完成重组。

扩展方法 依赖库
LiveData. observeAsState androidx.compose.runtime: runtime-livedata: $ composeVersion
Flow.collectAsState() 不依赖三方库,Compose自带
Observable.subscribeAsState() androidx.compose.runtime: runtime-rxjava3: $ composeVersion

如果你正打算往项目中引入响应式框架,从包体积以及Compose的兼容性角度考虑,Flow是首选方案,如果是一个Compose first项目,那么推荐在ViewModel中直接使用State

七、状态的分层管理

1、使用Stateful管理状态

简单的UI状态以及配套逻辑适合在Composable中直接管理。

2、使用StateHolder管理状态

StateHolder并不是一个类或函数,而是指使用普通类或者数据类管理状态的一种方式,称为StateHolder(状态容器)

状态会产生逻辑,随着UI状态的增多,UI逻辑也越发复杂,此时可以多个状态连同相关逻辑一起放进专门的StateHolder进行管理。剥离UI逻辑的Composable可以专注UI布局,符合关注点分离的设计原则。

kotlin 复制代码
// Plain class that manages App's UI logic and UI elements' state
class MyAppState(     //StateHolder状态容器
    val scaffoldState: ScaffoldState,
    val navController: NavHostController,
    private val resources: Resources
) {
    val bottomBarTabs = /* State */

    // Logic to decide when to show the bottom bar
    val shouldShowBottomBar: Boolean
        get() = /* ... */

    // Navigation logic, which is a type of UI logic
    fun navigateToBottomBarRoute(route: String) { /* ... */ }

    // Show snackbar using Resources
    fun showSnackbar(message: String) { /* ... */ }
}

@Composable
fun rememberMyAppState(    //定义配套的remember方法
    scaffoldState: ScaffoldState = rememberScaffoldState(),
    navController: NavHostController = rememberNavController(),
    resources: Resources = LocalContext.current.resources
) = remember(scaffoldState, navController, resources) {   //remember函数做可变状态值的缓存,下方展示源码
    MyAppState(scaffoldState, navController, resources)
}

//上面remember(...)的源码为:
@Composable
inline fun <T> remember(
    vararg keys: Any?,
    crossinline calculation: @DisallowComposableCalls () -> T
): T {
    var invalid = false
    for (key in keys) invalid = invalid or currentComposer.changed(key)
    return currentComposer.cache(invalid, calculation)    //缓存
}

随着MyApp的组件变多,SnackbarBottombarNavigation等各种逻辑越来越复杂,相关逻辑代码已经不适合直接写在MyApp中了,此时使用MyAppState这个StateHolder对状态进行统一管理。

由于StateHolder要使用remember保存在Composable中,所以需要为StateHolder定义一个配套的remember方法,便于在Composable中创建和使用。

StateHolder将逻辑抽离后,MyApp只关注UI布局,使职责变得更加清晰:

kotlin 复制代码
@Composable
fun MyApp() {
    MyTheme {
        val myAppState = rememberMyAppState()    //配套的remember方法
        Scaffold(
            scaffoldState = myAppState.scaffoldState,   //使用
            bottomBar = {
                if (myAppState.shouldShowBottomBar) {   //使用
                    BottomBar(
                        tabs = myAppState.bottomBarTabs,   //使用
                        navigateToRoute = {
                            myAppState.navigateToBottomBarRoute(it)   //使用
                        }
                    )
                }
            }
        ) {
            NavHost(navController = myAppState.navController, "initial") { /* ... */ }
        }
    }
}

StateHolder无法像ViewModel那样在横竖屏切换等ConfigurationChanged发生时自动恢复,但是可以通过rememberSavable帮它实现同样的效果。

3、使用ViewModel管理状态

从某种意义上讲,ViewModel只是一种特殊的StateHolder,但因为它保存在ViewModelStore中,所以有以下特点:

  • 存活范围大:可以脱离Composition存在,被所有Composable共享访问。
  • 存活时间长:不会因为横竖屏后者进程被杀死等情况丢失状态。

因此ViewModel适合管理应用级别的全局状态,各Composable可以通过viewModel()获取ViewModel单例达到"全局共享"的效果,而且ViewModel更倾向于管理那些非UI的业务状态 ,因为业务状态中的数据往往需要脱离UI长期保存,否则有可能造成内存泄漏。

kotlin 复制代码
data class ExampleUiState(
    dataToDisplayOnScreen: List<Example> = emptyList(),
    userMessages: List<Message> = emptyList(),
    loading: Boolean = false
)

class ExampleViewModel(     //ViewModel
    private val repository: MyRepository,
    private val savedState: SavedStateHandle
) : ViewModel() {

    var uiState by mutableStateOf<ExampleUiState>(...)
        private set   //私有化set函数,防止外界修改uiState

    // Business logic
    fun somethingRelatedToBusinessLogic() { ... }
}

@Composable
fun ExampleScreen(viewModel: ExampleViewModel = viewModel()) {   //ViewModel作为参数

    val uiState = viewModel.uiState   //使用ViewModel中的uiState
    ...

    Button(onClick = { viewModel.somethingRelatedToBusinessLogic() }) {
        Text("Do something")
    }
}

在上面的代码中,ExampleUiState中包含了userMessages这样的领域层数据,以及loading这样的代表数据加载状态的数据,这些都与UI无关,适合用ViewModel进行管理。此外,如果 ViewModel 中包含要在进程重建后保留的状态,请使用SavedStateHandleViewModel通过SavedStateHandler实现uiState的持久化保存。

先导包

kotlin 复制代码
implementation "androidx.lifecycle:lifecycle-viewmodel-savedstate:2.6.2"

下面是一个例子

kotlin 复制代码
class MainViewModel(private val savedStateHandle: SavedStateHandle) : ViewModel() {

    companion object {
        private const val KEY_COUNT = "count"
    }

    fun getCount(): Int {
        return savedStateHandle.get(KEY_COUNT) ?: 0
    }

    fun incrementCount() {
        val count = getCount()
        savedStateHandle.set(KEY_COUNT, count + 1)
    }
}

//使用
val mainViewModel:MainViewModel = viewModel()
val count = mainViewModel.getCount()

ViewModel的另一个优势是支持Hilt依赖注入,尤其是当业务逻辑依赖Repository等数据层对象时,通过配合hilt-navigation-compose组件库的使用,可以为每个页面的ViewModel实例自动注入所需的依赖。

ViewModelStateHolder也可以同时使用,两者各司其职。StateHolder可以用来管理UI相关的状态和逻辑,ViewModel可以用来管理与UI无关的状态和逻辑。下面的代码是一个StateHolderViewModel并存的例子:

kotlin 复制代码
private class ExampleState(    //UI状态
    val lazyListState: LazylistState,
    private valresources: Resources,
    private val expandedItems: List<Item> = emptyList()
) { ... }

@Composable
private fun rememberExampleState(...) {...}   //配套的remember方法

@Composable
fun ExampleScreen(viewModel: ExampleViewModel = viewModel()) {
    val uiState = viewModel.uiState               //ViewModel
    val exampleState = rememberExampleState()     //StateHolder 
    LazyColumn(state = exampleState.lazyListState) {
        items(uiState.dataToDisplayOnScreen) { item ->
            if (exampleState.isExpandedItem(item) {
                    ...
                }
            ...
        }
    }
}

4、状态分层管理的最佳实践

因为ViewModelViewModelStoreOwner的范围内只有唯一实例,所以更适合存储全局唯一状态,当State需要多实例 存在时,建议使用StateHolder进行管理。总的来说,在Compose中应该根据状态和逻辑的复杂度以及业务类型,选择不同的状态管理方式。在一些复杂场景中,多种管理方式也可能并存,如图所示:

Composable主攻UI的布局,可以持有少量UI状态。当UI逻辑较多时,可以依赖StateHolder管理,Composable同时依赖多个StateHolder负责不同的UI逻辑,StateHolderComposable都保存在Composable视图树上,所以所辖状态的生命周期与所处的Composable一致。

Composable或者StateHolder可以依赖ViewModel管理UI无关的状态及对应的业务逻辑。借助ViewModel,这些状态可以跨越Composable甚至Activity的生命周期长期存在。ViewModel依赖处于更底层的领域层或者数据层完成相关业务,从上图中可知道,处于底层的业务服务范围往往更广,存活时间也更长。

参考了以下内容:

本文大部分内容参考了实体书 Jetpack Compose从入门到实战

其他参考内容:

Jetpack Compose docs

官网Sate

初学者如有错误欢迎批评指正!

相关推荐
服装学院的IT男12 分钟前
【Android 13源码分析】Activity生命周期之onCreate,onStart,onResume-2
android
Arms20615 分钟前
android 全面屏最底部栏沉浸式
android
服装学院的IT男17 分钟前
【Android 源码分析】Activity生命周期之onStop-1
android
ChinaDragonDreamer3 小时前
Kotlin:2.0.20 的新特性
android·开发语言·kotlin
网络研究院5 小时前
Android 安卓内存安全漏洞数量大幅下降的原因
android·安全·编程·安卓·内存·漏洞·技术
凉亭下5 小时前
android navigation 用法详细使用
android
小比卡丘8 小时前
C语言进阶版第17课—自定义类型:联合和枚举
android·java·c语言
前行的小黑炭9 小时前
一篇搞定Android 实现扫码支付:如何对接海外的第三方支付;项目中的真实经验分享;如何高效对接,高效开发
android
落落落sss10 小时前
MybatisPlus
android·java·开发语言·spring·tomcat·rabbitmq·mybatis
代码敲上天.11 小时前
数据库语句优化
android·数据库·adb