Jetpack Compose 状态转换:从外部源到 UI,从 State 到协程 Flow

488 阅读5分钟

从协程(和其他)状态转换为 Compose 状态

为什么需要状态转换?

在 Jetpack Compose 中,我们的界面常常需要响应外部数据的变化。这些外部数据的数据源可能包括:位置服务提供的坐标信息、LiveData 对象、FlowStateFlow

如果这些外部数据源的状态不转换为 Compose 能够理解和响应的内部状态(即 State<T> 对象),那么 Compose 将无法感知到外部数据的变化,UI 也不会自动更新。

所以,我们需要将这些外部数据源转换为 State 对象,这样当外部数据更新时,对应的 State 状态对象的值也能随之更新,从而触发相关 Composable 函数的重组,刷新界面以显示最新的信息。

处理非协程状态:DisposableEffect 登场

我们先来看看 DisposableEffect 函数,它可以让我们 Composable 在进入组合(Composition)时执行副作用,在离开组合时执行相关的清理操作,这非常适合订阅和取消订阅(外部数据源)的场景。

DisposableEffect(key1 = ) { // key1 控制 DisposableEffect 的重启
    // 副作用:在这里执行订阅、启动监听等操作
    
    onDispose { 
        // 清理:在这里执行取消订阅、停止监听等操作
    }
}

比如:有一个位置管理器FakeGeoManager,每当用户的位置发生变化了,它都会通过回调推送最新的位置信息。

data class MyPoint(val x: Int, val y: Int)

interface FakeGeoManager {
    fun register(callback: (MyPoint) -> Unit)
    fun unregister(callback: (MyPoint) -> Unit)
}


@Composable
fun rememberFakeGeoManager(): FakeGeoManager {
    return remember {
        object : FakeGeoManager {
            private var listener: ((MyPoint) -> Unit)? = null
            private var job: Job? = null 

            override fun register(callback: (MyPoint) -> Unit) {
                listener = callback
                // 模拟位置更新
                job = CoroutineScope(Dispatchers.Default + SupervisorJob()).launch {
                    var i = 0
                    while (isActive) {
                        delay(1000)
                        listener?.invoke(MyPoint(i++, i * 2))
                    }
                }
            }

            override fun unregister(callback: (MyPoint) -> Unit) {
                if (listener == callback) {
                    job?.cancel()
                    listener = null
                }
            }
        }
    }
}

我们使用 DisposableEffect 来订阅位置信息,将这种“外部状态”转为 Compose 的状态:

// ------- 实时位置信息展示 -------
@Composable
fun RealTimeLocationScreenDemo() {
    val geoManager = rememberFakeGeoManager()
    // 定义一个 State 对象来存储位置信息
    var position by remember { mutableStateOf(MyPoint(0, 0)) }
    
    DisposableEffect(geoManager) {
        val callback = { newPosition: MyPoint ->
            position = newPosition // 当位置更新时,更新 State 对象的值
        }

        geoManager.register(callback) // 订阅(监听)位置更新

        
        onDispose {
            geoManager.unregister(callback) // 取消订阅,避免内存泄露
        }
    }

    Text(
        "DisposableEffect Location: ${position.x}, ${position.y}",
        modifier = Modifier.padding(8.dp)
    )
}

运行结果:

image.gif

而对于 LiveData

// 假设有一个 ViewModel 持有 LiveData
class LocationViewModel : ViewModel() {
    private val _positionLiveData = MutableLiveData<MyPoint>()
    val positionLiveData: LiveData<MyPoint> = _positionLiveData

    init {
        viewModelScope.launch { // 使用 viewModelScope 保证协程生命周期正确
            var i = 100
            while (isActive) { // 使用 isActive 确保协程可以被取消
                delay(1500) // 模拟异步更新
                val newPoint = MyPoint(i, i + 5)
                _positionLiveData.postValue(newPoint)
                i++
            }
        }
    }
}

你也可以使用 DisposableEffect 进行转换,像这样:

