从协程(和其他)状态转换为 Compose 状态
为什么需要状态转换?
在 Jetpack Compose 中,我们的界面常常需要响应外部数据的变化。这些外部数据的数据源可能包括:位置服务提供的坐标信息、LiveData
对象、Flow
或 StateFlow
。
如果这些外部数据源的状态不转换为 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)
)
}
运行结果:
而对于 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)
)
}
运行结果:
注意:需要添加依赖:
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
以上状态的转换都不涉及协程,而当外部状态的获取和更新是基于协程的,比如从一个 Flow
或 StateFlow
中收集数据,或者执行一个耗时的挂起函数来计算状态,我们就不能使用 DisposableEffect
,而是要使用 LaunchedEffect
或 produceState
。
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
对象的变化传递给网络请求(搜索建议)的协程中。
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
函数。