Contents

Stateful MVVM across process death using SharedFlow

/images/stateful_mvvm_image.jpg

onCreate

Lately, I have been playing and exploring SharedFlow. A pretty nice and very helpful design for situations like this. But as I was building a ViewModel this week, I ran into an architecture design dilema.

onPause

Disclamer: The solution given below, might not be the best out there, so please feel free to give your thoughts and critics for this.

onResume

Let’s consider this quick example:

class SomeViewModel @ViewModelInject constructor(
   private val dependency1: Dependency1,
   private val dependency2 : Dependency2,
   @Assisted private val savedStateHandle: SavedStateHandle
) : ViewModel() {
  private val _state : MutableSharedFlow<State> = MutableSharedFlow()
  val state : SharedFlow<State> = _state.asSharedFlow()

  sealed class State {
      data class Success(val data: List<Whatever>) : State()
      data class Fail(val message: String?) : State()
  }
}

This is how I normally manage it. But what if I want to save my data to survive process death? Adding a new extra variable to observe the savedStateHandle is error prone and actually makes things way worse, introducing a lot of bugs. But I really wanted to use SharedFlow, because of its’ flexibility regarding single events which eventually make live easier. Also, with the help of Hilt, there is not much to be done when it comes to creating a ViewModel anymore, so the savedStateHandle comes for free.

Therefore, I was thinking: How about, store the whole state in saveStateHandle? It would restore immediately what we had before process death. What I needed to do though, was kick out 2 state SharedFlow variables:

class SomeViewModel @ViewModelInject constructor(
   private val dependency1: Dependency1,
   private val dependency2 : Dependency2,
   @Assisted private val savedStateHandle: SavedStateHandle
) : ViewModel() {

  val state = savedStateHandle.getLiveData<State>(Key)

  sealed class State {
      data class Success(val data: List<Whatever>) : State()
      data class Fail(val message: String?) : State()
  }
}

Ok, so are we back to LiveData again? Well, not really. As I hope that in the future, Google team might introduce a direct Flow extention from savedStateHandle, I was planning to convert it to a Flow first, with the help of androidx.lifecycle:lifecycle-livedata-ktx:2.2.0. So it becomes:

val state = savedStateHandle.getLiveData<State>(Key).asFlow()

With a small modification, since I need a hot SharedFlow instead:

val state = savedStateHandle.getLiveData<State>(Key).asFlow().shareIn(viewModelScope, SharingStarted.Lazily)

Sometimes, depending on LiveData for such implementation, might sound a little hacky, but it seems that’s the only option to rely to (atm) in order to receive a hot sharedFlow which also survives process death.

One small detail that must not be missed is the fact that the State in this case must implement Parcelable. Whith the help of kotlin-android it can be easily tackled:

 sealed class State : Parcelable {
        @Parcelize
        data class Success(val data: List<Whatever>) : State()

        @Parcelize
        data class Fail(val message: String?) : State()
    }

All we would need to do now, is just keep setting the value of savedStateHandle according to our use case:

class SomeViewModel @ViewModelInject constructor(
   private val dependency1: Dependency1,
   private val dependency2 : Dependency2,
   @Assisted private val savedStateHandle: SavedStateHandle
) : ViewModel() {

  val state = savedStateHandle.getLiveData<State>(Key).asFlow().shareIn(viewModelScope, SharingStarted.Lazily)

  sealed class State : Parcelable {
        @Parcelize
        data class Success(val data: List<Whatever>) : State()

        @Parcelize
        data class Fail(val message: String?) : State()
    }

    fun doSomething(){
       val response =  dependency1.request()
       if(response.isSuccess) savedStateHandle.set(Key, State.Success(response.data))
       else savedStateHandle.set(Key, State.Fail(response.errorMessage))
    }

    companion object {
        const val Key = "saved_state_key"
    }
}

onDestroy

Handling process death is still debatable in terms of: “Do we always need it?”. In my opinion, it has its’ importance, but sometimes it is too much. However, when choosing not to ignore this topic, it really brings a little more time to think and complexity regarding the design, so I was hoping I brought something which would make it simpler in case your project is already fully relied on Flow, coroutines API, and perhaps one of you might already made a decision to kick LiveData out.