【问题标题】:How to properly use StateFlow with Jetpack compose?如何正确使用带有 Jetpack compose 的 StateFlow?
【发布时间】:2021-11-09 16:59:31
【问题描述】:

我正在 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 观察数据的正确更好方法是什么?

【问题讨论】:

    标签: android kotlin android-jetpack-compose android-jetpack kotlin-stateflow


    【解决方案1】:

    如果您只想处理每个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
            }
        }
    }
    

    【讨论】:

    • 我在 LaunchedEffect 中收集了一些 UIState。但是,我不知道为什么它弄乱了我的导航。然后我结合@Nestor Perez 的回答。现在它按预期工作。
    【解决方案2】:

    我对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
    }
    

    Event 类最初是为 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)))
    
    

    【讨论】:

      【解决方案3】:

      虽然您可以使用 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!")
              }
          }
      }
      

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2022-08-18
        • 1970-01-01
        • 2023-01-14
        • 2023-02-26
        • 2021-12-17
        • 2023-01-18
        相关资源
        最近更新 更多