Compose - 状态 State

文章详细介绍了JetpackCompose中如何管理状态,包括使用MutableState进行状态观察,利用remember()保持数据持久化,以及状态提升(StateHoisting)来优化重组性能。此外,还提到了其他状态存储方式如rememberSaveable(),以及如何通过Lambda参数减少不必要的重组。文章最后讨论了ViewModel在状态管理中的角色和最佳实践。
摘要由CSDN通过智能技术生成

参考文章1

参考文章2

一、概念

        Compose将界面(@Composable组合函数)和数据(State状态)分开,事件(Event)会引起状态的变化,进而使用了该状态的组合会发生重组。

        MutableState会与引用它的组合函数绑定,当所持有的状态(值)发生变化时通知组合函数进行界面刷新(重组)。

        remember( )提供持久化存储功能,因为使用了纯函数形式表达界面,如果在函数中声明属性(局部变量),会因为函数被多次调用而丢失状态(值)。

二、MutableState

        Compose专用的观察者模式的容器(同LiveData、StateFlow、Flow、Observable一样可以观察到值的变化)。MutableState会与引用它的组合函数绑定,当所持有的状态(值)发生变化时通知组合函数进行界面刷新(重组)。

  • 状态若是一个可变对象(例如MutableList),更改 MutableList 中的 element 不会触发重组,需要的是对象(MutableList)的改变,这是对象内部数据(element)的改变。
引用数据类型fun <T> mutableStateOf(
    value: T,        //默认值
    policy: SnapshotMutationPolicy<T> = structuralEqualityPolicy()
): MutableState<T>
ListmutableStateListOf( )
MapmutableStateMapOf( ) 
Number

mutableIntStateOf( )
mutableLongStateOf( )
mutableDoubleStateOf( )
mutableFloatStateOf( )

避免开装箱产生的性能消耗。

ViewModel {
    //方式一:使用赋值运算符获取的是MutableState对象,需要.value获取值再进行操作
    private val _strState = mutableStateOf<String>("Hello Word!")
    val strState = _strState as State<String>
    //方式二:使用属性委托直接通过变量名对value进行读写(推荐)
     var strState2 by mutableStateOf("")
        private set
}

@Composable
fun Input(
    viewModel:MainViewModel = viewModel()
) {
    Text(text = viewModel.strState.value)    //方式一
    Text(text = viewModel.strState2)    //方式二(推荐)
}

三、remember( )

        因为使用了纯函数形式表达界面,如果在函数中声明局部变量,会因为函数被多次调用(重组)而丢失状态(值)。

        remreber( )可以为组合函数提供数据持久化的功能,remember( )会记住所修饰的值(存储在UI树中),这样在重组后状态不会被初始化(在有添加或移除元素、重组发生在父元素中、屏幕旋转、恢复前台显示的情况下,使用 remember 会造成数据丢失变成初始值,见下方其它方式存储)。

        在组合函数中创建的 MutableState 需要对其进行 remember 操作记住该值,否则每次重组都会初始化成默认值丢失数据,只有在调用方不需要管理状态的时候使用。由于内部创建了状态,难以复用和测试,见下方状态提升。

inline fun <T> remember(crossinline calculation: @DisallowComposableCalls () -> T): T

记住Lambda中生成的值(参数不带key,默认key无变化不会重置为始化值)。

inline fun <T> remember(key: Any?,crossinline calculation: @DisallowComposableCalls () -> T): T

当key值变化时,就会执行Lambda重新生成值。key值默认不会自己变化,会返回上一次生成的值。

