简体   繁体   English

StateFlow 最后一个值在 ui 中再次收集

[英]StateFlow last value is collected again in ui

So lately I've been working with StateFlow, SharedFlow, and Channels API's but I'm struggling with one common use case while trying to migrate my code from LiveData to StateFlow in the presentation layer.所以最近我一直在使用 StateFlow、SharedFlow 和 Channels API,但在尝试将我的代码从 LiveData 迁移到表示层中的 StateFlow 时,我遇到了一个常见用例。

The problem I'm facing is when I emit my data and collect it in viewModel so I can set the value to a mutableStateFlow, when it finally gets to the fragment it shows some informative messages using a Toast to let the user knows whether an error happened or everything went fine.我面临的问题是当我发出数据并在 viewModel 中收集它时,我可以将值设置为 mutableStateFlow,当它最终到达片段时,它使用 Toast 显示一些信息性消息,让用户知道是否有错误发生或一切顺利。 Next, there's a button which navigates to another fragment, but if I go back to the previous screen which already has the result of the failed intent, again it displays the Toast.接下来,有一个导航到另一个片段的按钮,但如果我 go 返回到已经有失败意图结果的上一个屏幕,它会再次显示 Toast。 And that's exactly what I'm trying to figure out.而这正是我想要弄清楚的。 If I collected already the result and showed the message to the user I don't want to keep doing it.如果我已经收集了结果并向用户显示了消息,我不想继续这样做。 If I navigate to another screen and return (it also happens when the app comes back from the background, it collects again the last value).如果我导航到另一个屏幕并返回(当应用程序从后台返回时也会发生这种情况,它会再次收集最后一个值)。 This problem didn't happen with LiveData where I just did exact same thing, expose a flow from a repository and collected via LiveData in ViewModel.这个问题没有发生在 LiveData 上,我只是做了完全相同的事情,从存储库公开流并通过 ViewModel 中的 LiveData 收集。

Code:代码:

class SignInViewModel @Inject constructor(
    private val doSignIn: SigninUseCase
) : ViewModel(){

    private val _userResult = MutableStateFlow<Result<String>?>(null)
    val userResult: StateFlow<Result<String>?> = _userResult.stateIn(viewModelScope, SharingStarted.Lazily, null) //Lazily since it's just one shot operation

    fun authenticate(email: String, password: String) {
        viewModelScope.launch {
            doSignIn(LoginParams(email, password)).collect { result ->
                Timber.e("I just received this $result in viewmodel")
                _userResult.value = result
            }
        }
    }
    
}

Then in my Fragment:然后在我的片段中:

override fun onViewCreated(...){
super.onViewCreated(...)

launchAndRepeatWithViewLifecycle {
            viewModel.userResult.collect { result ->
                when(result) {
                    is Result.Success -> {
                        Timber.e("user with code:${result.data} logged in")
                        shouldShowLoading(false)
                        findNavController().navigate(SignInFragmentDirections.toHome())
                    }
                    is Result.Loading -> {
                        shouldShowLoading(true)
                    }
                    is Result.Error -> {
                        Timber.e("error: ${result.exception}")
                        if(result.exception is Failure.ApiFailure.BadRequestError){
                            Timber.e(result.exception.message)
                            shortToast("credentials don't match")
                        } else {
                            shortToast(result.exception.toString())
                        }

                        shouldShowLoading(false)
                    }
                }
            }
}

launchAndRepeatWithViewLifecycle extension function: launchAndRepeatWithViewLifecycle 扩展 function:

inline fun Fragment.launchAndRepeatWithViewLifecycle(
    minActiveState: Lifecycle.State = Lifecycle.State.STARTED,
    crossinline block: suspend CoroutineScope.() -> Unit
) {
    viewLifecycleOwner.lifecycleScope.launch {
        viewLifecycleOwner.lifecycle.repeatOnLifecycle(minActiveState) {
            block()
        }
    }
}

Any thoughts on why this happens and how to fancy solve it using StateFlow?关于为什么会发生这种情况以及如何使用 StateFlow 解决它有什么想法吗? I tried also with SharedFlow with replay = 0 and Channels with receiveAsFlow() but then other problems arise.我也尝试过使用带有 replay = 0 的 SharedFlow 和带有 receiveAsFlow() 的 Channels,但随后出现了其他问题。

It looks like you would be looking for SingleLiveEvent with Kotlin flow.看起来您正在寻找带有 Kotlin 流程的 SingleLiveEvent。

` class MainViewModel : ViewModel() { ` class MainViewModel : ViewModel() {

sealed class Event {
    data class ShowSnackBar(val text: String): Event()
    data class ShowToast(val text: String): Event()
}

private val eventChannel = Channel<Event>(Channel.BUFFERED)
val eventsFlow = eventChannel.receiveAsFlow()

init {
    viewModelScope.launch {
        eventChannel.send(Event.ShowSnackBar("Sample"))
        eventChannel.send(Event.ShowToast("Toast"))
    }
}

} }

