Skip to main content
Version: Next

ViewModel

The artifact I hope you don't need, but if you're not doing dependency injection, you probably do.

Examples

import dispatch.android.*

// DispatchViewModel is just a ViewModel with a lazy viewModelScope
class SomeViewModel : DispatchViewModel() {
// ...

init {

// auto-creates a MainImmediateCoroutineScope which is closed in onCleared()
viewModelScope. //...

// multiple invocations use the same instance
viewModelScope.launch { }

// it works as a normal CoroutineScope (because it is)
viewModelScope.launchMain { }

}
}

class SomeApplication : Application() {
override fun onCreate() {
super.onCreate()
// A custom factory can be set to add elements to the CoroutineContext
ViewModelScopeFactory.set { MainImmediateCoroutineScope() + SomeCustomElement() }
}
}

class SomeViewModelTest {

val viewModel = SomeViewModel()

@Before
fun setUp() {
// This custom factory can be used to use custom scopes for testing
ViewModelScopeFactory.set { TestProvidedCoroutineScope() }
}

@After
fun tearDown() {
// The factory can also be reset to default
ViewModelScopeFactory.reset()
}

@Test
fun someTest() = runBlocking {
// the AndroidX version is public, so it's public here as well.
viewModel.viewModelScope.launch { }
}
}

Difference from AndroidX

This module is essentially a fork of androidx-lifecycle-viewmodel-ktx — the library which gives us the viewModelScope property.

It exists entirely so that we can have a settable factory. This gives us a lot more options for JVM or instrumented tests, with custom dispatchers or other custom CoroutineContext elements.

Custom CoroutineScope factories

The way androidx-lifecycle-viewModel constructs its CoroutineScope is hard-coded, which eliminates the possibility of using a custom CoroutineContext such as a DispatcherProvider or IdlingDispatcher. With dispatch-android-lifecycle, we can set a custom factory.

class SomeViewModelTest {

@Before
fun setUp() {
// This custom factory can be used to use custom scopes for testing
ViewModelScopeFactory.set { TestProvidedCoroutineScope() }

// it could also return a specific instance
val someTestScope = TestProvidedCoroutineScope()
ViewModelScopeFactory.set { someTestScope }
}

@After
fun tearDown() {
// The factory can also be reset to default
ViewModelScopeFactory.reset()
}
}

Automatic cancellation in onCleared()

Just like AndroidX, this version of viewModelScope is automatically cancelled in ViewModel.onCleared().

viewModelScope is not lifecycleScope

It's important to remember that onCleared() is only called when a ViewModel is about to be destroyed -- when its associated LifecycleOwner(s) are all destroyed. This means that a viewModelScope is active while the LifecycleOwner is in the backstack.

Consider this example:

// Don't do this
class SomeViewModel : DispatchViewModel() {

init {
viewModelScope.launch {
// this job will continue forever even if the ViewModel is on the backstack.
someRepository.dataFlow.collect {
parseData(it)
}
}
}
}

A CoroutineScope in a ViewModel is better utilized for single-shot requests which shouldn't be restarted in the event of a configuration change. "Observer" behavior should be scoped to the associated view.

// Maybe do this
class SomeFragment : Fragment() {

val viewModel: SomeViewModel by viewModels()

init {
lifecycleScope.launchWhenResumed {
viewModel.dataFlow.collect { }
}
}
}

class SomeViewModel : DispatchViewModel() {

// a single shot request is made using the viewModelScope
val lazyData by lazy {
CompletableDeferred<Data>().apply {
viewModelScope.launch {
complete(someRepository.getData())
}
}
}

// collection of the Flow is done using the view's lifecycleScope,
// meaning that it will stop as soon as the screen is in the backstack
val dataFlow = someRepository.dataFlow.onEach {
parseData(it)
}
}

Extending ViewModel

Since nothing about the clear event is actually exposed outside of ViewModel, it's necessary to extend ViewModel in order to consume it for cancelling the viewModelScope. This is especially galling since ViewModel could absolutely have just been an interface to begin with.

Minimum Gradle Config

Add to your module's build.gradle.kts:

repositories {
mavenCentral()
}

dependencies {

implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.5.2")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.5.2")
implementation("com.rickbusarow.dispatch:dispatch-android-viewmodel:1.0.0-beta10-SNAPSHOT")
}