When dealing with coroutines inside a viewModel is best to have said viewModel implement CoroutineScope
so all coroutines are cancelled when the viewModel is cleared. Usually I see coroutineContext
defined as Dispatchers.Main + _job
so that coroutines are executed in the main UI thread by default. Usually this is done on a open class so that all your viewModels can extend it and get the scope without boilerplate code.
The issue arises when trying to unit test said viewModels as Dispatchers.Main
is not available and trying to use it throws an exception. I am tryin to find a good solution that doesn't involve external libraries or too much boiler plate on the child viewModels.
My current solution is to add the maincontext as a contructor paramenter with the Dispatchers.Main
as the default value. Then in the unit test, before testing the viewModel I set it to Dispatchers.Default
. I don't quiet like this solution as it exposes the coroutineContext implementation details for everyone to see and change:
open class ScopedViewModel(var maincontext = Dispatchers.Main) : ViewModel(), CoroutineScope {
private val _job = Job()
override val coroutineContext: CoroutineContext
get() = maincontext + _job
override fun onCleared() {
super.onCleared()
_job.cancel()
}
}
class MyViewModel : ScopedViewModel() {}
In the tests:
fun setup(){
viewModel = MyViewModel()
viewModel.maincontext = Dispacther.Default
}
Personally I copied a solution from RxJava2: if your test runs against RxJava2 flow which includes two or more different schedulers, you want, sure, all of them to actually run in a single thread. Here is how it is done with RxJava2 testing:
@BeforeClass
public static void prepare() {
RxJavaPlugins.setComputationSchedulerHandler(scheduler -> Schedulers.trampoline());
RxJavaPlugins.setIoSchedulerHandler(scheduler -> Schedulers.trampoline());
RxJavaPlugins.setNewThreadSchedulerHandler(scheduler -> Schedulers.trampoline());
RxJavaPlugins.setSingleSchedulerHandler(scheduler -> Schedulers.trampoline());
RxAndroidPlugins.setMainThreadSchedulerHandler(scheduler -> Schedulers.trampoline());
}
I did the same for coroutines. Just have created a class which collects dispatchers, but these dispatchers can be changed.
object ConfigurableDispatchers {
@JvmStatic
@Volatile
var Default: CoroutineDispatcher = Dispatchers.Default
@JvmStatic
@Volatile
var Main: MainCoroutineDispatcher = Dispatchers.Main
...
}
And, inside @BeforeClass
method I call
@ExperimentalCoroutinesApi
fun setInstantMainDispatcher() {
Main = object : MainCoroutineDispatcher() {
@ExperimentalCoroutinesApi
override val immediate: MainCoroutineDispatcher
get() = this
override fun dispatch(context: CoroutineContext, block: Runnable) {
block.run()
}
}
}
That will guarantee that the block will be executed in the calling thread.
It is the only alternative I found to constructor injection.