- Notifications
You must be signed in to change notification settings - Fork6
Automatic CoroutineDispatcher injection and extensions for kotlinx.coroutines
License
RBusarow/Dispatch
Folders and files
Name | Name | Last commit message | Last commit date | |
---|---|---|---|---|
Repository files navigation
Utilities forkotlinx.coroutines which make them type-safe, easier to test, and more expressive.Use the predefinedtypes and factories or define your own, and never injectaDispatchers
object again.
val presenter=MyPresenter(MainCoroutineScope())classMyPresenter @Inject constructor(/** * Defaults to the Main dispatcher*/valcoroutineScope:MainCoroutineScope) {funloopSomething()= coroutineScope.launchDefault { }suspendfunupdateSomething()= withMainImmediate { }}
classMyTest { @Testfun`no setting the main dispatcher`()= runBlockingProvidedTest {// automatically use TestCoroutineDispatcher for every dispatcher typeval presenter=MyPresenter(coroutineScope=this)// this call would normally crash due to the main looper presenter.updateSomething() }}
- Injecting dispatchers
- Types and Factories
- Referencing dispatchers
- Android Lifecycle
- Android Espresso
- Android ViewModel
- Testing
- Modules
- Full Gradle Config
- License
Everywhere you use coroutines, you use aCoroutineContext. If we embed theCoroutineDispatchers settings we want into the context, then we don't need topass them around manually.
The core of this library isDispatcherProvider - an interface with properties corresponding to the5 differentCoroutineDispatchers we can get from theDispatchers singleton.It lives inside theCoroutineContext, and gets passed from parent to child coroutinestransparently without any additional code.
interfaceDispatcherProvider :CoroutineContext.Element {overrideval key:CoroutineContext.Key<*> get()=Keyval default:CoroutineDispatcherval io:CoroutineDispatcherval main:CoroutineDispatcherval mainImmediate:CoroutineDispatcherval unconfined:CoroutineDispatchercompanionobject Key : CoroutineContext.Key<DispatcherProvider>}val someCoroutineScope=CoroutineScope(Job()+Dispatchers.Main+DispatcherProvider())
The default implementation of this interface simply delegates to thatDispatchers singleton, asthat is what we typically want for production usage.
ACoroutineScope may have any type ofCoroutineDispatcher. What if we have a View class whichwill always use theMain thread, or one which will always do I/O?
There are marker interfaces and factories to ensure that the correct type ofCoroutineScope isalways used.
val mainScope=MainCoroutineScope()val someUIClass=SomeUIClass(mainScope)classSomeUIClass(valcoroutineScope:MainCoroutineScope) {funfoo()= coroutineScope.launch {// because of the dependency type,// we're guaranteed to be on the main dispatcher even though we didn't specify it }}
Thesedispatcher settings can then be accessed via extension functions uponCoroutineScope, or thecoroutineContext, or directly from extensionfunctions:
| |Default |IO |Main |Main.immediate | **Unconfined** | | ------------ | --------------- | ---------- | ------------ |--------------------- | ------------------ | |Job |launchDefault |launchIO|launchMain |launchMainImmediate |launchUnconfined|Deferred |asyncDefault |asyncIO |asyncMain |asyncMainImmediate|asyncUnconfined|suspend T
|withDefault |withIO |withMain |withMainImmediate|withUnconfined|Flow<T>
|flowOnDefault |flowOnIO |flowOnMain |flowOnMainImmediate|flowOnUnconfined
classMyClass(valcoroutineScope:IOCoroutineScope) {funaccessMainThread()= coroutineScope.launchMain {// we're now on the "main" thread as defined by the interface }}
TheAndroidX.lifecycle library offers alifecycleScope extension function to provide a lifecycle-awareCoroutineScope, but there are two shortcomings:
- It delegates to a hard-coded
Dispatchers.Main
CoroutineDispatcher, which complicates unit andEspresso testing by requiring the use ofDispatchers.setMain. - Itpauses the dispatcher when the lifecycle state passes below its threshold,whichleaks backpressure to the producing coroutine and can create deadlocks.
Dispatch-android-lifecycle anddispatch-android-lifecycle-extensions completely replace theAndroidX version.
importdispatch.android.lifecycle.*importdispatch.core.*importkotlinx.coroutines.flow.*classMyActivity :Activity() {init { dispatchLifecycleScope.launchOnCreate { viewModel.someFlow.collect { channel.send("$it") } } }}
TheDispatchLifecycleScope may be configured with any dispatcher,sinceMainImmediateCoroutineScope is just a marker interface. Its lifecycle-aware functionscancelwhen dropping below a threshold, then automatically restart when entering into the desired lifecyclestate again. This is key to preventing the backpressure leak of the AndroidX version, and it's alsomore analogous to the behavior ofLiveData to which many developers are accustomed.
There are two built-in ways to define a custom LifecycleCoroutineScope - by simply constructing onedirectly inside a Lifecycle class, or by statically setting a customLifecycleScopeFactory. Thissecond option can be very useful when utilizing anIdlingCoroutineScope.
Espresso is able to useIdlingResource to infer when it should perform its actions, which helpsto reduce the flakiness of tests. Conventional thread-basedIdlingResource
implementations don'twork with coroutines, however.
IdlingCoroutineScope utilizesIdlingDispatchers, which count a coroutine asbeing "idle" when it is suspended. Using statically defined factories, service locators, ordependency injection, it is possible to utilize idling-aware dispatchers throughout a codebaseduring Espresso testing.
classIdlingCoroutineScopeRuleWithLifecycleSample {val customDispatcherProvider=IdlingDispatcherProvider() @JvmField @Ruleval idlingRule=IdlingDispatcherProviderRule {IdlingDispatcherProvider(customDispatcherProvider) }/** * If you don't provide CoroutineScopes to your lifecycle components via a dependency injection framework, * you need to use the `dispatch-android-lifecycle-extensions` and `dispatch-android-viewmodel` artifacts * to ensure that the same `IdlingDispatcherProvider` is used.*/ @BeforefunsetUp() {LifecycleScopeFactory.set {MainImmediateCoroutineScope(customDispatcherProvider) }ViewModelScopeFactory.set {MainImmediateCoroutineScope(customDispatcherProvider) } } @TestfuntestThings()= runBlocking {// Now any CoroutineScope which uses the DispatcherProvider// in TestAppComponent will sync its "idle" state with Espresso }}
TheAndroidX ViewModel library offers aviewModelScope extension function to provide an auto-cancelledCoroutineScope, but again, thisCoroutineScope
is hard-coded and usesDispatchers.Main
. Thislimitation needn't exist.
Dispatch-android-viewmodel doesn't have as many options as its lifecycle counterpart, because theViewModel.onCleared function isprotected
andViewModel does not expose anything about itslifecycle. The only way for a third party library to achieve a lifecycle-awareCoroutineScope
isthrough inheritance.
CoroutineViewModel is a simple abstract class which exposes a lazyviewModelScope property whichis automatically cancelled when theViewModel
is destroyed. The exact type of theviewModelScope
can be configured statically viaViewModelScopeFactory. In this way, you can useIdlingCoroutineScopes for Espresso testing,TestProvidedCoroutineScopes for unit testing, or any other customscope you'd like.
If you're using the AACViewModel
but not dependency injection, this artifact should be veryhelpful with testing.
importdispatch.android.viewmodel.*importkotlinx.coroutines.flow.*importtimber.log.*classMyViewModel :CoroutineViewModel() {init {MyRepository.someFlow.onEach {Timber.d("$it") }.launchIn(viewModelScope) }}
TheDispatchLifecycleScope may be configured with any dispatcher,sinceMainImmediateCoroutineScope is just a marker interface. Its lifecycle-aware functionscancelwhen dropping below a threshold, then automatically restart when entering into the desired lifecyclestate again. This is key to preventing the backpressure leak of the AndroidX version, and it's alsomore analogous to the behavior ofLiveData to which many developers are accustomed.
There are two built-in ways to define a custom LifecycleCoroutineScope - by simply constructing onedirectly inside a Lifecycle class, or by statically setting a customLifecycleScopeFactory. Thissecond option can be very useful when utilizing anIdlingCoroutineScope.
Testing is why this library exists.TestCoroutineScope andTestCoroutineDispatcher are verypowerful when they can be used, but any reference to a statically defined dispatcher (like aDispatchers property) removes that control.
To that end, there's a configurableTestDispatcherProvider:
classTestDispatcherProvider(overridevaldefault:CoroutineDispatcher =TestCoroutineDispatcher(),overridevalio:CoroutineDispatcher =TestCoroutineDispatcher(),overridevalmain:CoroutineDispatcher =TestCoroutineDispatcher(),overridevalmainImmediate:CoroutineDispatcher =TestCoroutineDispatcher(),overridevalunconfined:CoroutineDispatcher =TestCoroutineDispatcher()) : DispatcherProvider
As well as a polymorphicTestProvidedCoroutineScope which may be used in place of anytype-specificCoroutineScope:
val testScope=TestProvidedCoroutineScope()val someUIClass=SomeUIClass(testScope)classSomeUIClass(valcoroutineScope:MainCoroutineScope) {funfoo()= coroutineScope.launch {// ... }}
There's alsotestProvided, which delegates torunBlockingTest but whichincludes aTestDispatcherProvider inside theTestCoroutineScope.
classSubject {// this would normally be a hard-coded reference to Dispatchers.MainsuspendfunsayHello()= withMain { }}@Testfun`sayHello should say hello`()= runBlockingProvided {val subject=SomeClass(this)// uses "main" TestCoroutineDispatcher safely with no additional setup subject.getSomeData() shouldPrint"hello"}
artifact | features |
---|---|
dispatch-android-espresso | IdlingDispatcher |
dispatch-android-lifecycle-extensions | dispatchLifecycleScope |
dispatch-android-lifecycle | DispatchLifecycleScope |
dispatch-android-viewmodel | CoroutineViewModel |
dispatch-core | Dispatcher-specific types and factories Dispatcher-specific coroutine builders |
dispatch-detekt | Detekt rules for common auto-imported-the-wrong-thing problems |
dispatch-test-junit4 | TestCoroutineRule |
dispatch-test-junit5 | CoroutineTest |
dispatch-test | TestProvidedCoroutineScope |
repositories { mavenCentral()}dependencies {/* production code*/// core coroutines implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.5.0") implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.5.0")// a BOM ensures that all artifacts used from the library are of the same version implementation(platform("com.rickbusarow.dispatch:dispatch-bom:1.0.0-beta10"))// everything provides :core via "api", so you only need this if you have no other "implementation" dispatch artifacts implementation("com.rickbusarow.dispatch:dispatch-core")// LifecycleCoroutineScope for Android Fragments, Activities, etc. implementation("com.rickbusarow.dispatch:dispatch-android-lifecycle")// lifecycleScope extension function with a settable factory. Use this if you don't DI your CoroutineScopes// This provides :dispatch-android-lifecycle via "api", so you don't need to declare both implementation("com.rickbusarow.dispatch:dispatch-android-lifecycle-extensions")// ViewModelScope for Android ViewModels implementation("com.rickbusarow.dispatch:dispatch-android-viewmodel")/* jvm testing*/// core coroutines-test testImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:1.5.0")// you only need this if you don't have the -junit4 or -junit5 artifacts testImplementation("com.rickbusarow.dispatch:dispatch-test")// CoroutineTestRule and :dispatch-test// This provides :dispatch-test via "api", so you don't need to declare both// This can be used at the same time as :dispatch-test-junit5 testImplementation("com.rickbusarow.dispatch:dispatch-test-junit4")// CoroutineTest, CoroutineTestExtension, and :dispatch-test// This provides :dispatch-test via "api", so you don't need to declare both// This can be used at the same time as :dispatch-test-junit4 testImplementation("com.rickbusarow.dispatch:dispatch-test-junit5")/* Android testing*/// core android androidTestImplementation("androidx.test:runner:1.3.0") androidTestImplementation("androidx.test.espresso:espresso-core:3.3.0")// IdlingDispatcher, IdlingDispatcherProvider, and IdlingCoroutineScope androidTestImplementation("com.rickbusarow.dispatch:dispatch-android-espresso")}
Copyright (C) 2021 Rick BusarowLicensed under the Apache License, Version 2.0 (the "License");you may not use this file except in compliance with the License.You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0Unless required by applicable law or agreed to in writing, softwaredistributed under the License is distributed on an "AS IS" BASIS,WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.See the License for the specific language governing permissions andlimitations under the License.
About
Automatic CoroutineDispatcher injection and extensions for kotlinx.coroutines