//记录点击多少次
@Composable
fun Demo() {
    //方式一:使用赋值运算符拿到的是remember()所修饰的MutableState对象。
    val count = remember { mutableStateOf(0) }
    Button(onClick = { count.value++ }) {
        Text(text = "点击了 ${count.value} 次")
    }
    //方式二:属性委托直接操作变量名来读写value。
    //需要导包androidx.compose.runtime.getValue和setValue。
    var count by remember { mutableStateOf(0) }     //记录点击多少次
    Button(onClick = { count++ }) {
        Text(text = "点击了 $count 次")
    }
    //方式三:解构的是某个实现类,value是显示的值,setValue是如何处理新值(事件的处理)。
    val (value, setValue) = remember { mutableStateOf("默认值") }
    TextField(value = value, onValueChange = setValue)
    //通过key控制
    val num by remember { mutableStateOf(0) }
    val str = remember(key1 = num) { "数字是$num" }
}

四、状态提升 State Hoisting

        根据组合项中是否有 State 可分为有状态组合项(Stateful)和无状态组合项(Stateless)。有状态组合项复用性不高难做测试 。

        有状态组合变为无状态组合需要状态提升,即将状态(数据源和事件) 移出到组合外给调用方处理,具体是将 State 替换为两个形参:要显示的数据(value),对事件的操作 (T) -> Unit,T是新值。状态提升事件下降即单向数据流设计。

  • 状态应至少提升到使用该状态(读取)的所有Composable的最低共同父项。
  • 状态应至少提升到它可以发生变化(写入)的最高级别。
  • 如果两种状态发生变化以响应相同的事件,它们应该一直提升。
//有状态
@Composable
fun InputWithState() {
    val str = remember { mutableStateOf("Hello Word!") }
    Column {
        Text(text = str.value)
        TextField(value = str.value, onValueChange = { str.value = it })
    }
}
//状态提升(中间层或底层不必传入ViewModel)
@Composable
fun InputWithoutState(value: String, onValueChange: (String) -> Unit) {
    Column {
        Text(text = str)
        TextField(value = value, onValueChange = onValueChange)
    }
}
//ViewModel中处理
var strState = mutableStateOf("Hello Word!")
fun onStrChange(newValue: String) {
    strState.value = newValue
}
//Activity中各部分全部组合起来(只在顶层组合传入ViewModel)
@Composable
fun MainActivityScreen(viewMidel:MyViewModel) {
    InputWithoutState(viewMidel.strState, viewMidel.onStrChange)
}

五、其它状态存储方式

5.1 rememberSaveable( )

与 remember 类似,用于 Activity 或进程重建时恢复界面状态,类似于 onSaveInstanceState( ),任何可以存储在 Bundle 中的数据都可以通过 rememberSaveable( ) 进行存储。

5.2 @Parcelize

直接对数据类使用该注解,类似于 Java 中的 Serializable 都是将实例编码成字节流存储,但不会产生大量临时对象、没有反射效率更高,也不用写 Parcelable 模板代码。如果不涉及到本地化存储或者网络传输推荐使用。

// 第一步:添加插件
plugins {
    id 'kotlin-parcelize'    
}
//第二步:添加注解及Parcelable
@Parcelize    
data class City(val name: String, val country: String) : Parcelable
//第三步:保存到状态中
val cityBean = rememberSaveable{ mutableStateOf(City("0112","西京"))}

5.3 MapSaver( )

不适合用Parcelize的场景可以使用,定义自己的存储和回复规则,规定如何把实例转为可保存到 Bundle 中的值。通过 save 这个 lambda 可以将 Book 对象转化为一个 Map 进行存储;要使用的时候就通过 restore 这个 lambda 将 Map 又恢复为一个 Book 对象。

data class Book(val name: String, val author: String)

val BookSaver = run {
    val nameKey = "Name"
    val authorKey = "Author"
    mapSaver(
        save = { mapOf(nameKey to it.name, authorKey to it.author) },
        restore = { Book(it[nameKey] as String, it[authorKey] as String) }
    )
}

val chosenBook = rememberSaveable( stateSaver = BookSaver ) {
	mutableStateOf(Book("三体","刘慈欣"))
}

5.4 ListSaver( )

List相对于上面的Map不用定义key。

