简体   繁体   English

Android 协程和 LiveData

[英]Android Coroutines and LiveData

I would like to know if the code I have to use Coroutines and LiveData on an Android application is correct or if there is something wrong with it.我想知道我必须在 Android 应用程序上使用 Coroutines 和 LiveData 的代码是否正确,或者它是否有问题。

Take as an example an UserProfile fragment and an EditUserBio fragment.以 UserProfile 片段和 EditUserBio 片段为例。 I have a UserProfileViewModel scoped to these 2 fragments.我有一个范围为这两个片段的 UserProfileViewModel。

My UserProfileViewModel is something like:我的 UserProfileViewModel 类似于:

class UserProfileViewModel : ViewModel() {
    private val _loading = MutableLiveData<Boolean>()
    val loading: LiveData<Boolean> get() = _loading

    private val _errorMessage = MutableLiveData<String>()
    val errorMessage: LiveData<String> get() = _errorMessage
    
    private val _user = MutableLiveData<User>()
    val user: LiveData<User> get() = _user
...
}

In both UserProfileFragment and EditUserBioFragment I observe loading and errorMessage LiveData events to show a loading dialog and an error message.在 UserProfileFragment 和 EditUserBioFragment 中,我观察到loadingerrorMessage LiveData 事件以显示加载对话框和错误消息。

In EditUserBioFragment I want to be able to tap on a button, do a network call to update user profile, update cached User and then go back to UserProfileFragment.在 EditUserBioFragment 中,我希望能够点击一个按钮,进行网络调用以更新用户配置文件,更新缓存的用户,然后将 go 返回到 UserProfileFragment。

From my understanding of Use Kotlin coroutines with Architecture components and cortoutines LiveData I can do something like:根据我对Use Kotlin coroutines with Architecture components和 cortoutines LiveData的理解,我可以这样做:

viewModel.updateBio(data).observe(viewLifecycleOwner, {
    Navigation.findNavController(v).popBackStack()
})

and my UserProfileViewModel implementation would be something like我的 UserProfileViewModel 实现将类似于

fun updateBio(profile: UserProfileModel) = liveData {
    withContext(viewModelScope.coroutineContext) {
        _loading.postValue(true)
        try {
            /* suspend*/ userRepository.updateProfile(profile)
            emit(Void.TYPE)
        } catch (e : Exception) {
            _errorMessage.postValue(e.localizedMessage)
        } finally {
            _loading.postValue(false)
        }
    }
}

Is this a correct usage?这是正确的用法吗?

If userRepository.updateProfile(profile) throws are there any problems in:如果userRepository.updateProfile(profile)抛出有任何问题:

  1. not calling emit ?不调用emit
  2. or calling viewModel.updateBio(data).observe( multiple times?或多次调用viewModel.updateBio(data).observe(

I suggest you take a look at coroutines best practices by Google .我建议你看看Google 的协程最佳实践

I would not do (from a UI/Fragment/Activity) viewModel.someOperation(xxx).observe(...) all the time, I think it makes the code harder to follow.我不会(从 UI/Fragment/Activity) viewModel.someOperation(xxx).observe(...)一直这样做,我认为这会使代码更难理解。 You have all these LiveData exposed and have to make sure things happen in the correct stream.您已经公开了所有这些 LiveData,并且必须确保事情发生在正确的 stream 中。

I would instead do with one (or maybe two if you want to split "navigation") state:相反,我会使用一个(或者如果你想拆分“导航”,可能是两个)state:

viewModel.state.observe(viewLifecycleOwner) { state ->
            when (state) {
              is X -> ...
              is Y -> ...
  

you get the idea.你明白了。

In your VM, "state" is what you guessed:在您的虚拟机中,“状态”是您猜到的:

    private val _state = MutableLiveData<SomeState>(SomeState.DefaultState)
    val state: LiveData<SomeState> = _state

This has decoupled the UI (Fragment/Act) from the logic of having to deal with a coroutine, a response, etc. Instead, you now do all that inside your ViewModel, which will go the "extra mile" of checking what happened, and emitting a sensible/opinionated "SomeState" that the fragment can react to.这将 UI(片段/动作)与必须处理协程、响应等的逻辑分离。相反,您现在可以在 ViewModel 中执行所有这些操作,这将使 go 检查发生的事情“额外一英里”,并发出片段可以对其做出反应的明智/自以为是的“SomeState”。

So your updateBio fun becomes more..所以你的 updateBio 乐趣变得更多..

fun updateBio(profile: UserProfileModel) {
     viewModelScope.launch {
         _state.postValue(SomeState.Loading)

         // here call your useCase/Repo/etc. which is suspend.
         // evaluate the response, wait for it, do what it takes, and .postValue(SomeState.XXX) based on your logic. 

Now userRepository.updateProfile(profile) could return a concrete type, like returns the newly updated Profile directly or it could expose a sealed class UserProfileState {} with various "states" so the ViewModel would:现在userRepository.updateProfile(profile)可以返回一个具体类型,例如直接返回新更新的配置文件,或者它可以公开具有各种“状态”的sealed class UserProfileState {} ,因此 ViewModel 将:

sealed class UserProfileState {
   data class UpdatedOk(val profile: UserProfileModel) : UserProfileState()
   data class Failure(val exception: Exception) : UserProfileState()
}

And you'd then...然后你会...

when (val response = userRepo.update(profile)) {
    is UserProfileState.UpdatedOk -> _state.postValue(SomeState.Updated(response.profile))
    is Failure -> //deal with it
}

I hope you get the idea.我希望你能明白。 In the end, if the userProfile fails to update the bio, if you use a "UserProfileState" you can signal with .Failure .最后,如果 userProfile 未能更新 bio,如果您使用“UserProfileState”,您可以使用.Failure发出信号。 Or if it just returns a "UserProfileModel" you have to decide if you want to raise an exception or return Null and have the ViewModel decide what to do.或者,如果它只返回“UserProfileModel”,您必须决定是要引发异常还是返回 Null 并让 ViewModel 决定要做什么。 It really depends what kind of interaction you need.这真的取决于你需要什么样的互动。

I did find an issue with the ViewModel being scoped.我确实发现 ViewModel 的范围存在问题。

I end up creating a我最终创建了一个

sealed class LiveDataResult<out R> {
    data class Loading(val show: Boolean) : LiveDataResult<Nothing>()
    data class Result<out T>(val value: T) : LiveDataResult<T>()
    data class Error(val message: String?) : LiveDataResult<Nothing>()
}

and changing updateBio to emit LiveDataResult并更改updateBio以发出 LiveDataResult

fun updateBio(profile: UpdateProfileRequestModel) = liveData {
    withContext(viewModelScope.coroutineContext) {
        emit(LiveDataResult.Loading(true))
        try {
            userRepository.updateProfile(profile)
            emit(LiveDataResult.Result(true))
        } catch (e : Exception) {
            emit(LiveDataResult.Error(e.localizedMessage))
        } finally {
            emit(LiveDataResult.Loading(false))
        }
    }
}

I guess this is a better solution than single val loading: LiveData<Boolean> and val errorMessage: LiveData<String> for multiple calls.我想这是比单个val loading: LiveData<Boolean>val errorMessage: LiveData<String>用于多个调用。

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

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