Jetpack Compose 状态管理

2,897 阅读7分钟

Jetpack Compose 是用于构建原生 Android 界面的新工具包,状态管理对于构建健壮,高效且可维护的应用程序至关重要,理解和应用有效的状态管理模式,是充分利用 Jetpack Compose 能力的关键。

remember + MutableState

使用 remember 可以将对象存储在内存中,系统会在初始组合期间将由 remember 计算的值存储在组合中,并在重组期间返回存储的值。

mutableStateOf 会创建可观察的 MutableState,是与 Compose 运行时集成的可观察类型,如果 value 有任何变化,系统就会为读取 value 的所有可组合函数安排重组。

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

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

计数器案例

@Composable
fun Counter() {
    var count by remember { mutableStateOf(1) }

    Button(onClick = { count++ }) {
        Text(text = count.toString())
    }
}

增加列表的点击效果案例

@Composable
fun List(dataList: List<String>) {
    val selectedStates = remember {
        dataList.map { mutableStateOf(false) }
    }
    LazyColumn(content = {
        itemsIndexed(dataList) { index, s ->
            val isSelect = selectedStates[index]
            Text(text = s, modifier = Modifier.selectable(
                selected = isSelect.value,
                onClick = { isSelect.value = !isSelect.value }
            ))
        }
    })
}

remember 还可以接受 key 参数,当 key 发生变化,缓存值会失效并再次对 lambda 块进行计算。

@Composable
inline fun <T> remember(
    key1: Any?,
    crossinline calculation: @DisallowComposableCalls () -> T
): T {
    return currentComposer.cache(currentComposer.changed(key1), calculation)
}

remember 虽然可以跨越重组,但是不会保留状态跨配置变化,比如横竖屏切换的时候,状态会丢失。如果此时想保存状态,就得使用 rememberSaveable 了。rememberSavable 实际上就是将数据以 Bundle 的形式保存,所以凡是 Bundle 支持的基本数据类型都可以自动保存,对于对象类型,则可以通过添加 @Parcelize 变为一个 Parcelable 对象进行保存。

MutableState + ViewModel

mutableStateOf 也可以直接在 ViewModel 中使用,mutableStateOf 是线程安全的,也能够保证状态的更新能通知到观察者,即 Composable 函数。

mutableStateOf 在 Composable 函数中使用需要搭配 remember 来保持状态,但是在 ViewModel 中使用却不用,因为 ViewModel 本身就可以缓存状态,并可在配置更改后持久保留相应状态。

class MainViewModel : ViewModel() {
    var count by mutableStateOf(1)
        private set

    fun increase() {
        count++
    }
}
@Composable
fun Counter() {
    val viewModel: MainViewModel = viewModel()

    Button(onClick = { viewModel.increase() }) {
        Text(text = viewModel.count.toString())
    }
}

其中,使用 viewModel() 函数需要引入依赖:

implementation ("androidx.lifecycle:lifecycle-viewmodel-compose:2.6.2")

StateFlow 管理状态

使用 StateFlow 来管理状态,一般与 ViewModel 结合使用,并在 Composable 函数中通过 collectAsState 收集状态,从而实现响应式 UI 更新。

class MainViewModel : ViewModel() {
    
    private val _dataFlow = MutableStateFlow<List<String>>(emptyList())
    val dataFlow: StateFlow<List<String>> = _dataFlow

    fun getData() {
        val dataList =
            arrayListOf("hello", "google", "android", "apple", "ios", "huawei", "harmony")
        _dataFlow.value = dataList
    }
}

点击按钮,获取数据。

@Composable
fun List() {
    val viewModel: MainViewModel = viewModel()
    val dataList by viewModel.dataFlow.collectAsState()
    Column {
        Button(onClick = { viewModel.getData() }) {
            Text(text = "GetData")
        }
        LazyColumn(content = {
            items(dataList) {
                Text(text = it)
            }
        })
    }
}

LiveData 管理状态

LiveData 是一种具有生命周期感知能力的可观察的数据存储器类,一般与 ViewModel 搭配使用,在 Compose 中使用 LiveData 进行状态管理,通过 observeAsState 转换为 Compose 可用的状态。

使用 observeAsState 需要引入依赖:

implementation("androidx.compose.runtime:runtime-livedata:1.4.0")

与上面 StateFlow 的代码案例类似,这里简单修改一下即可。

class MainViewModel : ViewModel() {

    private val _liveData = MutableLiveData<List<String>>(emptyList())
    val liveData: LiveData<List<String>> = _liveData

    fun getData() {
        val dataList =
            arrayListOf("hello", "google", "android", "apple", "ios", "huawei", "harmony")
        _liveData.value = dataList
    }
}
@Composable
fun List() {
    val viewModel: MainViewModel = viewModel()
    val dataList by viewModel.liveData.observeAsState(emptyList())
    Column {
        Button(onClick = { viewModel.getData() }) {
            Text(text = "GetData")
        }
        LazyColumn(content = {
            items(dataList) {
                Text(text = it)
            }
        })
    }
}