val BookListSaver = listSaver<Book, Any>(
    save = { listOf(it.name, it.author) },
    restore = { Book(it[0] as String, it[1] as String) }
)

val chosenBook = rememberSaveable( stateSaver = BookSaver ) {
	mutableStateOf(Book("三体","刘慈欣"))
}

六、重组性能优化

6.1 使用父组合项中高频变化的状态时采用Lambda传值

        由于 Column、Row、Box等是内联函数,编译后不是一个函数,如果内部有读取状态的行为,实际是外层在读取和重组,因此会引发不必要的外层重组。不要在父组合项中读取状态后再传递值给子组合项,而是子组合项的参数类型使用 Lambda,让子组合项读取。简而言之就是尽可能将读取状态的行为延后。

        在Compose自带的关于偏移、可见度、大小变化的 API 中都有一个 Lambda 版本,效率更高因为可以跳过重组的过程只重新绘制或布局。

@Preview(showBackground = true)
@Composable
fun DemoPre() {
    Column(modifier = Modifier.background(Color.White)) {
        Demo1()
        Demo2()
    }
}

class DemoViewModel : ViewModel() {
    var aState by mutableStateOf("A:默认")      //A使用属性委托
    val bState = mutableStateOf("B:默认")       //B使用赋值运算符
    var c1State by mutableStateOf("C1:默认")    //C1使用属性委托
    val c2State = mutableStateOf("C2:默认")     //C2使用赋值运算符

    fun updateA() { aState = "A:已更新" }
    fun updateB() { bState.value = "B:已更新" }
    fun updateC1() { c1State = "C1:已更新" }
    fun updateC2() { c2State.value = "C2:已更新" }
}

//使用外部传入的状态
@Composable
fun Demo1(
    viewModel: DemoViewModel = viewModel()
) {
    SideEffect { Log.e("发生重组", "Demo") }
    Column {//内联函数,内部读取状态的话,实际是Demo在读取状态,状态变化Demo就会重组
        //aState是属性委托方式,调用变量名直接就是读取状态,因此会引发Demo重组
        TestA(str = viewModel.aState)
        //bState是赋值运算符方式,调用变量名获取的是MutableState对象,不会引发Demo重组
        TestB(str = viewModel.bState)
        //通过Lambda传参,两种方式都不会引起Demo重组
        TestC { viewModel.c1State } //属性委托
        TestC { viewModel.c2State.value }   //赋值运算符
        //值写死,由于没有读取状态,不会有任何重组
        TestD(str = "D的值写死")

        Button(onClick = {viewModel.updateA()}) { Text(text = "更新A状态") }
        Button(onClick = {viewModel.updateB()}) { Text(text = "更新B状态") }
        Button(onClick = {viewModel.updateC1()}) { Text(text = "更新C状态-委托模式") }
        Button(onClick = {viewModel.updateC2()}) { Text(text = "更新C状态-赋值运算符") }
    }
}

//使用内部持有的状态(注释同上,效果一样)
@Composable
fun Demo2() {
    var aState by remember { mutableStateOf("A:默认") }
    val bState = remember { mutableStateOf("B:默认") }
    var c1State by remember { mutableStateOf("C1:默认") }
    val c2State = remember { mutableStateOf("C2:默认") }

    SideEffect { Log.e("发生重组", "Demo") }

    Column {
        TestA(str = aState)
        TestB(str = bState)
        TestC {c1State}
        TestC {c2State.value}
        TestD(str = "D的值写死")

        Button(onClick = { aState = "A:已更新" }) { Text(text = "更新A状态") }
        Button(onClick = { bState.value  = "B:已更新" }) { Text(text = "更新B状态") }
        Button(onClick = { c1State = "C1:已更新" }) { Text(text = "更新C状态-委托模式") }
        Button(onClick = { c2State.value  = "C2:已更新" }) { Text(text = "更新C状态-赋值运算符") }
    }
}

