Skip to content

状态管理与重组

更新: 1/31/2026 字数: 0 字 时长: 0 分钟

状态管理

1.创建 MutableState

可以使用 mutableStateOf(0)创建 counter:

kotlin
val counter: MutableState<Int> = mutableStateOf(0)

还可以使用解构,返回 T 类型的 value 以及 (T) -> Unit类型的 set 方法:

kotlin
val (counter, setCounter) = mutableStateOf(0)
setCounter(1) // 使用setCounter更新值,而不用counter.value获取

也可以使用属性代理创建,Kotlin 的 by 关键字直接获取 Int 类型的 counter。

kotlin
var counter by mutableStateOf(0)

通过 by 关键字,可以重写 getValue 和 setValue 最终代理为对 value 的操作。

使用 remember 缓存状态

我们在更新值时,通常用了 remember 进行包裹:

kotlin
var progress by remember { mutableFloatStateOf(0.1f) }

这里 remember 就是获取更新值的关键,因为如果移除的话就会每次重新创建一个初始值为 0 的 MutableState 对象,无法继承前次组合的状态。remember 则可以解决这个问题,在 Composable 首次创建时,remember 中计算得到的数据会自动缓存,再次重组时就会返回之前已缓存的数据。

状态上提

常用于某 Composable 组件需要多处复用,但同时我们希望更改一些逻辑使得具有更高的复用性,状态管理逻辑也有调用方实现,我们可以将 Statefule 改造为 Stateless,Stateful 是指内部持有或者访问了一些状态,而 Stateless 的重组只能来自上层 Composable 的调用。

Stateless 由于不耦合任何业务逻辑,所以功能更加纯粹,相对于 Stateful 的可复用性更好,对测试也更加友好。

比如下面用一个计数器组件实现状态上提:

kotlin
@Composable
fun CounterComponent(
    counter: Int,
    onIncrement: () -> Unit,
    onDecrement: () -> Unit
) {
    Column(
        modifier = Modifier.padding(16.dp)
    ) {
        Text(
            "Click the buttons",
            Modifier.fillMaxWidth(),
            textAlign = TextAlign.Center
        )
        Text(
            "$counter",
            Modifier.fillMaxWidth(),
            textAlign = TextAlign.Center,
            style = Typography.bodyMedium
        )
        Row {
            Button(
                onClick = { onDecrement() },
                Modifier.weight(1f)
            ) {
                Text("-")
            }
            Spacer(Modifier.width(16.dp))
            Button(
                onClick = { onIncrement() },
                Modifier.weight(1f)
            ) {
                Text("+")
            }
        }
    }
}

@Composable
fun CounterScreen() {
    var counter by remember { mutableStateOf(0) }
    CounterComponent(counter = counter, { counter++ }) {
        if (counter > 0) {
            counter--
        }
    }
}

CounterScreen 在调用 CounterComponent 时为其注入了 counter,onIncrement(),onDecrement 几个参数。

状态上提还有助于实现单一数据源,状态总是自上而下流动,事件总是自下向上传递。

但是更推荐管理状态时使用 ViewModel 因为参数往往都很复杂。

添加依赖:

kotlin
lifecycle = "2.8.7"
compose-lifecycle-viewmodel = { group = "androidx.lifecycle", name = "lifecycle-viewmodel-compose", version.ref = "lifecycle"}

创建 ViewModel:

kotlin
class CounterViewModel : ViewModel() {

    private val _counter = mutableStateOf(0)
    val counter: State<Int> = _counter

    fun increment() {
        _counter.value++
    }

    fun decrement() {
        if(_counter.value > 0) {
            _counter.value--
        }
    }

}

使用:

kotlin
@Composable
fun CounterScreen() {
    val viewmodel: CounterViewModel = viewModel()
    CounterComponent(counter = viewmodel.counter.value, viewmodel::increment, viewmodel::decrement)
}

状态分层管理

使用 Stateful 管理状态

简单的 UI 状态以及逻辑可以直接在 Composable 中管理(使用 rememberState):

