I want to have a typed solution to pass an applica...
# coroutines
s
I want to have a typed solution to pass an application-wide scope, which I am doing by simply doing this
Copy code
class ApplicationScope : CoroutineScope by MainScope()
And in the classes that need it I can inject it by using this type and I know which one it will get, since I am providing this as a singleton in my DI setup. I’d like to do the same with a background dispatcher, but I can’t quite delegate like this, as CoroutineDispatcher is an abstract class, and not an interface. So I figured this compiles just fine
Copy code
class BackgroundDispatcher : CoroutineDispatcher() {
  override fun dispatch(context: CoroutineContext, block: Runnable) {
    Dispatchers.IO.dispatch(context, block)
  }
}
And then in the classes where I want to provide this, I can ask for a
BackgroundDispatcher
and it should be IO for prod (again providing as a singleton so it’s just one instance of this class), and for tests I can provide the alternative that I want to use per-test. So my question is does this look odd? Any obvious problems I may be oblivious to doing something like this?
k
If you’re using dagger, you should probably be using
@Qualifier
annotations instead.
It will be difficult to test things which have a tight coupling to
MainScope
and
<http://Dispatchers.IO|Dispatchers.IO>
like the examples above.
m
CoroutineDispatcher
has a lot of open functions that you will not be delegating to
Dispatchers.IO.dispatch
, so it will probably not work the way you want. Also I would be confused by
BackgroudnDispatcher
being IO and not Default. Both are background, so the name doesn't make sense. And like mentioned above, by tightly coupling the type to the dispatcher, you are defeating the point of injecting a dispatcher to begin with. You might as well just hard code it.
s
Yeah yeah you’re totally right, I was just trying to avoid the qualifier as it results in a compilation error instead of getting a warning in the IDE immediately when you’re not providing the qualifier and can be a bit annoying. But I think I’ll just go with a qualifier after all. We’re using Koin btw, but that has qualfier support too, so shouldn’t be an issue. Thanks for saving me from this bad idea 😅
k
For dispatcher you’ll likely want to invert the dependency you inject upwards to
CoroutineContext
.
That way you can use
EmptyCoroutineContext
in tests without having to worry about threading.
s
Why do you want to subclass. It's easier to just create them using the
CoroutinScope(...)
factory methods.... And if you want to be able to tell them apart when injecting them, use qualifier annotations. Subclassing CoroutineScopes and properly implementing them is asking for trouble 🙂
And if you really don't want to use qualifier annotations, create two classes that just plainly wrap them. E.g
class ApplicationScopeWrapper(val scope: CoroutineScope = MainScope())
s
Yeap makes total sense to go with CoroutineContext for that one. I am in an Android codebase here, so I do still want to be able to provide an application-wide CoroutineScope which I give to some classes to perform some work that should outlive what they’re doing. I think I could just do
Copy code
abstract class ApplicationScope(coroutineScope: CoroutineScope) : CoroutineScope by coroutineScope
And provide this as
Copy code
single<ApplicationScope> { object : ApplicationScope(MainScope()) {} }
or something similar hmm
s
And inject these wrappers and use the
.scope
to get the actual scope.
s
A wrapper like that can work actually yeah, without inheriting from CoroutineScope at all, lemme try that
For your suggestion Anton, I wonder if it really matters doing this
Copy code
class ApplicationScope(scope: CoroutineScope = MainScope())
vs this
Copy code
class ApplicationScope(scope: CoroutineScope = MainScope()) : CoroutineScope by scope
since I’m completely delegating to whatever
scope
is, it really shouldn’t matter and I miss having to call
.scope
each time I am using this, no? The way I had it before
Copy code
class ApplicationScope : CoroutineScope by MainScope()
was wrong since I could not replace it for tests of course, but this adjustment should be enough to let me change it for tests.
s
If you want to make it suitable for testing, add a constructor parameter that takes a CoroutineContext, which you then can use to pass the proper Dispatcher.
s
For those tests, could I not pass both at the same time by doin smth like:
Copy code
ApplicationScope(CoroutineScope(<http://Dispatchers.IO|Dispatchers.IO>))
Since CoroutineScope constructor itself takes CoroutineContext where I can pass that dispatcher?
s
Copy code
class ApplicationScope(context: CoroutineContext = EmptyCoroutineContext) : CoroutineScope by CoroutineScope(SupervisorJob() + Dispatchers.Main + context)
This will by default create a main-scope (https://github.com/Kotlin/kotlinx.coroutines/blob/master/kotlinx-coroutines-core/common/src/CoroutineScope.kt#L118) and allow you to override stuff through the
context
parameter
s
Then in tests if I want to opt-out of the SupervisorJob functionality and go with a normal Job I’d need to explicitly pass in, sounds like more trouble than worth compared to
Copy code
class ApplicationScope(scope: CoroutineScope = MainScope()) : CoroutineScope by scope
no? Like I’m not sure what this last suggestion will help me with in terms of ease of use in my tests compared to passing an entire CoroutineScope? Passing the entire CoroutineScope will also allow me to pass backgroundScope from inside
TestScope
if that’s what I need to use. With your impl that would make it harder no?
One unfortunate thing with all this and using Koin is that now on the class itself the parameter simply looks like
coroutineContext: CoroutineContext,
without the information of the qualifier, and I need to make sure I remember to pass it in on the place where I am constructing it, like
coroutineContext = get(ioDispatcherQualifier)
. Let’s hope I don’t change this somehow and forget to do the same thing some months in the future 😅