如何安全地(生命周期感知).collectAsState() 一个 StateFlow?

How to safely (lifecycle aware) .collectAsState() a StateFlow?

我正在尝试遵循官方指南,根据这些文章从 LiveData 迁移到 Flow/StateFlow 使用 Compose:

A safer way to collect flows from Android UIs

Migrating from LiveData to Kotlin’s Flow

我正在尝试遵循第一篇文章中的建议,在接近尾声的 Jetpack Compose 中的安全流集合 部分中。

In Compose, side effects must be performed in a controlled environment. For that, use LaunchedEffect to create a coroutine that follows the composable’s lifecycle. In its block, you could call the suspend Lifecycle.repeatOnLifecycle if you need it to re-launch a block of code when the host lifecycle is in a certain State.

我已经设法以这种方式使用 .flowWithLifecycle() 来确保当应用程序进入后台时流量不会发出:

@Composable
fun MyScreen() {

    val lifecycleOwner = LocalLifecycleOwner.current

    val someState = remember(viewModel.someFlow, lifecycleOwner) {
        viewModel.someFlow
            .flowWithLifecycle(lifecycleOwner.lifecycle, Lifecycle.State.STARTED)
            .stateIn(
                scope = viewModel.viewModelScope,
                started = SharingStarted.WhileSubscribed(5000),
                initialValue = null
            )
    }.collectAsState()

}

我觉得这很“老套”-一定有更好的东西。我想在 ViewModel 中使用 StateFlow,而不是在 @Composable 中转换为 StateFLow 的 Flow,并使用 .repeatOnLifeCycle(), so I can use multiple .collectAsState() 和更少的样板文件。

当我尝试在协同程序 (LaunchedEffect) 中使用 .collectAsState() 时,我显然收到有关必须从 @Composable 函数的上下文中调用 .collectAsState() 的错误。

如何实现与 .collectAsState() 类似的功能,但在 .repeatOnLifecycle() 内部。我是否必须在 StateFlow 上使用 .collect() 然后用 State 包装值?没有比这更少样板的东西了吗?

在阅读了更多文章后,包括

Things to know about Flow’s shareIn and stateIn operators

repeatOnLifecycle API design story

并最终意识到我想在 ViewModel 中而不是在可组合项中使用 StateFlow,我想出了这两个解决方案:

1. 我最终使用的是,这对于驻留在 ViewModel 中的多个 StateFlow 更好,这些 StateFlow 需要在后台收集,同时有来自 UI(在这种情况下,加上 5000 毫秒的延迟来处理配置更改,例如屏幕旋转,其中 UI 仍然对数据感兴趣,因此我们不想重新启动 StateFlow 收集例程)。就我而言,原始 Flow 来自 Room,并在 VM 中成为 StateFlow,因此应用程序的其他部分可以访问最新数据。

class MyViewModel: ViewModel() {

    //...

    val someStateFlow = someFlow.stateIn(
        scope = viewModelScope,
        started = SharingStarted.WhileSubscribed(5000),
        initialValue = Result.Loading()
    )
    val anotherStateFlow = anotherFlow.stateIn(
        scope = viewModelScope,
        started = SharingStarted.WhileSubscribed(5000),
        initialValue = Result.Loading()
    )
       
    //...
}

然后收藏在UI:

@Composable
fun SomeScreen() {

    var someUIState: Any? by remember { mutableStateOf(null)}
    var anotherUIState: Any? by remember { mutableStateOf(null)}

    LaunchedEffect(true) {
        lifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
            launch {
                viewModel.someStateFlow.collectLatest {
                    someUIState = it
                }
            }
            launch {
                viewModel.anotherStateFlow.collectLatest {
                    anotherUIState = it
                }
            }
        }
    }
}

2. 一个扩展函数,用于在收集 single StateFlow 作为@Composable 中的状态。这仅在我们有一个单独的 HOT 流不会与 UI 的其他 Screens/parts 共享但仍然需要最新数据时才有用给定时间(像这样用 .stateIn 运算符创建的热流将在后台继续收集,根据 started 的行为会有一些差异范围)。如果冷流足以满足我们的需求,我们可以将 .stateIn 运算符连同 initialscope 参数,但在那种情况下没有那么多样板,我们可能不需要这个扩展函数。


@Composable
fun <T> Flow<T>.flowWithLifecycleStateInAndCollectAsState(
    scope: CoroutineScope,
    initial: T? = null,
    context: CoroutineContext = EmptyCoroutineContext,
): State<T?> {
    val lifecycleOwner = LocalLifecycleOwner.current
    return remember(this, lifecycleOwner) {
        this
            .flowWithLifecycle(
                lifecycleOwner.lifecycle,
                Lifecycle.State.STARTED
             ).stateIn(
                 scope = scope,
                 started = SharingStarted.WhileSubscribed(5000),
                 initialValue = initial
             )
    }.collectAsState(context)
}

这将在@Composable 中像这样使用:

@Composable
fun SomeScreen() {

//...

    val someState = viewModel.someFlow
        .flowWithLifecycleStateInAndCollectAsState(
            scope = viewModel.viewModelScope  //or the composable's scope
        )

    //...
    
}

根据 OP 的回答,如果您不关心 WhileSubscribed(5000) 行为,则可以不通过内部 StateFlow 来 light-weight 多一点。

@Composable
fun <T> Flow<T>.toStateWhenStarted(initialValue: T): State<T> {
    val lifecycleOwner = LocalLifecycleOwner.current
    return produceState(initialValue = initialValue, this, lifecycleOwner) {
        lifecycleOwner.lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) {
            collect { value = it }
        }
    }
}