kotlin
fun MyApp() {
    ComposeStudyTheme {
        val snackbarHostState = remember { SnackbarHostState() }
        val coroutineScope = rememberCoroutineScope()
        Scaffold(snackbarHost = { SnackbarHost(snackbarHostState) }) { innerPadding ->
            coroutineScope.launch {
                snackbarHostState.showSnackbar("Hello")
            }
        }
    }
}

使用 StateHolder 管理状态

可以创建一个专门的 StateHolder 用来剥离 UI 逻辑:

kotlin
class MyAppState(
    val snackbarHostState: SnackbarHostState,
    val navController: NavHostController,
	private val resources: Resources,
	...
) {
	val bottomBarTabs = ...
	val shouldShowBottomBar: Boolean
		get() = ...
	fun navigateToBottomBarRoute(route: String) { }
	fun showSnackbar(message: String) { }
}

// 创建remember方法便于在Composable中使用
fun rememberMyAppState(...
) = remember ( ... ) {
	MyAppState(...)
}

使用 ViewModel 管理状态

可以使用 ViewModel 管理与 UI 状态无关的数据与逻辑,也能实现全局共享。

kotlin
// 使用private set避免来自ViewModel外对于UIState的更新
    var uiState by mutableStateOf(0)
        private set

LiveData,RxJava,Flow 转 State:

LiveData.observeAsState: androidx.compose.runtime: runtime-livedata: $compose version

Flow.collectAsState(): 自带

Observable.subscribeAsState(): androidx.compose.runtime:runtime-rxjava3: $compose version

Composable 生命周期

  1. onActive(添加到视图树):首次被执行时
  2. onUpdate(重组)
  3. onDispose(从视图树移除)

Composable 副作用

Composable 在执行过程中,凡是会影响外界的操作都属于副作用(Side-Effects),比如弹出 Toast,保存本地文件,访问远程或本地数据等。重组会导致 Composable 频繁反复执行,而副作用显然是不应该跟着重组反复执行。所以 Compose 提供了一系列 API,让副作用 API 只发生在 Composable 生命周期的特定阶段。

副作用 API

DisposableEffect

kotlin
@Composable
fun backPressHandler(enabled: Boolean = true, onBackPressed: () -> Unit) {
    val backDispatcher = checkNotNull(LocalOnBackPressedDispatcherOwner.current) {
        "No OnBackPressedDispatcherOwner was provided via LocalOnBackPressedDispatcherOwner"
    }.onBackPressedDispatcher
    val backCallback = remember {
        object : OnBackPressedCallback(enabled) {
            override fun handleOnBackPressed() {
                onBackPressed()
            }
        }
    }
    DisposableEffect(backDispatcher) {
        backDispatcher.addCallback(backCallback)
        onDispose {
            //进入onDispose时移除
            backCallback.remove()
        }
    }
}

先创建了 OnBackPressedCallback 回调处理返回事件,使用 remember 包裹防止重复创建。

然后在 DisposableEffect 后的 block 内向 OnBackPressedDispatcher 注册返回事件回调。也能接受参数 key,但不能为空:

  • 如果 key 为 Unit/true 常量,则只在 OnActive 时执行一次
  • 如果为其他变量,则还会再 OnActive 内执行,比如如果 dispatcher 变化,则会注册新的回调。

DisposableEffect 最后必须跟 onDispose 进行首位处理,如注销回调。

使用时就很简单了:

kotlin
@Composable
fun HomeScreen() {
    backPressHandler { 
        //返回键处理
    }
}

SideEffect

SideEffect 会在每次成功重组时执行,所以不能用来处理耗时或异步的副作用逻辑。

与直接写在 Composable 里面相比,SideEffect 只会在重组成功时执行,因为重组会触发 Composable 执行,但不一定会成功结束,可能会中途失败。可用来将当前正确的 State 暴露给外部。

异步处理的副作用 API

LaunchedEffect