状态提升

状态提升用于在组合界面中管理和共享状态,可以更好地组织和管理应用程序的状态,并确保状态的一致性。状态提升的基本思想是将状态从子组件移动到父组件,并通过参数传递给子组件,状态向下流动,事件向上传播。

@Composable
fun Counter() {
    var count by remember { mutableStateOf(1) }
    CounterPage(count = count) {
        count++
    }
}

@Composable
fun CounterPage(count: Int, increase: () -> Unit) {
    Button(onClick = increase) {
        Text(text = count.toString())
    }
}

状态提升的好处:

  • 集中管理状态:状态提升可以将状态集中管理在顶层组件中,使得状态更易于追踪和管理。
  • 状态一致性:通过将状态提升到顶层组件并向下传递,可以确保整个应用程序中的各个组件都使用相同的状态。
  • 可重用性:将状态从子组件移动到父组件后,子组件变得更加通用,因为它们不再依赖于特定的状态,这样可以促进组件的复用。
  • 逻辑分离:状态提升有助于将 UI 逻辑与状态管理逻辑分离开来,使得组件更加专注于 UI 渲染和交互,这种分离有助于代码模块化和降低代码的耦合度。

附带效应

附带效应是指发生在可组合函数作用域之外的应用状态的变化,如果需要更改应用的状态,应该使用 Effect API,以便以可预测的方式执行这些附带效应。

虽然 mutableStateOf 提供了一种方便的方式来跟踪 Compose 中的可变状态,但它本身并不支持执行副作用操作。这意味着如果需要在 Composable 函数内部执行具有副作用的操作如网络请求,文件读写等,则需要使用附带效应。

LaunchedEffect

LaunchedEffect 用于在可组合项的作用域内运行挂起函数,当 LaunchedEffect 进入组合时,它会启动一个协程,并将代码块作为参数传递,如果 LaunchedEffect 退出组合,协程将取消。

@Composable
fun Effect() {
    val count = remember { mutableStateOf(1) }
    LaunchedEffect(Unit) {
        while (true) {
            delay(1000)
            count.value++
        }
    }
    Text(text = "${count.value}")
}

rememberCoroutineScope

LaunchedEffect 只能在可组合函数中使用,如果需要在可组合项外启动协程, 就可以使用 rememberCoroutineScope

@Composable
fun Effect() {
    val count = remember { mutableStateOf(1) }
    val coroutineScope = rememberCoroutineScope()
    Column {
        Button(onClick = {
            coroutineScope.launch {
                delay(1000)
                count.value++
            }
        }) {
            Text(text = "${count.value}")
        }
    }
}

rememberUpdatedState

当其中一个键参数发生变化时,LaunchedEffect 会重启。不过,有时可能希望在效应中捕获某个值,但如果该值发生变化,又不希望效应重启。为此,就需要使用 rememberUpdatedState 来创建对可捕获和更新的该值的引用,可以确保在效应中读取的状态是最新的。

举个例,先来看没有使用 rememberUpdatedState 的情况:

@Composable
fun NewCounter() {
    var count by remember {
        mutableStateOf(0)
    }
    Column {
        Button(onClick = { count++ }) {
            Text(text = "increase: $count")
        }
        DelayText(count.toString())
    }
}

@Composable
fun DelayText(text: String) {
    var delayText by remember {
        mutableStateOf("")
    }
    LaunchedEffect(Unit) {
        delay(3000)
        delayText = text
    }
    Text(text = "DelayText: $delayText")
}

在3秒内连续点击按钮使其值增加,但是实际的结果并没有拿到最新的值,如下所示:

image.png

为什么会出现这种情况呢?因为如果 key 没有变化的话,那么 LaunchedEffect 内部的 lambda 一直都是最初的那个实例,那个实例拿到的 text 就是最初刚启动的值。

这种情况就可以使用 rememberUpdateState 来解决,下面来改写一下 DelayText 函数。

@Composable
fun DelayText(text: String) {
    var delayText by remember {
        mutableStateOf("")
    }
    val updateText by rememberUpdatedState(newValue = text)
    LaunchedEffect(Unit) {
        delay(3000)
        delayText = updateText
    }
    Text(text = "DelayText: $delayText")
}

运行效果:

image.png

这就是 rememberUpdateState 存在的意义,说白了,就是用一个容器装着值,我们取的也依旧是最初的那个容器,但是这并不影响,因为取的不是容器本身,而是容器里面的值。

DisposableEffect

对于需要在键发生变化或可组合项退出组合后进行清理的附带效应,可以使用 DisposableEffect。DisposableEffect 必须添加一个 onDispose 方法 ,此方法一般用来处理资源清理与释放。这里举例 Compose 官方文档的监听生命周期的例子:

