How to Inject Other Dispatchers to MainCoroutineRule? - android

In the Google Codelab for Coroutines, we are shown a MainCoroutineScopeRule. Within the rule, it explains that this rule can be extended to other dispatchers in addition to Dispatchers.Main:
override fun starting(description: Description?) {
super.starting(description)
// If your codebase allows the injection of other dispatchers like
// Dispatchers.Default and Dispatchers.IO, consider injecting all of them here
// and renaming this class to `CoroutineScopeRule`
//
// All injected dispatchers in a test should point to a single instance of
// TestCoroutineDispatcher.
Dispatchers.setMain(dispatcher)
}
My question is, how exactly are we to inject the other dispatchers? Does this assume that we're using dependency injection? If so, what if I'm not using DI, can I still extend this rule to the other dispatchers? I don't see anything in the kotlinx-coroutines-test library that allows me to set the TestCoroutineDispatcher to the other dispatchers. So, there's this:
Dispatchers.setMain(dispatcher)
...but not this:
Dispatchers.setIO(dispatcher) // Or Default, etc.
Am I instead expected to rewrite my suspend functions to take in a dispatcher as a parameter:
suspend doSomeIO(dispatcher: CoroutineDispatcher = Dispatchers.IO) {
launch(dispatcher) {
// Some long-running IO operation
}
}

