Android Compose UI状态管理之生命周期(一)

一、前言

借用官方的回答 由于 Compose 是声明式工具集,因此更新它的唯一方法是通过新参数调用同一可组合项。这些参数是界面状态的表现形式。每当状态更新时,都会发生重组。 不会像在基于 XML 的命令式视图中那样自动更新。可组合项必须明确获知新状态,才能相应地进行更新,而获取新状态使用的就是State

1.1、传统思维

什么意思呢可以看一段代码: 可以测试到clickable更改value的值变化,文本的值并不会发生改变,虽然状态变化但是没有发生重组。要想根据状态发生重组可以使用MutableState API 将对象存储在内存中。系统会在初始组合期间将由 remember 包裹记忆更改的MutableState值,并在重组期间返回存储的值。remember 既可用于存储可变对象,又可用于存储不可变对象。

kotlin 复制代码
@Composable
private fun testValueChange() {
    var value=1
    Text(text = "测试传统值改变:$value", modifier = Modifier
        .padding(vertical = 6.dp)
        .background(Color.Blue)
        .padding(6.dp)
        .clickable {
            value ++
        })
}

1.2、使用可组合项更新

如果 countRemember 有任何变化,系统就会为用于读取 countRemember 的所有可组合函数安排重组。

在可组合项中声明 MutableState 对象的方法有三种:

  • val mutableState = remember { mutableStateOf(default) }
  • var value by remember { mutableStateOf(default) }
  • val (value, setValue) = remember { mutableStateOf(default) }

这些声明是等效的,以语法糖的形式针对状态的不同用法提供。

kotlin 复制代码
@Composable
private fun testRemember() {
    var countRemember = remember { mutableStateOf(0) }
    Log.d("tgw", "testRemember: ")
    Text(text = "测试remember的更新方式--${countRemember.value}", modifier = Modifier
        .padding(vertical = 6.dp)
        .background(Color.Blue)
        .padding(6.dp)
        .clickable {
            countRemember.value = countRemember.value + 1
        })
 }

注意下面两点问题,后续将给出原因与解决方法

  • remember 会将对象存储在组合中,当调用 remember 的可组合项从组合中移除后,它会忘记该对象组合发生移除重组后,会造成值丢失或不正确等一些不可控的情况出现,后面重点要讲的。
  • 虽然 remember 可帮助您在重组后保持状态,但不会帮助您在配置更改后保持状态。为此,您必须使用 rememberSaveablerememberSaveable 会自动保存可保存在 Bundle 中的任何值。对于其他值,您可以将其传入自定义 Saver 对象。

二、remember使用不当造成的状态忘记

2.1、什么叫状态忘记

状态忘记估计初次见到也会一脸懵逼,看一个例子解释一下。

1.先关注 testRemember方法观察现象,testLiveData是对于问题的解决。 我们一般很容易就写出这样子的代码,写了一个Column然后根据一些条件动态的显示(这里是用了一个点击事件让当前的GreetingMain函数发生重组)布局A(testRemember),或布局B(testLiveData)

2.如果单纯的看布局A或B的显示隐藏当然没问题,可一旦(布局A)testRemember函数中使用了一些状态记录(如:countRemember更改它的值,使用Text进行显示)这样的话,GreetingMain函数的重组就会导致,布局A从显示到移除再到显示之后(布局A)testRemember函数中之前修改的countRemember值丢失掉如下图:测试remember的更新方式更改的值,在重组后丢失。

scss 复制代码
@Composable
fun GreetingMain(name: String) {
    var parentUpdate = remember { mutableStateOf(0) }
    Column {
        Log.d("tgw", "GreetingMain:开始测试")
        if (parentUpdate.value == 0) {
            testRemember()
            testLiveData()
        }else if (parentUpdate.value % 2 == 0){
            testLiveData(parentUpdate.value) //布局B
        }else if (parentUpdate.value % 2 != 0){
            testRemember() //布局A
        }

        Button(onClick = {
            //更改条件值
            parentUpdate.value= parentUpdate.value + 1
        }) {
            Text(
                text = "点击开始测试父类重组${parentUpdate.value}", modifier = Modifier
                    .padding(vertical = 6.dp)
                    .background(Color.Blue)
                    .padding(6.dp)
            )
        }
    }
}

