1

我正在 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 可组合中 someUIState 的“何时”块代码在可组合重组期间被多次触发,即使没有单击 Scaffold 中的按钮(例如:当 AlertDialog 出现时)。我在哪里做错了?在 Composable 中使用 StateFlow 观察数据的正确更好方法是什么?

4

3 回答 3

2

如果您只想处理每个someUIState值一次,则应将其放在 a 中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
        }
    }
}
于 2021-11-09T17:09:55.297 回答
1

我对snackbars 所做的其他解决方案是通知视图模型数据已被使用:在您的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)
    }
}

编辑

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

创建事件类:

/*
 * 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
}

事件类最初是为 LiveData 创建的,但可以与 Flow 一起正常工作,如果已经使用,事件的值将是公牛,这样您可以保存对视图模型的调用。

在您的屏幕中使用它:

...
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)))

于 2021-11-10T03:26:42.967 回答
0

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

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!")
        }
    }
}
于 2021-11-09T17:34:22.687 回答