当副作用中有处理异步任务的需求时,可以使用 LaunchedEffect。 在 Composable 进入 OnActive 时,LaunchedEffect 会启动协程执行内容,也可以启动子协程或调用挂起函数,当进入 OnDispose 时协程会自动取消,故不需要实现 OnDispose。

同时 LaunchedEffect 支持参数 key 的设置,当 key 发生变化时,协程自动结束,同时开启新的协程。

rememberCoroutineScope

LaunchedEffect 只能在 Composable 中调用,如果想在非 Composable 环境中使用协程,比如 Button 的 OnClick 中使用协程显示 snackBar,可以使用这个:

kotlin
@Composable
fun SnackbarExample() {
    val snackbarHostState = remember { SnackbarHostState() }
    val scope = rememberCoroutineScope()

    Scaffold(
        snackbarHost = { SnackbarHost(snackbarHostState) }
    ) { innerPadding ->
        Button(
            onClick = {
                scope.launch {
                    snackbarHostState.showSnackbar(
                        message = "数据已保存",
                        actionLabel = "撤销"
                    )
                }
            },
            modifier = Modifier.padding(innerPadding)
        ) {
            Text("显示 Snackbar")
        }
    }
}

这里 scope.launch 会返回一个 CoroutineScope,可以在进入 onDispose 时自动取消。

rememberUpdateState

LaunchedEffect 会在参数 key 变化时启动一个协程,但有时我们不希望协程中断,只要能实时获取最新状态,此时可以借助 rememberUpdateState 实现:

kotlin
@Composable
fun MyScreen(onTimeout: () -> Unit) {
    val currentOnTimeOut by rememberUpdatedState(onTimeout)
    LaunchedEffect(Unit) {
        delay(1000)
        currentOnTimeOut() // 总是能收到最新的onTimeout
    }
}

这里 block 开始执行后,不会因 MyScreen 重组而中断。但是当执行到 currentOnTimeout()时,仍能获取最新的 OnTimeOut 实例。

snapshotFlow

用来转成 Coroutine Flow,观察参数 key 来通知状态变化,每当 State 发生变化时,flow 就会发送新数据,如果无变化则不发射。

kotlin
val pagerState = rememberPagerState()
LaunchedEffect(pagerState) {
	snapshotFlow { pagerState.currentPage }
		.collect { page ->
			// 发生变化
		}
}

状态创建的副作用 API

produceState

这个 API 用来将一个外部的数据源转成 State。

kotlin
@Composable
fun loadNetWworkImage(
    url: String,
    imageRepository: ImageRepository
) : State<Result<Image>> {
    return produceState<Result<Image>>(initialValue = Result.Loading) { 
        val image = imageRepository.load(url)
        value = if (image == null) {
            Result.Error
        } else {
            Result.success(image)
        }
    }
}

上面代码中,通过网路请求获取图片并用 produceState 转换为 State<Result<Image>>,当 Image 获取失败时,会返回 Result.Error。produceState 观察 url 和 imageRepository 两个参数,当发生变化时,producer 会重新执行。

derivedStateOf

用来将一个/多个 State 转成另一个 State。derivedStateOf{...}的 block 中可以依赖其他 State 创建并返回一个 DerivedState,当 block 中依赖的 State 发生变化时,会更新此 DerivedState,依赖此 State 的所有 Composable 会因其变化而重组。

kotlin
@Composable
fun SearchScreen() {
    val postList = remember { mutableStateListOf<String>() }
    var keyword by remember { mutableStateOf("") }
    val result by remember {
        derivedStateOf { postList.filter { it.contains(keyword, false) } }
    }
    Box(Modifier.fillMaxSize()) {
        LazyColumn { 
            item(result) { /* ... */ }
        }
    }
}

上面代码在 derivedStateOf{...}的 block 内部实现了检索逻辑,当 postList 或者 keyword 任意变化时,result 会更新。

当一个状态的变化需要造成副作用终止时,才将其添加为观察参数 key,否则应该将其使用rememberUpdateState包装后,在副作用中使用,以避免打断执行中的副作用。