@Composable
fun TestA(str: String) {
    SideEffect { Log.e("发生重组", "A") }
    Text(text = str)
}

@Composable
fun TestB(str: MutableState<String>) {
    SideEffect { Log.e("发生重组", "B") }
    Text(text = str.value)  //这里调用.value才是读取状态
}

@Composable
fun TestC(str:() -> String) {
    SideEffect { Log.e("发生重组", "C") }
    Text(text = str())    //这里才是Lambda实际调用的地方(状态读取)
}

@Composable
fun TestD(str: String) {
    SideEffect { Log.e("发生重组", "D") }
    Text(text = str)
}

6.2 使用派生状态来降低重组次数 derivedStateOf( )

一个状态基于另一个或多个状态得出,即对条件状态经过计算后得出结果状态。可对条件状态进行过滤,避免每次条件状态更新都要连带自己重组。通常使用 remember 的 key 可以实现,有些情况的状态无法用作key,例如元素改变了而 List 没变。

举例一:有一个变化频率非常高的数值,我们只关心正负,正为绿色负为红色。优化前每次数值变化都会导致重组(例如从0.2变成0.3颜色是不变的却进行了重组),优化后只根据正负变化重组。

@Composable
fun Demo() {
    var value by remember{ mutableStateOf(0F) }
    //监听value的变化派生出isPositive
    val isPositive by remember { derivedStateOf { value >= 0 } }

    SideEffect { Log.e("发生重组", "Demo") }

    Column {
        Row {
            Button(onClick = { value += 0.1F }) { Text(text = "点击+0.1") }
            Button(onClick = { value -= 0.1F }) { Text(text = "点击-0.1") }
        }
        //这里如果是以 value>=0 为条件,每次数值变化都会引起Demo重组
        //将条件换成 isPositive 只有正负变化才会引起Demo重组
        Box(modifier = Modifier
            .size(50.dp)
            .background(if(isPositive) Color.Green else Color.Red)  //正数显示绿色,负数显示红色
        ){
            SideEffect { Log.e("发生重组", "Box") }
        }
    }
}

七、状态转换

7.1 LiveData、Flow、RxJava → State。

LiveDataval liveDataState = liveData.observeAsState()
Flow

val flowState = flow.collectAsState("str")

val flowState = flow.collectAsStateWithLifecycle("str")

//仅适用于Android,处于后台不收集

//需要导包 implementation 'androidx.lifecycle:lifecycle-runtime-compose:2.6.1'

implementation "androidx.compose.runtime:runtime-livedata:$compose_version"
implementation "androidx.compose.runtime:runtime-rxjava2:$compose_version"
implementation "androidx.compose.runtime:runtime-flow:$compose_version"
@Composable
fun Demo(
    viewModel:DemoViewModel = viewModel()
) {
    //LiveData
    val dataState: State<String?> = viewModel.data.observeAsState()
    Text("Username: ${dataState}")
    //Flow
    val uiState by viewModel.user.collectAsState(ViewState.Loading)
    //RxJava
    val uiState by viewModel.user.subscribeAsState(ViewState.Loading)

    when (uiState) {
        ViewState.Loading -> TODO("Show loading")
        ViewState.Error -> TODO("Show Snackbar")
        is ViewState.Content -> TODO("Show content")
    }
}

class DemoViewModel : ViewModel() {
    //LiveData
    private val _data = MutableLiveData("")
    val data = _data as LiveData<String>
    //Flow
//    val user: Flow<ViewState> = flowOf(ViewState.Loading)    //冷流无意义(后期无法发送值)
    val user: MutableStateFlow<ViewState> = MutableStateFlow(ViewState.Loading)
    //RxJava
    val user: Observable<ViewState> = Observable.just(ViewState.Loading)
}
val countDownTimer = flow {
    repeat(5) {
        emit(it)
        delay(1000)
    }
}

