Rui Rodrigues
Rui Rodrigues

Reputation: 413

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.

Take as an example an UserProfile fragment and an EditUserBio fragment. I have a UserProfileViewModel scoped to these 2 fragments.

My UserProfileViewModel is something like:

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.

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.

From my understanding of Use Kotlin coroutines with Architecture components and cortoutines LiveData I can do something like:

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

and my UserProfileViewModel implementation would be something like

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:

  1. not calling emit?
  2. or calling viewModel.updateBio(data).observe( multiple times?

Upvotes: 0

Views: 2106

Answers (2)

Martin Marconcini
Martin Marconcini

Reputation: 27226

I suggest you take a look at coroutines best practices by 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. You have all these LiveData exposed and have to make sure things happen in the correct stream.

I would instead do with one (or maybe two if you want to split "navigation") 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.

So your updateBio fun becomes more..

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:

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. 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. It really depends what kind of interaction you need.

Upvotes: 1

Rui Rodrigues
Rui Rodrigues

Reputation: 413

I did find an issue with the ViewModel being scoped.

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

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.

Upvotes: 0

Related Questions