布局A:

kotlin 复制代码
@Composable
private fun testRemember() {
    var countRemember = remember { mutableStateOf(0) }
    Log.d("tgw", "testRemember: ")
    Text(text = "测试remember的更新方式--${countRemember.value}", modifier = Modifier
        .padding(vertical = 6.dp)
        .background(Color.Blue)
        .padding(6.dp)
        .clickable {
            countRemember.value = countRemember.value + 1
        })
 }

2.2、如何解决状态丢失

  1. :使用rememberSaveable进行初始化
  2. 在ViewModel remember 初始化状态

其实主要就是延长所需要保存的值,使其生命周期较长

使用后效果如下图:

  • rememberSaveable进行初始化如下
kotlin 复制代码
@Composable
private fun testRemember() {
    var countRemember = rememberSaveable { mutableStateOf(0) }
    Log.d("tgw", "testRemember: ")
    Text(text = "测试remember的更新方式--${countRemember.value}", modifier = Modifier
        .padding(vertical = 6.dp)
        .background(Color.Blue)
        .padding(6.dp)
        .clickable {
            countRemember.value = countRemember.value + 1
        })
 }
  • 在ViewModel 中使用observeAsState的状态在重组后不丢失,它的状态不丢失最关键的是值的初始化在另一个类里面如:viewmodel(状态所有者,管理保存数据状态),意味着在viewmodel中使用 var parentUpdate = remember { mutableStateOf(0) }也可以防止数据状态丢失。

要想使用的LiveData的observeAsState得加入依赖

scss 复制代码
dependencies {
      ...
      implementation("androidx.compose.runtime:runtime-livedata:1.3.2")
}

创建viewmodel

kotlin 复制代码
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.snapshotFlow
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.launch

/**
 *@Author tgw
 *@Date 2023/07/06
 *@describe
 */
class MainViewModel:ViewModel() {
    val count = MutableLiveData<Int>(0)
    fun add() {
        count.value = count.value?.plus(1)
    }
}
kotlin 复制代码
//创建一个viewModel
private val mainViewModel : MainViewModel by lazy {         ViewModelProvider(this).get(MainViewModel::class.java) }

@Composable
private fun testLiveData() {
    var value=1
    var countLiveData = mainViewModel.count.observeAsState()
    Log.d("tgw", "testLiveData: ")
    Text(text = "测试LiveData的更新方式传统值:$value--${countLiveData.value}", modifier = Modifier
        .padding(vertical = 6.dp)
        .background(Color.Blue)
        .padding(6.dp)
        .clickable {
            mainViewModel.add()
            value = value ++
        })
    Log.d("tgw", "testLiveData:LocalLifecycleOwner current ${LocalLifecycleOwner.current}")
    Log.d("tgw", "testLiveData:countLiveData ${countLiveData}")

    LaunchedEffect(Unit){
        Log.d("tgw", "testLiveData:LaunchedEffect ")
    }

    val lifecycleOwner = LocalLifecycleOwner.current


    DisposableEffect(Unit ){
        Log.d("tgw", "testLiveData:DisposableEffect ")
        val observer = LifecycleEventObserver { source, event ->
            when (event) { //根据Event执行不同生命周期的操作
                Lifecycle.Event.ON_CREATE -> {
                    Log.d("tgw", "testLiveData:DisposableEffect ON_CREATE")

                }
                Lifecycle.Event.ON_RESUME -> {
                    Log.d("tgw", "testLiveData:DisposableEffect ON_RESUME")

                }
                Lifecycle.Event.ON_PAUSE -> {
                    Log.d("tgw", "testLiveData:DisposableEffect ON_PAUSE")

                }
                Lifecycle.Event.ON_DESTROY -> {
                    Log.d("tgw", "testLiveData:DisposableEffect ON_DESTROY")

                }
                else -> {}
            }
        }
        lifecycleOwner.lifecycle.addObserver(observer)

        onDispose{
            Log.d("tgw", "testLiveData:DisposableEffect onDispose ")
            lifecycleOwner.lifecycle.removeObserver(observer)

        }
    }
}

