Advanced Usage

Let us continue with the example described in the Basics page.

Dependency Injection support

Our ViewModel needs access to a Repository or a Use-Case class in order to be able to fetch the user's notes. In the Usage section, we setup the repository as a singleton Kotlin object, so that we could access it without creating an instance of it.

This is rarely the case in real applications, and it is much more common to split your business logic into different Use-Case classes. Let us create a GetNotesUseCase class for our notes app:

class GetNotesUseCase (private val repository: NotesRepository) {
  fun getAllNotes() { ... }
  fun getPinnedNotes() { ... }
  fun getArchivedNotes() { ... }
}

Our ViewModel now looks like this:

class NotesListViewModel(
  initialState: NotesListState,
  private val notesUseCase: GetNotesUseCase
): VectorViewModel<NotesListState>(initialState) {
  ...

  suspend fun getNotes() { ... }

  suspend fun getAllNotes() {
    val allNotes = notesUseCase.getAllNotes()
    setState { copy(notes = allNotes) }
  }
  suspend fun getPinnedNotes() { ... }
  suspend fun getArchivedNotes() { ... }
}

Due to the addition of an additional dependency in the constructor which Vector can not satisfy on its own, we can not instantiate this ViewModel in our Fragment or Activity in the way we did before.

We need to tell Vector how to satisfy this ViewModel's dependencies using a VectorViewModelFactory, implemented in this ViewModel's companion object.

The VectorViewModelFactory interface has a method named create(), which is used to create and return an instance of this ViewModel. The create() method is supplied with a ViewModelOwner parameter, which is a wrapper around the Fragment/Activity owning this ViewModel. It can be used to get access to your dependency injection library's object graph. You must not store a reference to this owner in your ViewModel, or you will create a memory leak for your Activity or Fragment.

class NotesListViewModel(
  initialState: NotesListState,
  private val notesUseCase: GetNotesUseCase
): VectorViewModel<NotesListState>(initialState) {
  ...

  companion object: VectorViewModelFactory<NotesListViewModel, NotesListState> {
    fun create(initialState: NotesListState, owner: ViewModelOwner, handle: SavedStateHandle): NotesListViewModel? { 
      val usecase = // use the ViewModelOwner parameter to access DI graph and get GetNotesUseCase
      return NotesListViewmodel(initialState, usecase)
    }
  }
}

With this factory now implemented, we can get access to our ViewModel again using view model delegates such as by fragmentViewModel(). Vector will lookup the factory automatically, and use it to instantiate your ViewModel.

Koin

If you are using Koin, the create() method in a VectorViewModelFactory looks like this:

fun create(initialState: NotesListState, owner: ViewModelOwner, handle: SavedStateHandle): NotesListViewModel? {
  val usecase = when (owner) {
    is FragmentViewModelOwner -> owner.fragment.get<GetNotesUseCase>() // `get` extension method from Koin
    is ActivityViewModelOwner -> owner.activity.get<GetNotesUseCase>()
  }
  return NotesListViewmodel(initialState, usecase)
}

Dagger and AssistedInject

Dagger can generate your DI graph at compile time, but it can not handle constructor paramters only available at run-time such as the initialState parameter in our ViewModel. The AssistedInject library helps with this, as it automatically generates factories for classes which depend on such runtime parameters.

Usage of AssistedInject and Dagger in Vector looks like this:

class NotesListViewModel @AssistedInject constructor(
  @Assisted initialState: NotesListState,
  private val usecase: GetNotesUseCase
) {

  ...

  @AssistedInject.Factory
  interface Factory {
    fun create(initialState: NotesListState): NotesListViewModel
  }
}

class NotesListFragment: VectorFragment() {
  @Inject
  lateinit var viewModelFactory: NotesListViewModel.Factory

  private val viewModel: NotesListViewModel by fragmentViewModel { initialState, savedStateHandle ->
    viewModelFactory.create(initialState)
  }

  override fun onCreate(...) {
    inject()
    super.onCreate(...)
  }
}

We leverage another ViewModel delegate supplied by Vector which accepts a ViewModel producing lambda as an input. When this lambda is supplied, you don't need to implement the VectorViewModelFactory interface just to create your ViewModel with custom dependencies.

Handling process death

What's process death?

When Android kills your application process while it is in the background, we call it process death. Before your application is killed, onSaveInstanceState is called for your Activities/Fragments, which can persist their state in a Bundle. When your application is recreated after a Process Death, you receive this bundle back as an argument as savedInstanceState in the onCreate() method.

Doesn't a ViewModel handle this automatically?

No, a ViewModel is built to handle configuration changes, such as rotations or locale changes. It does not survive process death. Any state held by the ViewModel is lost after a process death, and is not recoverable.

The ViewModel is meant to handle state while in-memory, and the owning Fragment/Activity is supposed to save relevant parts of this state when being killed. You need to use both of these together to correctly handle state restoration.

Problems with onSaveInstanceState

This setup works okay-ish when your application is small, or when the state is not complex. The biggest problem with this setup is that it also makes the Activity or Fragment responsible for managing state. Besides, it is difficult to implement this method correctly, and the user must also remember to extract the saved state from the savedInstanceState bundle.

Vector's solution

The AndroidX ViewModel-SavedState library helps with this problem. Vector builds on this library and provides a SavedStateVectorViewModel, which uses the SavedStateHandle object to help with state persistence.

class NotesListViewModel @AssistedInject constructor(
  @Assisted initialState: NotesListState,
  @Assisted handle: SavedStateHandle,
  private val notesUse: GetNotesUseCase
): SavedStateVectorViewModel(intitialState, savedStatehandle = handle)

The SavedStateVectorVieWModel has additional methods to help with state persistence: setStateAndPersist and persistState. To use them, we need to make sure that our state class implements the Parcelable interface. We can use the @Parcelize annotation from Kotlin-Android-Extensions to automatically generate the implementation for us.

@Parcelize
data class NotesListState(...): Parcelable

Persisting state

To persist state, we can simply replace the usage of setState in our ViewModel with setStateAndPersist to make sure that we save the latest state whenever it is modified. This way, whenever the application process is killed we would have already persisted the latest state.

The setStateAndPersist method first invokes the state reducer, and then calls persistState(). The default implementation of persistState() simply takes the state object, and saves it to the SavedStateHandle using the key KEY_SAVED_SAVED defined in the companion object of SavedStateVectorViewModel.

To customize this behaviour, you can override the persistState() method and provide your own implementation.

Restoring state

To restore the state, the initialState method of VectorViewModelFactory must be implemented in the ViewModel's companion object:

fun initialState(handle: SavedStateHandle, owner: ViewModelOwner): NotesListState? {
  val persistedState = handle[KEY_SAVED_STATE]
  if (persistedState != null) {
    return persistedState
  } else {
    ...
  }
}