Movatterモバイル変換


[0]ホーム

URL:


Skip to content

Navigation Menu

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up

Automatic CoroutineDispatcher injection and extensions for kotlinx.coroutines

License

NotificationsYou must be signed in to change notification settings

RBusarow/Dispatch

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

CILicense

Dispatch

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

Contents

Injecting dispatchers

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.

Types and Factories

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.

TypeDispatcher
DefaultCoroutineScopeDispatchers.Default
IOCoroutineScopeDispatchers.IO
MainCoroutineScopeDispatchers.Main
MainImmediateCoroutineScopeDispatchers.Main.immediate
UnconfinedCoroutineScopeDispatchers.Unconfined
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  }}

Referencing dispatchers

Thesedispatcher settings can then be accessed via extension functions uponCoroutineScope, or thecoroutineContext, or directly from extensionfunctions:

Builder Extensions

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

Android Lifecycle

TheAndroidX.lifecycle library offers alifecycleScope extension function to provide a lifecycle-awareCoroutineScope, but there are two shortcomings:

  1. It delegates to a hard-codedDispatchers.MainCoroutineDispatcher, which complicates unit andEspresso testing by requiring the use ofDispatchers.setMain.
  2. 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.

Android Espresso

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

Android ViewModel

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 theviewModelScopecan 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

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

Modules

artifactfeatures
dispatch-android-espressoIdlingDispatcher

IdlingDispatcherProvider

dispatch-android-lifecycle-extensionsdispatchLifecycleScope
dispatch-android-lifecycleDispatchLifecycleScope

launchOnCreate

launchOnStart

launchOnResume

onNextCreate

onNextStart

onNextResume

dispatch-android-viewmodelCoroutineViewModel

viewModelScope

dispatch-coreDispatcher-specific types and factories

Dispatcher-specific coroutine builders

dispatch-detektDetekt rules for common auto-imported-the-wrong-thing problems
dispatch-test-junit4TestCoroutineRule
dispatch-test-junit5CoroutineTest

CoroutineTestExtension

dispatch-testTestProvidedCoroutineScope

TestDispatcherProvider

runBlockingProvided andtestProvided

Full Gradle Config

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

License

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

Resources

License

Stars

Watchers

Forks

Packages

No packages published

Languages


[8]ページ先頭

©2009-2025 Movatter.jp