如何在 Jetpack Compose 中正确使用 StateFlow?

How to properly use StateFlow with Jetpack compose?

我正在 ViewModel 中执行 API 调用并在可组合项中观察它,如下所示:

class FancyViewModel(): ViewModel(){
 private val _someUIState =
     MutableStateFlow<FancyWrapper>(FancyWrapper.Nothing)
 val someUIState: StateFlow<FancyWrapper> =
     _someUIState

 fun attemptAPICall() = viewModelScope.launch {
  _someUIState.value = FancyWrapper.Loading
  when(val res = doAPICall()){
   is APIWrapper.Success -> _someUIState.value = FancyWrapper.Loading(res.vaue.data)
   is APIWrapper.Error -> _someUIState.value = FancyWrapper.Error("Error!")
  }
 }
}

在组合中,我正在听 'someUIState' 这样的:

@Composable
fun FancyUI(viewModel: FancyViewModel){

 val showProgress by remember {
    mutableStateOf(false)
 }
 val openDialog = remember { mutableStateOf(false) }

 val someUIState =
    viewModel.someUIState.collectAsState()
 
 when(val res = someUIState.value){
  is FancyWrapper.Loading-> showProgress = true
  is FancyWrapper.Success-> {
     showProgress = false
     if(res.value.error)
      openDialog.value = true
     else
     navController.navigate(Screen.OtherScreen.route)
    }
  is FancyWrapper.Error-> showProgress = false
 }

 if (openDialog.value){
  AlertDialog(
   ..
  )
 }

 Scaffold(
  topBar = {
   Button(onClick={viewModel.attemptAPICall()}){
    if(showProgress)
     CircularProgressIndicator()
    else
     Text("Click")
    }
   }
 ){
  SomeUI()
 }

}

我面临的问题是 FancyUI 可组合项中某些 UIState 的 'when' 块代码在可组合项重组期间被多次触发,即使没有单击脚手架中的按钮(例如:当 AlertDialog 出现时)。我哪里做错了?在 Composable 中使用 StateFlow 观察数据的正确更好方法是什么?

如果你只想处理每个 someUIState 值一次,你应该把它放在一个 LaunchedEffect 中并传递 someUIState 作为键,这样每当它改变时块就会被重新触发.

val someUIState by viewModel.someUIState.collectAsState()
LaunchedEffect(someUiState) {
    when(someUiState) {
        // Same as in the question
    }
}

或者,您可以只在 LaunchedEffect.

中收集流量
LaunchedEffect(Unit) {
    viewModel.someUIState.collect { uiState -> 
        when(uiState) {
            // Same as in the question
        }
    }
}

虽然您可以使用 Arpit 提供的解决方案,但我个人更喜欢在视图模型中管理 API 调用的状态。很容易滥用 LaunchEffect。此外,LaunchEffect - 在我看来 - 确实应该是 UI 相关的东西,而不是用于处理对某些后端的 API 调用。由于您已经有一个用于处理状态的变量 - someUIState - 仅当状态设置为 Nothing 时才进行 API 调用。像这样:

class FancyViewModel() : ViewModel() {
    private val _someUIState = MutableStateFlow<FancyWrapper>(FancyWrapper.Nothing)
    val someUIState: StateFlow<FancyWrapper> = _someUIState

    fun attemptAPICall() = viewModelScope.launch {
        if (_someUIState.value != FancyWrapper.Nothing) {
            return
        }
        
        _someUIState.value = FancyWrapper.Loading
        
        when (val res = doAPICall()) {
            is APIWrapper.Success -> _someUIState.value = FancyWrapper.Loading(res.vaue.data)
            is APIWrapper.Error -> _someUIState.value = FancyWrapper.Error("Error!")
        }
    }
}

我对小吃店做的其他解决方案是通知视图模型数据已被消耗: 在你的 FancyUI 中:

...
when(val res = someUIState.value){
  is FancyWrapper.Loading-> showProgress = true
  is FancyWrapper.Success-> {
    ...
    viewModel.onResultConsumed()
  }
  is FancyWrapper.Error-> showProgress = false
 }
...

并且在您的视图模型中:

class FancyViewModel() : ViewModel() {
    private val _someUIState = MutableStateFlow<FancyWrapper>(FancyWrapper.Nothing)
    ...

    fun onResultConsumed() {
       _someUIState.tryEmit(FancyWrapper.Nothing)
    }
}

编辑

如果有人还在寻找这个,这里有另一个解决方案:

创建事件class:

/*
 * Copyright 2017, The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

/**
 * Used as a wrapper for data that is exposed via a LiveData that represents an event.
 */
open class Event<out T>(private val content: T) {

    var hasBeenHandled = false
        private set // Allow external read but not write

    /**
     * Returns the content and prevents its use again.
     */
    fun getContentIfNotHandled(): T? {
        return if (hasBeenHandled) {
            null
        } else {
            hasBeenHandled = true
            content
        }
    }

    /**
     * Returns the content, even if it's already been handled.
     */
    fun peekContent(): T = content
}

事件class最初是为LiveData创建的,但在Flow上工作得很好,如果已经被消费,事件的价值将是bull,这样你可以保存对视图模型的调用。

在您的屏幕中使用它:

...
when(val res = someUIState.value){
  is FancyWrapper.Loading-> showProgress = true
  is FancyWrapper.Success-> {
    res.event.getContentIfNotHandled?.let {
      //do stuff here
      ...
    }
  }
  is FancyWrapper.Error-> showProgress = false
 }
...

要在视图模型中使用,您只需为要显示的状态创建一个事件,例如:

_someUIState.tryEmit(FancyWrapper.Success(event = Event(data)))