@Composable
fun HomeScreen(
    lifecycleOwner: LifecycleOwner = LocalLifecycleOwner.current,
    onStart: () -> Unit,
    onStop: () -> Unit
) {
    val currentOnStart by rememberUpdatedState(onStart)
    val currentOnStop by rememberUpdatedState(onStop)
    DisposableEffect(lifecycleOwner) {
        val observer = LifecycleEventObserver { _, event ->
            if (event == Lifecycle.Event.ON_START) {
                currentOnStart()
            } else if (event == Lifecycle.Event.ON_STOP) {
                currentOnStop()
            }
        }
        lifecycleOwner.lifecycle.addObserver(observer)
        onDispose {
            lifecycleOwner.lifecycle.removeObserver(observer)
        }
    }
}
class MainActivity : ComponentActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            StateComposeAppTheme {
                Surface(
                    modifier = Modifier.fillMaxSize(),
                    color = MaterialTheme.colorScheme.background
                ) {
                    Column {
                        HomeScreen(LocalLifecycleOwner.current, {
                            //执行 onStart
                        }, {
                            //执行 onStop
                        })
                    }

                }
            }
        }
    }
}

SideEffect

使用 SideEffect 可保证效果在每次成功重组后都会执行,它能正确的向外传递状态,可用于将 Compose 状态发布到非 Compose 代码,但不能用来处理耗时和异步任务。

@Composable
fun Counter() {
    var count by remember { mutableStateOf(1) }
    SideEffect {
        // 每次重组都会触发,在此调用非 Composable 函数 handleCount 处理这个 count 的值。
        handleCount(count)
    }
    Text(text = count.toString())
    Button(onClick = { count++ }) {
        Text(text = "increase")
    }
}

produceState

produceState 会启动一个协程,使用此协程可以将非 Compose 状态转换为 Compose 状态。produceState 可以在 Compose 中创建一个可观察的状态,并且可以通过协程来对其进行更新。这样就可以使用常规的 Kotlin 代码来管理应用程序状态,并且状态更改时,Compose 会自动重新绘制与该状态相关联的 UI 部分。

var count = 0

@Composable
fun StateCounter() {
    val countState = produceState(initialValue = count) {
        while (true) {
            delay(1000)
            value++
        }
    }
    Column(
        modifier = Modifier.fillMaxSize(),
        verticalArrangement = Arrangement.Center,
        horizontalAlignment = Alignment.CenterHorizontally
    ) {
        Text(text = countState.value.toString())
    }
}

derivedStateOf

derivedStateOf 可以将一个或多个状态对象转换为其他状态,它可以根据其他状态的变化来计算新的状态,同时确保只有在相关状态发生变化时才进行重新计算,从而提高性能。

@Composable
fun Counter() {
    var count by remember { mutableStateOf(1) }
    val beyondTen by remember {
        derivedStateOf {
            count > 10
        }
    }
    Column {
        Text(text = "Does it exceed 10:$beyondTen")
        Button(onClick = { count++ }) {
            Text(text = count.toString())
        }
    }
}

snapshotFlow

使用 snapshotFlow 将 State 对象转换为冷 Flow,它会在收集到块时运行该块,并发出从块中读取的 State 对象的结果。

@Composable
fun Counter() {
    var count by remember { mutableStateOf(1) }
    val countFlow = snapshotFlow { count }
    LaunchedEffect(Unit) {
        countFlow.collect {
            //todo 这里拿到值做些处理
        }
    }
    Column {
        Text(text = count.toString())
        Button(onClick = { count++ }) {
            Text(text = "increase")
        }
    }
}

重启效应

Compose 中有一些效应如 LaunchedEffect,produceState,DisposableEffect等,会采用可变数量的参数和键来取消运行效应,并使用新的键启动一个新的效应。

比如 LaunchedEffect,它有个参数 key,用于标识启动的效果,确保在 key 改变时重新启动效果,而在 key 保持不变时不会重新启动效果。

@Composable
@NonRestartableComposable
@OptIn(InternalComposeApi::class)
fun LaunchedEffect(
    key1: Any?,
    block: suspend CoroutineScope.() -> Unit
) {
    val applyContext = currentComposer.applyCoroutineContext
    remember(key1) { LaunchedEffectImpl(applyContext, block) }
}
@Composable
fun Counter() {
    var count by remember { mutableStateOf(1) }
    LaunchedEffect(count) {
        //todo count每次变化时都会执行
    }
    Column {
        Text(text = count.toString())
        Button(onClick = { count++ }) {
            Text(text = "increase")
        }
    }
}

也可以使用常量作为键,使其遵循调用点的生命周期。举个例子:

@Composable
fun Counter() {
    var count by remember { mutableStateOf(1) }
    LaunchedEffect(true) {
        //只执行一次
    }
    Text(text = count.toString())
    Column {
        Button(onClick = { count++ }) {
            Text(text = "increase")
        }
    }
}

因为 LaunchedEffect 的 key 参数是一个常量 true,所以它的副作用会在组合第一次被计算时执行,之后不会再次执行。