Skip to content

Files

Latest commit

 

History

History

dispatch-android-viewmodel

Folders and files

NameName
Last commit message
Last commit date

parent directory

..
 
 
 
 
 
 
 
 
 
 
 
 

Module dispatch-android-viewmodel

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

Contents

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")
}