当调用 remember 的可组合项从组合中移除后,它会忘记该对象,可以认为重新添加的话将是一个新的对象,我们可以从函数的生命周期观测到。

除了Livedata与remember,当然还有flow相关的 使用flow.collectAsState方法转化为可观察状态的对象,相当于在viewmodel中初始化了flow在compose中使用collectAsState方法转化。 而关于flow的使用可以看我的另一篇文章: Kotlin中 Flow、SharedFlow与StateFlow区别

  1. snapshotFlow(compose独有)
  2. MutableStateFlow 侧重状态---mainViewModel.sharedFlow.collectAsState(initial = 1)
  3. MutableSharedFlow 侧重事件---mainViewModel.stateFlow.collectAsState(initial = 1)

2.3、解析LiveData的observeAsState,与flow的collectAsState

为什么observeAsState,与flow的collectAsState能够根据数据变化更新状态,跟一下源码可知晓:

1.LiveData的observeAsState,内部也是取了livedata的值构建了一个remember对象,然后利用DisposableEffect对数据进行观察修改

kotlin 复制代码
@Composable
fun <T> LiveData<T>.observeAsState(): State<T?> = observeAsState(value)
kotlin 复制代码
@Composable
fun <R, T : R> LiveData<T>.observeAsState(initial: R): State<R> {
    val lifecycleOwner = LocalLifecycleOwner.current
    val state = remember { mutableStateOf(initial) }
    DisposableEffect(this, lifecycleOwner) {
        val observer = Observer<T> { state.value = it }
        observe(lifecycleOwner, observer)
        onDispose { removeObserver(observer) }
    }
    return state
}

2.flow的collectAsState

less 复制代码
@Suppress("StateFlowValueCalledInComposition")
@Composable
fun <T> StateFlow<T>.collectAsState(
    context: CoroutineContext = EmptyCoroutineContext
): State<T> = collectAsState(value, context)
kotlin 复制代码
@Composable
fun <T : R, R> Flow<T>.collectAsState(
    initial: R,
    context: CoroutineContext = EmptyCoroutineContext
): State<R> = produceState(initial, this, context) {
    if (context == EmptyCoroutineContext) {
        collect { value = it }
    } else withContext(context) {
        collect { value = it }
    }
}

关注produceState方法(生产状态)

kotlin 复制代码
@Composable
fun <T> produceState(
    initialValue: T,
    key1: Any?,
    key2: Any?,
    @BuilderInference producer: suspend ProduceStateScope<T>.() -> Unit
): State<T> {
    val result = remember { mutableStateOf(initialValue) }
    LaunchedEffect(key1, key2) {
        ProduceStateScopeImpl(result, coroutineContext).producer()
    }
    return result
}

结论:compose其实都是通过remember进行数据状态更改

三、观测compose函数的生命周期

3.1、DisposableEffect

要观测生命周期可在函数中添加DisposableEffect使用,在函数从组合项中移除的时候会回调onDispose方法,LocalLifecycleOwner.current的话就是为当前函数绑定activity的周期。点击重组按钮testLiveDatatestRemember一个添加一个移除刚好符合下图日志 而LaunchedEffect会开启一个协成,他自动与当前函数的生命周期绑定。他俩都可以接受参数,只要参数发生了变化就会再次触发执行