@Composable
fun Test() {
    //显示5 4 3 2 1
    Text("倒计时:${countDownTimer.collectAsState(initial = 10).value}")
}

7.2 协程 → State(produceState)、State → Flow(snapshotFlow)

详见附带效应

八、状态持有者

考虑将状态保存在何处的因素有:界面状态(数据还是UI)、逻辑(业务还是UI)

组合项如果状态数量较少和逻辑比较简单,在组合项中直接增加逻辑和状态是可以的,与其相关的交互都应该在这个组合项进行。但是如果将它传递给其它组合项,就不符合单一可信来源,而且会使调试更困难。
状态容器当组合项涉及多个界面的状态等复杂逻辑时,应将相应事务委派给状态容器。这样更易于单独对该逻辑进行测试,还降低了组合的复杂性。保证组合项只是负责展示,而状态容器负责逻辑和状态。
ViewModelViewModel 的生命周期往往是比较长的,原因是它们在配置发生变化后仍然有效。ViewModel 可以遵循 Activity、Fragment、或导航(如果使用了导航库)的生命周期。正因为 ViewModel 的生命周期较长,因此不应该长期持有和组合密切相关的一些状态,否则,可以会导致内存泄漏。如果 ViewModel 中包含要在进程重建后保留的状态,使用SavedStateHandle。

8.1 组合 作为可信来源

val scaffoldState = rememberScaffoldState()
val coroutineScope = rememberCoroutineScope()
Scaffold(scaffoldState = scaffoldState) {
    MyContent(
        showSnackbar = { message ->
            coroutineScope.launch { scaffoldState.snackbarHostState.showSnackbar(message) }
        }
    )
}

8.2 状态容器 作为可信来源

class MyAppState(
    val scaffoldState: ScaffoldState,
    val navController: NavHostController,
    private val resources: Resources
) {
    val bottomBarTabs = /* State */
    val shouldShowBottomBar: Boolean
        get() {
            //何时显示底栏的逻辑
        }
    fun navigateToBottomBarRoute(route: String) {
        //导航逻辑
    }
    fun showSnackbar(message: String) {
        //使用资源显示 snackbar
    }
}

//使用MyAppState 的时候需要使用remember来进行信赖
//可以创建一个rememberMyAppState方法来直接返回MyAppState实例。
@Composable
fun rememberMyAppState(
    scaffoldState: ScaffoldState = rememberScaffoldState(),
    navController: NavHostController = rememberNavController(),
    resources: Resources = LocalContext.current.resources,
) = remember(scaffoldState, navController, resources, /* ... */) {
    MyAppState(scaffoldState, navController, resources, /* ... */)
}

//使用
val myAppState = rememberMyAppState()
Scaffold(
    scaffoldState = myAppState.scaffoldState,
    bottomBar = {
        if (myAppState.shouldShowBottomBar) {
            BottomBar(
                tabs = myAppState.bottomBarTabs,
                navigateToRoute = { myAppState.navigateToBottomBarRoute(it) }
            )
        }
    }
) {
    NavHost(navController = myAppState.navController, "initial") { /* ... */ }
}

8.3 ViewModel 作为可信来源

在函数式编程一大特点就是「不可变性」,即在函数式编程中,状态不是通过修改变量,而是通过创建和返回新的状态来更新的。这意味着数据是不可变的(immutable),一旦创建,就不能更改。通过这种不可变性尽可能的避免副作用。

class LoginVM : ViewModel() {
    var uiState by mutableStateOf(LoginUiState())
        private set
    private fun toLogin() {
        uiState = uiState.copy(isFirstRefresh = false)    //修改状态
    }
}

data class LoginUiState(
    val datas: List<VisitorBean> = emptyList(),    //为状态提供默认值
    val isFirstRefresh: Boolean = true
)
@Composable
fun LoginScreen(
    viewModel: LoginVM = viewModel()
) {
    if(viewModel.isFirstRefresh) {...}    //读取状态
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值