@Composable
fun LiveDataLocationScreenWithDisposableEffect(
    viewModel: LocationViewModel = viewModel(),
    lifecycleOwner: LifecycleOwner = LocalLifecycleOwner.current // 获取当前生命周期所有者
) {
    var position by remember { mutableStateOf(MyPoint(0, 0)) }

    DisposableEffect(Unit) {
        val observer = Observer<MyPoint> { newPosition ->
            position = newPosition
        }
        viewModel.positionLiveData.observe(lifecycleOwner, observer)
        onDispose {
            viewModel.positionLiveData.removeObserver(observer)
        }
    }

    Text(
        text = "LiveData (DisposableEffect) Location: ${position.x}, ${position.y}",
        modifier = Modifier.padding(8.dp)
    )
}

运行结果:

image.gif

注意:需要添加依赖:

implementation("androidx.compose.runtime:runtime-livedata:${compose_version}") —— 使用observeAsState() 函数

implementation("androidx.lifecycle:lifecycle-viewmodel-compose:${lifecycle_version}") —— 使用viewModel() 函数

LiveData 转换为 State 也可以使用 Compose 提供的更为便捷的扩展函数——observeAsState()

@Composable
fun LiveDataLocationScreenDemo(viewModel: LocationViewModel = viewModel()) {
    // 初始值为 MyPoint(0,0)
    val position: MyPoint? by viewModel.positionLiveData.observeAsState(initial = MyPoint(0,0))

    Text(
        text = "LiveData Location: ${position?.x ?: "N/A"}, ${position?.y ?: "N/A"}",
        modifier = Modifier.padding(8.dp)
    )
}

observeAsState 函数的内部其实也使用了 DisposableEffect ,来自动订阅和取消订阅 LiveData

@Composable
fun <R, T : R> LiveData<T>.observeAsState(initial: R): State<R> {
    val lifecycleOwner = LocalLifecycleOwner.current
    val state = remember {
        @Suppress("UNCHECKED_CAST") /* Initialized values of a LiveData<T> must be a T */
        mutableStateOf(if (isInitialized) value as T else initial)
    }
    DisposableEffect(this, lifecycleOwner) {
        val observer = Observer<T> { state.value = it }
        observe(lifecycleOwner, observer)
        onDispose { removeObserver(observer) }
    }
    return state
}

处理协程状态:LaunchedEffect 与 produceState

以上状态的转换都不涉及协程,而当外部状态的获取和更新是基于协程的,比如从一个 FlowStateFlow 中收集数据,或者执行一个耗时的挂起函数来计算状态,我们就不能使用 DisposableEffect,而是要使用 LaunchedEffectproduceState

LaunchedEffect

// 模拟一个在 Composable 生命周期内持续更新的 StateFlow
@Composable
fun rememberUpdatingStateFlow(
    initialValue: MyPoint,
    delayMillis: Long,
    startNumber: Int
): StateFlow<MyPoint> {
    return remember(initialValue, delayMillis, startNumber) { 
        val mutableStateFlow = MutableStateFlow(initialValue)
        CoroutineScope(Dispatchers.Default + SupervisorJob()).launch {
            var i = startNumber
            while (isActive) {
                delay(delayMillis)
                mutableStateFlow.value = MyPoint(i, i + 5)
                i++
            }
        }
        mutableStateFlow
    }
}

@Composable
fun FlowLocationScreenWithLaunchedEffect() {
    val positionStateFlow = rememberUpdatingStateFlow(MyPoint(200, 200), 1200, 200)
    // 初始值直接取 StateFlow 的当前值
    var position by remember { mutableStateOf(positionStateFlow.value) }

    LaunchedEffect(positionStateFlow) {
        println("LaunchedEffect collecting from StateFlow: ${positionStateFlow.value}")
        positionStateFlow.collect { newPosition ->
            position = newPosition // 当 Flow 发送新值时,更新 State 对象
        }
        // 当 LaunchedEffect 取消时,collect 操作也会自动取消,无需我们手动取消
    }

    Text(
        "LaunchedEffect StateFlow Location: ${position.x}, ${position.y}",
        modifier = Modifier.padding(8.dp)
    )
}

produceState