kotlin 复制代码
@Composable
private fun testLiveData() {
    var value=1
    var countLiveData = mainViewModel.count.observeAsState()
    Log.d("tgw", "testLiveData: ")
    Text(text = "测试LiveData的更新方式传统值:$value--${countLiveData.value}", modifier = Modifier
        .padding(vertical = 6.dp)
        .background(Color.Blue)
        .padding(6.dp)
        .clickable {
            mainViewModel.add()
            value = value ++
        })
    Log.d("tgw", "testLiveData:LocalLifecycleOwner current ${LocalLifecycleOwner.current}")
    Log.d("tgw", "testLiveData:countLiveData ${countLiveData}")

    LaunchedEffect(Unit){
        delay(3000)
        Log.d("tgw", "testLiveData:LaunchedEffect ")
    }

    val lifecycleOwner = LocalLifecycleOwner.current
    DisposableEffect(Unit ){
        Log.d("tgw", "testLiveData:DisposableEffect ")
        val observer = LifecycleEventObserver { source, event ->
            when (event) { //根据Event执行不同生命周期的操作
                Lifecycle.Event.ON_CREATE -> {
                    Log.d("tgw", "testLiveData:DisposableEffect ON_CREATE")

                }
                Lifecycle.Event.ON_RESUME -> {
                    Log.d("tgw", "testLiveData:DisposableEffect ON_RESUME")

                }
                Lifecycle.Event.ON_PAUSE -> {
                    Log.d("tgw", "testLiveData:DisposableEffect ON_PAUSE")

                }
                Lifecycle.Event.ON_DESTROY -> {
                    Log.d("tgw", "testLiveData:DisposableEffect ON_DESTROY")

                }
                else -> {}
            }
        }
        lifecycleOwner.lifecycle.addObserver(observer)

        onDispose{
            Log.d("tgw", "testLiveData:DisposableEffect onDispose ")
            lifecycleOwner.lifecycle.removeObserver(observer)

        }
    }
}

3.2、rememberCoroutineScope:获取组合感知作用域,以便在可组合项外启动协程

less 复制代码
/**
 *自动取消协成
 */
@Composable
fun MoviesScreen() {

    // Creates a CoroutineScope bound to the MoviesScreen's lifecycle
    val scope = rememberCoroutineScope()
    val rememberSaveable = rememberSaveable{ mutableStateOf("初始化") }
    Button(
        onClick = {
            //不会取消
            lifecycleScope.launch{
                delay(3000)
                Toast.makeText(this@ObserveDataActivity,"普通的 不会取消协程的",Toast.LENGTH_SHORT).show()
            }

            // 会取消
            scope.launch {
                delay(3000)
                Toast.makeText(this@ObserveDataActivity,"点击事件延迟三秒",Toast.LENGTH_SHORT).show()
            }
        }
    ) {
        Text("rememberCoroutineScope ,开启的协成自动取消")
    }

}

假设MoviesScreen函数在协程响应之前,就因为状态变化而被移除,那么lifecycleScope.launch还会响应,而rememberCoroutineScope无法响应toast,因为被自动注销了。

到这里主要讲了一些数据状态更新以及监听函数的重组,和函数绑定activity的生命周期 后面将讲一下状态提升以及状态恢复

参考: developer.android.google.cn/jetpack/com...

developer.android.google.cn/reference/k...

developer.android.google.cn/jetpack/com...

相关推荐
有梦想的刺儿16 分钟前
webWorker基本用法
前端·javascript·vue.js
cy玩具37 分钟前
点击评论详情,跳到评论页面,携带对象参数写法:
前端
qq_390161771 小时前
防抖函数--应用场景及示例
前端·javascript
John.liu_Test2 小时前
js下载excel示例demo
前端·javascript·excel
Yaml42 小时前
智能化健身房管理:Spring Boot与Vue的创新解决方案
前端·spring boot·后端·mysql·vue·健身房管理
PleaSure乐事2 小时前
【React.js】AntDesignPro左侧菜单栏栏目名称不显示的解决方案
前端·javascript·react.js·前端框架·webstorm·antdesignpro
哟哟耶耶2 小时前
js-将JavaScript对象或值转换为JSON字符串 JSON.stringify(this.SelectDataListCourse)
前端·javascript·json
getaxiosluo2 小时前
react jsx基本语法,脚手架,父子传参,refs等详解
前端·vue.js·react.js·前端框架·hook·jsx
理想不理想v2 小时前
vue种ref跟reactive的区别?
前端·javascript·vue.js·webpack·前端框架·node.js·ecmascript
知孤云出岫2 小时前
web 渗透学习指南——初学者防入狱篇
前端·网络安全·渗透·web