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

Posted

技术标签:

【中文标题】ui中再次收集StateFlow最后一个值【英文标题】:StateFlow last value is collected again in ui 【发布时间】:2021-09-17 07:27:19 【问题描述】:

最近我一直在使用 StateFlow、SharedFlow 和 Channels API,但在尝试将我的代码从 LiveData 迁移到表示层中的 StateFlow 时,我遇到了一个常见的用例。

我面临的问题是,当我发出数据并在 viewModel 中收集数据时,我可以将值设置为 mutableStateFlow,当它最终到达片段时,它使用 Toast 显示一些信息性消息让用户知道是否发生错误或一切正常。接下来,有一个按钮可以导航到另一个片段,但是如果我返回到已经有失败意图的结果的上一个屏幕,它会再次显示 Toast。这正是我想要弄清楚的。如果我已经收集了结果并将消息显示给用户,我不想继续这样做。如果我导航到另一个屏幕并返回(当应用程序从后台返回时也会发生这种情况,它会再次收集最后一个值)。 LiveData 没有出现这个问题,我只是做了完全相同的事情,从存储库公开一个流并通过 ViewModel 中的 LiveData 收集。

代码:

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
            
        
    
    

然后在我的片段中:

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 扩展功能:

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

对于为什么会发生这种情况以及如何使用 StateFlow 解决它有什么想法吗?我也尝试使用带有 replay = 0 的 SharedFlow 和带有 receiveAsFlow() 的 Channels,但随后出现了其他问题。

【问题讨论】:

这是 StateFlow 的预期行为。做你所描述的正确方法是使用 SharedFlow 和 replay 0。也许你可以描述这样做会出现什么其他问题。 你能找到答案吗?我遇到了这个问题,也许使用 livedata 更好。对于这个问题,我在互联网上找不到任何答案。 【参考方案1】:

您似乎正在寻找带有 Kotlin 流的 SingleLiveEvent。

` 类 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"))
    

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

`

致谢:Michael Ferguson 撰写了一篇很棒的文章,其中包含更新的库增强功能。建议你通过它。我已经复制了它的摘录。

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

【讨论】:

以上是关于ui中再次收集StateFlow最后一个值的主要内容,如果未能解决你的问题,请参考以下文章

收集 StateFlow 时 Api 没有响应不会更改 kotlin

Kotlin Flow 冷流 StateFlow 热流 StateFlow 的应用

ViewModel中的StateFlow和SharedFlow,使用建议以及单元测试

ViewModel中的StateFlow和SharedFlow,使用建议以及单元测试

爬虫:Scrapy12 - Stats Collection

第三百五十四节,Python分布式爬虫打造搜索引擎Scrapy精讲—数据收集(Stats Collection)