4

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

我面临的问题是,当我发出数据并在 viewModel 中收集它时,我可以将值设置为 mutableStateFlow,当它最终到达片段时,它使用 Toast 显示一些信息性消息,让用户知道是否有错误发生或一切顺利。接下来,有一个按钮可以导航到另一个片段,但是如果我返回到已经有失败意图的结果的上一个屏幕,它会再次显示 Toast。这正是我想要弄清楚的。如果我已经收集了结果并将消息显示给用户,我不想继续这样做。如果我导航到另一个屏幕并返回(当应用程序从后台返回时也会发生这种情况,它会再次收集最后一个值)。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,但随后出现了其他问题。

4

2 回答 2

2

您可以像这样创建扩展函数:

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

它在发出新值后设置所需的默认值。就我而言,我使用 null 作为所需的值。

在 ViewModel 内部,您可以使用set()扩展函数,而不是直接设置值。

fun signIn() = authRepository.signIn(phoneNumber.value).onEach {
    _signInState.set(it)
}.launchIn(viewModelScope)
于 2022-01-15T13:07:55.797 回答
0

看起来您正在寻找带有 Kotlin 流的 SingleLiveEvent。

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() {

    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

于 2021-12-01T15:38:30.570 回答