You are correct in that this does assume you are injecting your dispatchers. If you are not using the main dispatcher, you should be injecting the dispatcher in order to test it properly.
The way you wrote the suspend function is one way to do it, if you want to force that particular function to be on the Dispatchers.IO thread. However, then you will end up having nested launches.
Instead of that, I would just pass the dispatcher in to a viewModel, and let the viewmodel decide how to call the suspend function.
//Your version:
suspend fun doSomeIO(dispatcher: CoroutineDispatcher = Dispatchers.IO) {
launch(dispatcher) {
// Some long-running IO operation
}
}
class MyViewModel(val dispatcher: CoroutineDispatcher = Dispatchers.IO: ViewModel() {
init {
viewModelScope.launch {
doSomeIO(dispatcher) // here you are launching one coroutine inside the other
}
}
}
// Instead try this:
suspend fun doSomeIO() {
// Some long-running IO operation
}
class MyViewModel(val dispatcher: CoroutineDispatcher = Dispatchers.IO: ViewModel() {
init {
viewModelScope.launch(dispatcher) {
doSomeIO()
}
}
}

Related

What's different between these parameter vs injected Dispatchers?

These two methods invoke the same use-case. In the first version, I hard-coded Dispatchers.IO, and things work as expected.
The second version (which I prefer) uses an injected dispatcher that defaults to the Dispatchers.IO type. It fails with the IllegalStateException described in the comments. Any ideas?
#HiltViewModel
class MainViewModel #Inject constructor(
private val getUsers: GetUsers,
private val dispatcher: CoroutineDispatcher = Dispatchers.IO,
) : ViewModel() {
val liveData: MutableLiveData<List<User>> = MutableLiveData()
suspend fun getUsersByParamDispatcher(params: GetUsers.Params) {
// Successfully works as intended.
viewModelScope.launch(Dispatchers.IO) {
getUsers(params).collectLatest {
liveData.postValue(it)
}
}
}
suspend fun getUsersByInjectDispatcher(params: GetUsers.Params) {
// IllegalStateException: Cannot access database on the main thread since it may potentially
// lock the UI for a long period of time.
// at androidx.room.RoomDatabase.assertNotMainThread(RoomDatabase.java:494).
viewModelScope.launch(dispatcher) {
getUsers(params).collectLatest {
liveData.postValue(it)
}
}
}
}
Logs confirm the exception and my curiosity is why are they different and how would I arrive at a working injected version.
Failing injected Dispatchers.IO:
>> coroutine.name: main
Working parameter Dispatchers.IO:
>> coroutine.name: DefaultDispatcher-worker-1
The dependencies are provided by #HiltViewModel and I expect dispatcher to respect its assigned default value. The Fragment creates this view model with the by viewModels() delegate.
It might be fine to hard-code the dispatcher. But with injection a blocking TestCoroutineDispatcher is easily passed during testing.
Maybe I'm overlooking something simple, or another way altogether.
// MainViewModelTest
#Before
fun setup() {
MockKAnnotations.init(this)
viewModel = MainViewModel(
getUsers,
coroutinesTestRule.testDispatcher
)
}

What is the lifetime of coroutineScope in Kotlin?

The Code A is from the project architecture samples at https://github.com/android/architecture-samples
1: I don't know if the function activateTask(task: Task) need to be wrapped with runBlocking just like Code B. I'm afraid that activateTask(task: Task) maybe not be run if the object of DefaultTasksRepository is destroyed quickly.
2: Normally I run coroutines in ViewModel.viewModelScope, I don't know whether the ViewModel.viewModelScope will be destroyed when I finish the app, and whether the coroutines running in ViewModel.viewModelScope will be destroyed too. If so, I think it will be bad, some long time coroutines such as writing data to remote server will be cancel.
3: And more, the function activateTask in Code A is a coroutines function, it can invoke another coroutines function directly, so I think the Code A+ is correct, right?
Code A
import kotlinx.coroutines.coroutineScope
...
class DefaultTasksRepository(
private val tasksRemoteDataSource: TasksDataSource,
private val tasksLocalDataSource: TasksDataSource,
private val ioDispatcher: CoroutineDispatcher = Dispatchers.IO
) : TasksRepository {
...
override suspend fun activateTask(task: Task) = withContext<Unit>(ioDispatcher) {
coroutineScope {
launch { tasksRemoteDataSource.activateTask(task) }
launch { tasksLocalDataSource.activateTask(task) }
}
}
override suspend fun clearCompletedTasks() {
coroutineScope {
launch { tasksRemoteDataSource.clearCompletedTasks() }
launch { tasksLocalDataSource.clearCompletedTasks() }
}
}
...
}
Code A+
import kotlinx.coroutines.coroutineScope
...
class DefaultTasksRepository(
private val tasksRemoteDataSource: TasksDataSource,
private val tasksLocalDataSource: TasksDataSource,
private val ioDispatcher: CoroutineDispatcher = Dispatchers.IO
) : TasksRepository {
...
override suspend fun activateTask(task: Task) = withContext<Unit>(ioDispatcher) {
tasksRemoteDataSource.activateTask(task)
tasksLocalDataSource.activateTask(task)
}
override suspend fun clearCompletedTasks() {
tasksRemoteDataSource.clearCompletedTasks()
tasksLocalDataSource.clearCompletedTasks()
}
...
}
Code B
fun main() = runBlocking {
launch {
delay(1000L)
println("World!")
}
println("Hello,")
}
You should not use runBlocking in any coroutine application, it blocks the thread.
If you really want to make activateTask non-cancellable there is a factory implementation of NonCancellable already in the stdlib
And you should not use coroutineScope wrapper inside the withContext, as a newly created CoroutineScope along with a new job is already passed as receiver within withContext.
Implement your activateTask like this:
override suspend fun activateTask(task: Task) = withContext<Unit>(ioDispatcher + NonCancellable) {
launch { tasksRemoteDataSource.activateTask(task) }
launch { tasksLocalDataSource.activateTask(task) }
}
In this way it will be called on the IODispatcher but will not be cancellable since the Job element of the resulting context does not provide functionality to cancel it.
ViewModelScope runs till your application is destroyed, more info and lifecycle chart is here. If you want to run some very important tasks, then use other dispatchers.
Yes code A+ is completely correct
PS: You should not implement runBlocking in a coroutine application, its default implementation is just the event loop.
runBlocking is the way to bridge synchronous and asynchronous code
Better implementation of main function should be:
suspend fun main() = coroutineScope {
// code here
}
It runs on the CommonPool, and if it suspends another coroutine could reuse the same thread.

Marking a custom coroutine dispatcher so that it is treated like Dispatchers.IO

I am using a simple class to inject dispatchers to my objects, so I can have different dispatchers for testing and for actual work, the classes I use are :
interface IDispatchers {
val IO: CoroutineDispatcher
val CPU: CoroutineDispatcher
val UI: CoroutineDispatcher
}
object AppDispatchers : IDispatchers{
override val UI = Dispatchers.Main
override val CPU = Dispatchers.Default
override val IO = Dispatchers.IO
}
#ExperimentalCoroutinesApi
object TestDispatchers : IDispatchers {
override val IO = Dispatchers.Unconfined
override val CPU = Dispatchers.Unconfined
override val UI = Dispatchers.Unconfined
}
this works great and does exactly what I want.
my problem is with the latest update of android studio, when I have an operation that could be blocking (for example reading from a stream) it cannot understand that I am indeed using an IO dispatcher and it keeps showing a message
for example let's say I have this class:
class fooViewModel(val dispatchers:IDispatchers) {
suspend fun thisDoesStuffOnIO() {
//code
val nextLine = withContext(dispatchers.IO) { stream.readLine() }
//more code
}
}
now when I replace dispatchers.IO with the actual Dispatchers.IO it shows no warning, but when I go back to my implementation it shows a warning
I tried looking for an annotation or property in Dispatchers.IO that ,if copied to my interface, would disable the inspection but I couldn't find any
so is there a way to make that inspection understand that the dispatcher I am using is indeed an IO dispatcher and there is no actual problem?

Kotlin Coroutine Testing with Dispatchers.IO

So maybe there has been a tutorial going over this, but none of the ones I have read have addressed this issue for me. I have the structure as below and am trying to unit test, but when I go to test I always fails stating the repo method doSomthing() was never called. My best guess is because i have launched a new coroutine in a different context. How do I test this then?
Repository
interface Repository {
suspend fun doSomething(): String
}
View Model
class ViewModel(val repo: Repository) {
val liveData = MutableLiveData<String>()
fun doSomething {
//Do something here
viewModelScope.launch(Dispatchers.IO) {
val data = repo.doSomething()
withContext(Dispatchers.Main) {
liveData.value = data
}
}
}
}
View Model Test
class ViewModelTest {
lateinit var viewModel: ViewModel
lateinit var repo: Repository
#Before
fun setup() {
Dispatchers.setMain(TestCoroutineDispatcher())
repo = mock<Repository>()
viewModel = ViewModel(repo)
}
#Test
fun doSomething() = runBlockingTest {
viewModel.doSomething()
viewModel.liveData.test().awaitValue().assertValue {
// assert something
}
verify(repo).doSomthing()
}
}
According to Google:
Dispatchers should be injected into your ViewModels so you can properly test. You are setting the TestCorotutineDispatcher as the main Dispatcher via Dispatchers.setMain which takes control over the MainDispatcher, but you still have no control over the the execution of the coroutine launched via viewModelScope.launch(Dispatchers.IO).
Passing the Dispatcher via the constructor would make sure that your test and production code use the same dispatcher.
Typically an #Rule is defined that:
Overrides the MainDispatcher via Dispatchers.setMain (like you are doing)
Uses the TestCoroutineDispatcher's own runBlockingTest() to actually run the test.
Here is a really nice talk about testing and coroutines that happened at last year's Android Dev Summit.
And here is an example of such an #Rule. (Shameless plug. There are also examples of coroutine tests on that repo as well)
I write this solution for who use Dagger.
Inject CoroutineDispatcher in ViewModel constructor like this:
class LoginViewModel #Inject constructor(val dispatcher: CoroutineDispatcher) : BaseViewModel() {
and Provide Dispatcher like this:
#Singleton
#Provides
fun provideDispatchers(): CoroutineDispatcher = Dispatchers.IO
and in test package, Provide Dispatcher like this:
#Singleton
#Provides
fun provideDispatchers(): CoroutineDispatcher = UnconfinedTestDispatcher()
and now all lines in viewModelScope.launch(dispatcher) will be run

Best approach for unit-testing scoped viewmodels

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.

Categories

Resources