你也可以使用 Compose 提供的 produceState 函数来转换,它的返回值是一个 State 对象,我们就不用手动创建一个 State 对象了,并且它内部会启动一个协程,会将新值推送到返回的 State 对象中。

@SuppressLint("StateFlowValueCalledInComposition")
@Composable
fun FlowLocationScreenWithProduceState() {
    val positionStateFlow = rememberUpdatingStateFlow(MyPoint(300, 300), 1300, 300)
    
    val position by produceState(initialValue = positionStateFlow.value, positionStateFlow) {

        positionStateFlow.collect { newPosition ->
            value = newPosition // 更新 State
        }
    }

    Text(
        "produceState StateFlow Location: ${position.x}, ${position.y}",
        modifier = Modifier.padding(8.dp)
    )
}

如果有一些非协程的资源需要清理,可以在 produceState 函数的内部使用 awaitDispose

@SuppressLint("StateFlowValueCalledInComposition")
@Composable
fun FlowLocationScreenWithProduceState() {
    val positionStateFlow = rememberUpdatingStateFlow(MyPoint(300, 300), 1300, 300)

    val position by produceState(initialValue = positionStateFlow.value, positionStateFlow) {

        positionStateFlow.collect { newPosition ->
            value = newPosition // 更新 State
        }
+        // `awaitDispose` 是一个挂起函数,它会挂起 producer 协程,
+        // 直到该协程被取消,它的 lambda 块会在协程取消后执行,用于清理资源。
+        awaitDispose {
+            println("produceState for StateFlow ${positionStateFlow.value} disposed/cancelled. Clean up here if needed.")
+        }
    }

    Text(
        "produceState StateFlow Location: ${position.x}, ${position.y}",
        modifier = Modifier.padding(8.dp)
    )
}

collectAsState

最后,对于 Flow,Compose 提供了扩展函数 collectAsState(),它只需一行代码就能完成转换。

@Composable
fun FlowLocationScreenWithCollectAsState() {
    val positionStateFlow = rememberUpdatingStateFlow(MyPoint(400, 400), 1400, 400)
    
    // 对于冷流 (cold Flow),建议提供,不提供的话,在 Flow 发送第一个值之前,State 对象可能没初始状态
    val position by positionStateFlow.collectAsState(initial = MyPoint(0,0)) // 初始值不提供的话,为 positionStateFlow.value

    Text(
        "collectAsState StateFlow Location: ${position.x}, ${position.y}",
        modifier = Modifier.padding(8.dp)
    )
}

collectAsState 的内部,其实也是使用 produceState 实现的。

@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 } }
    }

Compose 的 State 状态转换为协程

有时,我们可能需要将 State 对象的变化在协程中被感知到。

比如搜索时的搜索建议,这就需要将输入框的 State 对象的变化传递给网络请求(搜索建议)的协程中。

image.png

snapshotFlow()

snapshotFlow 函数的返回结果是一个 Flow 对象,它会在 snapshotFlow 函数内部所涉及到任何一个的 State 对象发生改变时(可能有多个 State 对象),发出新的值。

也就是能够感知到内部使用到的状态对象的变化。

比如:

@Composable
fun StateToFlowDemo() {
    var count by remember { mutableIntStateOf(0) }

    Button(onClick = { count++ }) {
        Text("Click to increase count: $count")
    }

    // 创建一个 Flow,它会感知 count 的变化
    val countFlow: Flow<Int> = snapshotFlow {
        println("snapshotFlow block re-executed, current count: $count")
        count // 当 count 改变时,Flow 会发出新的 count 值
    }

    // 收集 Flow
    LaunchedEffect(Unit) { 
        countFlow
            .collect { collectedCount ->
                println("Collected from snapshotFlow: $collectedCount")
                // 在这里可以将 collectedCount 传递给其他非 Compose 的协程代码
            }
    }
}

每次点击按钮,都会增加 count 的值,从而重新执行 snapshotFlow 函数的 lambda 块,发出新的值,collect 会接收到这个新值,这样每次打印的都是新值。

使用场景:当你的 Flow 对象使用到了 State 对象,就不能使用 flow 函数,因为它不能响应后续状态对象的变化,而是要用 snapshotlow 函数。