dispatch-android-viewmodel
The artifact I hope you don't need, but if you're not doing dependency injection, you probably do.
Contents
#examples
#difference-from-androidx
#custom-coroutinescope-factories
#automatic-cancellation-in-oncleared
#viewmodelscope-is-not-lifecyclescope
#extending-viewmodel
#minimum-gradle-config
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.0")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.5.0")
implementation(platform("com.rickbusarow.dispatch:dispatch-bom:1.0.0-beta10"))
implementation("com.rickbusarow.dispatch:dispatch-android-viewmodel")
}