class MainFragment : Fragment() {类 MainFragment : Fragment() {

companion object {
    fun newInstance() = MainFragment()
}

private val viewModel by viewModels<MainViewModel>()
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
    return inflater.inflate(R.layout.main_fragment, container, false)
}

override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
    super.onViewCreated(view, savedInstanceState)
    // Note that I've chosen to observe in the tighter view lifecycle here.
    // This will potentially recreate an observer and cancel it as the
    // fragment goes from onViewCreated through to onDestroyView and possibly
    // back to onViewCreated. You may wish to use the "main" lifecycle owner
    // instead. If that is the case you'll need to observe in onCreate with the
    // correct lifecycle.
    viewModel.eventsFlow
        .onEach {
            when (it) {
                is MainViewModel.Event.ShowSnackBar -> {}
                is MainViewModel.Event.ShowToast -> {}
            }
        }
        .flowWithLifecycle(lifecycle = viewLifecycleOwner.lifecycle, minActiveState = Lifecycle.State.STARTED)
        .onEach {
            // Do things
        }
        .launchIn(viewLifecycleOwner.lifecycleScope)
}

} }

` `

Credit : Michael Ferguson has authored a great article with the updated library enhancement.信用:迈克尔·弗格森 (Michael Ferguson) 撰写了一篇很棒的文章,其中包含更新的库增强功能。 Would recommend you to go through it.会建议你通过它。 I have copied the extract of it.我已经复制了它的摘录。

https://proandroiddev.com/android-singleliveevent-redux-with-kotlin-flow-b755c70bb055

You can create an extension function like this:您可以像这样创建扩展函数:

fun <T> MutableStateFlow<T?>.set(value: T, default: T? = null) {
    this.value = value
    this.value = default
}

It sets the desired default value after the new value has been emitted.它在发出新值后设置所需的默认值。 In my case, I use null as the desired value.就我而言,我使用 null 作为所需的值。

Inside ViewModel instead of setting the value directly you can use the set() extension function.在 ViewModel 内部,您可以使用set()扩展函数,而不是直接设置值。

fun signIn() = authRepository.signIn(phoneNumber.value).onEach {
    _signInState.set(it)
}.launchIn(viewModelScope)

You can set your state's value to the Idle(), which means an empty state您可以将您的状态值设置为 Idle(),这意味着一个空的 state

lifecycleScope.launch {
           viewModel.signInState.collect {
               when (it) {
                   is ViewState.Success -> {
                       stopLoading()
                       viewModel._signInState.value = ViewState.Idle()
                       // do your tasks here
                   }
                   is ViewState.Error<*> -> requireActivity().onBackPressedDispatcher.onBackPressed()
                   is ViewState.BusinessException<*> -> requireActivity().onBackPressedDispatcher.onBackPressed()
                   is ViewState.Idle<*> -> stopLoading()
                   is ViewState.Loading<*> -> startLoading()
               }
           }
       }

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM