From 093b60d5661770046c4aac9c97d940f2a8e12033 Mon Sep 17 00:00:00 2001 From: Rick Busarow Date: Fri, 3 Jul 2020 09:14:48 -0500 Subject: [PATCH] Lifecycle coroutine scope leak fixes (#141) * - automatically cancel LifecycleCoroutineScope when lifecycle reaches DESTROYED (fixes #135) - automatically remove lifecycleScope extension property from cache when lifecycle reaches DESTROYED (fixes #136) * dispatch-internal-test-android module * update lifecycle handling * update lifecycle handling * update lifecycle handling * convert LifecycleScopeExtensionTest from Kotest to JUnit5 to fix some weird recursive behavior * convert LifecycleScopeExtensionTest from Kotest to JUnit5 to fix some weird recursive behavior * remove workspace.xml backup which shouldn't have been added to git --- .idea/codeStyles/Project.xml | 2 +- buildSrc/src/main/kotlin/Dependencies.kt | 2 +- .../lifecycle/LifecycleScopeFactory.kt | 3 +- .../internal/LifecycleCoroutineScopeStore.kt | 73 ++- .../android/lifecycle/internal/atomic.kt | 60 +++ .../{LifecycleScope.kt => lifecycleScope.kt} | 3 +- .../lifecycle/LifecycleScopeExtensionTest.kt | 418 ++++++++++++++++++ .../lifecycle/LifecycleScopeFactoryTest.kt | 8 +- .../lifecycle/internal/AtomicGetOrPutTest.kt | 88 ++++ dispatch-android-lifecycle/README.md | 3 +- dispatch-android-lifecycle/build.gradle.kts | 8 +- .../lifecycle/LifecycleCoroutineScope.kt | 13 + .../LifecycleCoroutineScopeBinding.kt | 62 +++ .../lifecycle/LifecycleCoroutineScopeTest.kt | 45 +- .../rules/AndroidxLifecycleScopeUsageTest.kt | 3 - .../internal/test/android/FakeLifecycle.kt | 55 +++ .../test/android/FakeLifecycleOwner.kt | 6 +- dispatch-internal-test/build.gradle.kts | 1 + .../java/dispatch/internal/test/reflect.kt | 45 ++ .../-lifecycle-scope-factory/index.md | 2 +- .../-lifecycle-scope-factory/reset.md | 2 +- .../-lifecycle-scope-factory/set.md | 2 +- .../lifecycle-scope.md | 5 +- .../alltypes/index.md | 2 + .../-lifecycle-coroutine-scope/-init-.md | 3 + .../-minimum-state-policy/-c-a-n-c-e-l.md | 2 +- .../-r-e-s-t-a-r-t_-e-v-e-r-y.md | 2 +- .../-minimum-state-policy/index.md | 2 +- .../-lifecycle-coroutine-scope/index.md | 5 +- .../launch-on-create.md | 2 +- .../launch-on-resume.md | 2 +- .../launch-on-start.md | 2 +- .../-lifecycle-coroutine-scope/lifecycle.md | 2 +- .../dokka/dispatch-android-lifecycle/index.md | 60 +++ docs/modules/dispatch-android-lifecycle.md | 3 +- 35 files changed, 922 insertions(+), 74 deletions(-) create mode 100644 dispatch-android-lifecycle-extensions/src/main/java/dispatch/android/lifecycle/internal/atomic.kt rename dispatch-android-lifecycle-extensions/src/main/java/dispatch/android/lifecycle/{LifecycleScope.kt => lifecycleScope.kt} (92%) create mode 100644 dispatch-android-lifecycle-extensions/src/test/java/dispatch/android/lifecycle/LifecycleScopeExtensionTest.kt create mode 100644 dispatch-android-lifecycle-extensions/src/test/java/dispatch/android/lifecycle/internal/AtomicGetOrPutTest.kt create mode 100644 dispatch-android-lifecycle/src/main/java/dispatch/android/lifecycle/internal/LifecycleCoroutineScopeBinding.kt create mode 100644 dispatch-internal-test-android/src/main/java/dispatch/internal/test/android/FakeLifecycle.kt create mode 100644 dispatch-internal-test/src/main/java/dispatch/internal/test/reflect.kt diff --git a/.idea/codeStyles/Project.xml b/.idea/codeStyles/Project.xml index e6295f500..3e7a42c25 100644 --- a/.idea/codeStyles/Project.xml +++ b/.idea/codeStyles/Project.xml @@ -181,4 +181,4 @@ - \ No newline at end of file + diff --git a/buildSrc/src/main/kotlin/Dependencies.kt b/buildSrc/src/main/kotlin/Dependencies.kt index e72ae50c2..94ed8edb4 100644 --- a/buildSrc/src/main/kotlin/Dependencies.kt +++ b/buildSrc/src/main/kotlin/Dependencies.kt @@ -161,7 +161,7 @@ object Libs { } object Kotest { - private const val version = "4.1.0" + private const val version = "4.1.1" const val assertions = "io.kotest:kotest-assertions-core-jvm:$version" const val consoleRunner = "io.kotest:kotest-runner-console-jvm:$version" const val properties = "io.kotest:kotest-property-jvm:$version" diff --git a/dispatch-android-lifecycle-extensions/src/main/java/dispatch/android/lifecycle/LifecycleScopeFactory.kt b/dispatch-android-lifecycle-extensions/src/main/java/dispatch/android/lifecycle/LifecycleScopeFactory.kt index 98b920bdc..cc52175b8 100644 --- a/dispatch-android-lifecycle-extensions/src/main/java/dispatch/android/lifecycle/LifecycleScopeFactory.kt +++ b/dispatch-android-lifecycle-extensions/src/main/java/dispatch/android/lifecycle/LifecycleScopeFactory.kt @@ -15,6 +15,7 @@ package dispatch.android.lifecycle +import androidx.lifecycle.* import dispatch.android.lifecycle.LifecycleScopeFactory.reset import dispatch.core.* @@ -47,7 +48,7 @@ public object LifecycleScopeFactory { _factory = factory } - internal fun create() = _factory.invoke() + internal fun create(lifecycle: Lifecycle) = LifecycleCoroutineScope(lifecycle, _factory.invoke()) /** * Immediately resets the factory function to its default. diff --git a/dispatch-android-lifecycle-extensions/src/main/java/dispatch/android/lifecycle/internal/LifecycleCoroutineScopeStore.kt b/dispatch-android-lifecycle-extensions/src/main/java/dispatch/android/lifecycle/internal/LifecycleCoroutineScopeStore.kt index 7f7f04e68..ec5f6d1d4 100644 --- a/dispatch-android-lifecycle-extensions/src/main/java/dispatch/android/lifecycle/internal/LifecycleCoroutineScopeStore.kt +++ b/dispatch-android-lifecycle-extensions/src/main/java/dispatch/android/lifecycle/internal/LifecycleCoroutineScopeStore.kt @@ -19,57 +19,57 @@ import android.os.* import androidx.lifecycle.* import dispatch.android.lifecycle.* import dispatch.core.* -import kotlinx.coroutines.* -import java.util.* import java.util.concurrent.* -import kotlin.collections.set -internal object LifecycleCoroutineScopeStore { +@Suppress("MagicNumber") +internal object LifecycleCoroutineScopeStore : LifecycleEventObserver { // ConcurrentHashMap can miss "put___" operations on API 21/22 https://issuetracker.google.com/issues/37042460 - @Suppress("MagicNumber") - private val map = if (Build.VERSION.SDK_INT < 23) { - Collections.synchronizedMap(mutableMapOf()) - } else { - ConcurrentHashMap() - } - - fun get(lifecycle: Lifecycle): LifecycleCoroutineScope { + private val map: MutableMap = + if (Build.VERSION.SDK_INT < 23) { + mutableMapOf() + } else { + ConcurrentHashMap() + } - return map[lifecycle] ?: bindLifecycle(lifecycle, LifecycleScopeFactory.create()) + override fun onStateChanged(source: LifecycleOwner, event: Lifecycle.Event) { + if (source.lifecycle.currentState <= Lifecycle.State.DESTROYED) { + map.remove(source.lifecycle) + } } - private fun bindLifecycle( - lifecycle: Lifecycle, coroutineScope: MainImmediateCoroutineScope - ): LifecycleCoroutineScope { - - val scope = LifecycleCoroutineScope(lifecycle, coroutineScope) - - map[lifecycle] = scope + fun get(lifecycle: Lifecycle): LifecycleCoroutineScope { - if (!lifecycle.currentState.isAtLeast(Lifecycle.State.INITIALIZED)) { - scope.coroutineContext.cancel() - return scope + if (lifecycle.currentState <= Lifecycle.State.DESTROYED) { + return LifecycleScopeFactory.create(lifecycle) } - val observer = object : LifecycleEventObserver { - - override fun onStateChanged(source: LifecycleOwner, event: Lifecycle.Event) { - - if (!lifecycle.currentState.isAtLeast(Lifecycle.State.INITIALIZED)) { - - scope.coroutineContext.cancel() - lifecycle.removeObserver(this) + return when { + Build.VERSION.SDK_INT >= 24 -> { + map.computeIfAbsent(lifecycle) { bindLifecycle(lifecycle) } + } + Build.VERSION.SDK_INT == 23 -> { + /* + `getOrPut` by itself isn't atomic. It is guaranteed to only ever return one instance + for a given key, but the lambda argument may be invoked unnecessarily. + */ + (map as ConcurrentMap).atomicGetOrPut(lifecycle) { bindLifecycle(lifecycle) } + } + else -> { + synchronized(map) { + map.getOrPut(lifecycle) { bindLifecycle(lifecycle) } } } } + } - scope.launchMainImmediate { + private fun bindLifecycle(lifecycle: Lifecycle): LifecycleCoroutineScope { + + val scope = LifecycleScopeFactory.create(lifecycle) - if (lifecycle.currentState.isAtLeast(Lifecycle.State.INITIALIZED)) { - lifecycle.addObserver(observer) - } else { - scope.coroutineContext.cancel() + scope.launchMainImmediate { + if (lifecycle.currentState >= Lifecycle.State.INITIALIZED) { + lifecycle.addObserver(this@LifecycleCoroutineScopeStore) } } @@ -77,4 +77,3 @@ internal object LifecycleCoroutineScopeStore { } } - diff --git a/dispatch-android-lifecycle-extensions/src/main/java/dispatch/android/lifecycle/internal/atomic.kt b/dispatch-android-lifecycle-extensions/src/main/java/dispatch/android/lifecycle/internal/atomic.kt new file mode 100644 index 000000000..ce3b9aa08 --- /dev/null +++ b/dispatch-android-lifecycle-extensions/src/main/java/dispatch/android/lifecycle/internal/atomic.kt @@ -0,0 +1,60 @@ +/* + * Copyright (C) 2020 Rick Busarow + * Licensed 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.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed 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 and + * limitations under the License. + */ + +package dispatch.android.lifecycle.internal + +import androidx.lifecycle.* +import dispatch.android.lifecycle.* +import dispatch.core.* +import kotlinx.coroutines.* +import java.util.concurrent.* + +/** + * Normal [getOrPut] is guaranteed to only return a single instance for a given key, + * but the [defaultValue] lambda may be invoked unnecessarily. This would create a new instance + * of [LifecycleCoroutineScope] without actually returning it. + * + * This extra [LifecycleCoroutineScope] would still be active and observing the [lifecycle][key]. + * + * This function compares the result of the lambda to the result of [getOrPut], + * and cancels the lambda's result if it's different. + * + * @see getOrPut + */ +internal inline fun ConcurrentMap.atomicGetOrPut( + key: Lifecycle, + defaultValue: () -> LifecycleCoroutineScope +): LifecycleCoroutineScope { + + var newInstance: LifecycleCoroutineScope? = null + + val existingOrNew = getOrPut(key) { + newInstance = defaultValue.invoke() + newInstance!! + } + + // If the second value is different, that means the first was never inserted into the map. + // Cancel the observer for `newInstance` and cancel the coroutineContext. + newInstance?.let { new -> + if (new != existingOrNew) { + existingOrNew.launchMainImmediate { + + new.coroutineContext[Job]?.cancel() + } + } + } + + return existingOrNew +} diff --git a/dispatch-android-lifecycle-extensions/src/main/java/dispatch/android/lifecycle/LifecycleScope.kt b/dispatch-android-lifecycle-extensions/src/main/java/dispatch/android/lifecycle/lifecycleScope.kt similarity index 92% rename from dispatch-android-lifecycle-extensions/src/main/java/dispatch/android/lifecycle/LifecycleScope.kt rename to dispatch-android-lifecycle-extensions/src/main/java/dispatch/android/lifecycle/lifecycleScope.kt index 208a1377b..d8e112ac9 100644 --- a/dispatch-android-lifecycle-extensions/src/main/java/dispatch/android/lifecycle/LifecycleScope.kt +++ b/dispatch-android-lifecycle-extensions/src/main/java/dispatch/android/lifecycle/lifecycleScope.kt @@ -28,7 +28,8 @@ import kotlinx.coroutines.* * * The type of [CoroutineScope] created is configurable via [LifecycleScopeFactory.set]. * - * The `viewModelScope` is automatically cancelled when the [LifecycleOwner]'s [lifecycle][LifecycleOwner.getLifecycle]'s [Lifecycle.State] drops to [Lifecycle.State.DESTROYED]. + * The `viewModelScope` is automatically cancelled when the [LifecycleOwner]'s + * [lifecycle][LifecycleOwner.getLifecycle]'s [Lifecycle.State] drops to [Lifecycle.State.DESTROYED]. * * @sample samples.LifecycleCoroutineScopeSample.lifecycleCoroutineScopeSample */ diff --git a/dispatch-android-lifecycle-extensions/src/test/java/dispatch/android/lifecycle/LifecycleScopeExtensionTest.kt b/dispatch-android-lifecycle-extensions/src/test/java/dispatch/android/lifecycle/LifecycleScopeExtensionTest.kt new file mode 100644 index 000000000..4ece68490 --- /dev/null +++ b/dispatch-android-lifecycle-extensions/src/test/java/dispatch/android/lifecycle/LifecycleScopeExtensionTest.kt @@ -0,0 +1,418 @@ +/* + * Copyright (C) 2020 Rick Busarow + * Licensed 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.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed 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 and + * limitations under the License. + */ + +package dispatch.android.lifecycle + +import androidx.lifecycle.* +import dispatch.android.lifecycle.internal.* +import dispatch.internal.test.* +import dispatch.internal.test.android.* +import dispatch.test.* +import hermit.test.* +import hermit.test.junit.* +import io.kotest.matchers.* +import kotlinx.coroutines.* +import kotlinx.coroutines.test.* +import org.junit.jupiter.api.* +import java.util.concurrent.* + +@ObsoleteCoroutinesApi +@ExperimentalCoroutinesApi +internal class LifecycleScopeExtensionTest : HermitJUnit5() { + + val storeMap: MutableMap = + LifecycleCoroutineScopeStore.getPrivateObjectFieldByName("map") + + @BeforeEach + fun beforeEach() { + + val dispatcher = TestCoroutineDispatcher() + + LifecycleScopeFactory.set { + TestProvidedCoroutineScope( + dispatcher, + TestDispatcherProvider(dispatcher), + Job() + ) + } + } + + @AfterEach + fun afterEach() { + storeMap.clear() + } + + @Nested + inner class `lifecycle is already destroyed` { + + val lifecycleOwner by resets { FakeLifecycleOwner(initialState = Lifecycle.State.DESTROYED) } + + @Nested + inner class `scope is created` { + + val scope by resets { lifecycleOwner.lifecycleScope } + + @Test + fun `scope and job should already be cancelled`() = runBlocking { + + scope.isActive shouldBe false + scope.coroutineContext[Job]!!.isActive shouldBe false + } + + @Test + fun `scope should not be cached`() { + + storeMap[lifecycleOwner.lifecycle] shouldBe null + } + + // This is a change detector. + // This isn't actually desired, but the precondition should be impossible. + @Test + fun `additional scopes should be unique`() { + + lifecycleOwner.lifecycleScope shouldNotBe scope + } + } + } + + @Nested + inner class `initial lifecycle of Lifecycle State INITIALIZED` { + + val lifecycleOwner by resets { FakeLifecycleOwner(initialState = Lifecycle.State.INITIALIZED) } + + @Nested + inner class `scope is created` { + + val scope by resets { lifecycleOwner.lifecycleScope } + + @Test + fun `scope and job should be active`() { + + scope.isActive shouldBe true + } + + @Test + fun `scope should be cached`() { + + scope + + storeMap[lifecycleOwner.lifecycle] shouldBe scope + } + + @Test + fun `repeated access should return the same scope`() { + + lifecycleOwner.lifecycleScope shouldBe scope + } + + @Nested + inner class `lifecycle passes to destroyed` { + + @BeforeEach + fun beforeEach() { + // special case for Initialized state + lifecycleOwner.create() + + lifecycleOwner.destroy() + } + + @Test + fun `scope should be cancelled`() { + + scope.isActive shouldBe false + } + + @Test + fun `scope should be removed from the cache`() { + + storeMap[lifecycleOwner.lifecycle] shouldBe null + } + } + } + + @Nested + inner class `multiple threads access lifecycleScope at once` { + + @Test + fun `all threads should get the same instance`() = runBlocking { + + val hugeExecutor = ThreadPoolExecutor( + 200, 200, 5000, TimeUnit.MILLISECONDS, LinkedBlockingQueue() + ) + + val dispatcher = hugeExecutor.asCoroutineDispatcher() + + val lock = CompletableDeferred() + + val all = List(200) { + async(dispatcher) { + lock.await() + lifecycleOwner.lifecycleScope + } + } + + yield() + lock.complete(Unit) + + all.awaitAll().distinct() shouldBe listOf(lifecycleOwner.lifecycleScope) + } + } + } + + @Nested + inner class `initial lifecycle of CREATED` { + + val lifecycleOwner by resets { FakeLifecycleOwner(initialState = Lifecycle.State.CREATED) } + + @Nested + inner class `scope is created` { + + val scope by resets { lifecycleOwner.lifecycleScope } + + @Test + fun `scope and job should be active`() { + + scope.isActive shouldBe true + } + + @Test + fun `scope should be cached`() { + + scope + + storeMap[lifecycleOwner.lifecycle] shouldBe scope + } + + @Test + fun `repeated access should return the same scope`() { + + lifecycleOwner.lifecycleScope shouldBe scope + } + + @Nested + inner class `lifecycle passes to destroyed` { + + @BeforeEach + fun beforeEach() { + lifecycleOwner.destroy() + } + + @Test + fun `scope should be cancelled`() { + + scope.isActive shouldBe false + } + + @Test + fun `scope should be removed from the cache`() { + + storeMap[lifecycleOwner.lifecycle] shouldBe null + } + } + } + + @Nested + inner class `multiple threads access lifecycleScope at once` { + + @Test + fun `all threads should get the same instance`() = runBlocking { + + val hugeExecutor = ThreadPoolExecutor( + 200, 200, 5000, TimeUnit.MILLISECONDS, LinkedBlockingQueue() + ) + + val dispatcher = hugeExecutor.asCoroutineDispatcher() + + val lock = CompletableDeferred() + + val all = List(200) { + async(dispatcher) { + lock.await() + lifecycleOwner.lifecycleScope + } + } + + yield() + lock.complete(Unit) + + all.awaitAll().distinct() shouldBe listOf(lifecycleOwner.lifecycleScope) + } + } + } + + @Nested + inner class `initial lifecycle of STARTED` { + + val lifecycleOwner by resets { FakeLifecycleOwner(initialState = Lifecycle.State.STARTED) } + + @Nested + inner class `scope is created` { + + val scope by resets { lifecycleOwner.lifecycleScope } + + @Test + fun `scope and job should be active`() { + + scope.isActive shouldBe true + } + + @Test + fun `scope should be cached`() { + + scope + + storeMap[lifecycleOwner.lifecycle] shouldBe scope + } + + @Test + fun `repeated access should return the same scope`() { + + lifecycleOwner.lifecycleScope shouldBe scope + } + + @Nested + inner class `lifecycle passes to destroyed` { + + @BeforeEach + fun beforeEach() { + lifecycleOwner.destroy() + } + + @Test + fun `scope should be cancelled`() { + + scope.isActive shouldBe false + } + + @Test + fun `scope should be removed from the cache`() { + + storeMap[lifecycleOwner.lifecycle] shouldBe null + } + } + } + + @Nested + inner class `multiple threads access lifecycleScope at once` { + + @Test + fun `all threads should get the same instance`() = runBlocking { + + val hugeExecutor = ThreadPoolExecutor( + 200, 200, 5000, TimeUnit.MILLISECONDS, LinkedBlockingQueue() + ) + + val dispatcher = hugeExecutor.asCoroutineDispatcher() + + val lock = CompletableDeferred() + + val all = List(200) { + async(dispatcher) { + lock.await() + lifecycleOwner.lifecycleScope + } + } + + yield() + lock.complete(Unit) + + all.awaitAll().distinct() shouldBe listOf(lifecycleOwner.lifecycleScope) + } + } + } + + @Nested + inner class `initial lifecycle of RESUMED` { + + val lifecycleOwner by resets { FakeLifecycleOwner(initialState = Lifecycle.State.RESUMED) } + + @Nested + inner class `scope is created` { + + val scope by resets { lifecycleOwner.lifecycleScope } + + @Test + fun `scope and job should be active`() { + + scope.isActive shouldBe true + } + + @Test + fun `scope should be cached`() { + + scope + + storeMap[lifecycleOwner.lifecycle] shouldBe scope + } + + @Test + fun `repeated access should return the same scope`() { + + lifecycleOwner.lifecycleScope shouldBe scope + } + + @Nested + inner class `lifecycle passes to destroyed` { + + @BeforeEach + fun beforeEach() { + + lifecycleOwner.destroy() + } + + @Test + fun `scope should be cancelled`() { + + scope.isActive shouldBe false + } + + @Test + fun `scope should be removed from the cache`() { + + storeMap[lifecycleOwner.lifecycle] shouldBe null + } + } + } + + @Nested + inner class `multiple threads access lifecycleScope at once` { + + @Test + fun `all threads should get the same instance`() = runBlocking { + + val hugeExecutor = ThreadPoolExecutor( + 200, 200, 5000, TimeUnit.MILLISECONDS, LinkedBlockingQueue() + ) + + val dispatcher = hugeExecutor.asCoroutineDispatcher() + + val lock = CompletableDeferred() + + val all = List(200) { + async(dispatcher) { + lock.await() + lifecycleOwner.lifecycleScope + } + } + + yield() + lock.complete(Unit) + + all.awaitAll().distinct() shouldBe listOf(lifecycleOwner.lifecycleScope) + } + } + } + +} diff --git a/dispatch-android-lifecycle-extensions/src/test/java/dispatch/android/lifecycle/LifecycleScopeFactoryTest.kt b/dispatch-android-lifecycle-extensions/src/test/java/dispatch/android/lifecycle/LifecycleScopeFactoryTest.kt index 8fdebccb4..b9bae6a37 100644 --- a/dispatch-android-lifecycle-extensions/src/test/java/dispatch/android/lifecycle/LifecycleScopeFactoryTest.kt +++ b/dispatch-android-lifecycle-extensions/src/test/java/dispatch/android/lifecycle/LifecycleScopeFactoryTest.kt @@ -61,7 +61,7 @@ internal class LifecycleScopeFactoryTest : HermitJUnit5() { @Test fun `default factory should be a default MainImmediateCoroutineScope`() = runBlockingTest { - val scope = LifecycleScopeFactory.create() + val scope = LifecycleScopeFactory.create(lifecycleOwner.lifecycle) scope.coroutineContext[DispatcherProvider]!!.shouldBeTypeOf() @@ -77,7 +77,7 @@ internal class LifecycleScopeFactoryTest : HermitJUnit5() { LifecycleScopeFactory.set { MainImmediateCoroutineScope(originContext) } - val scope = LifecycleScopeFactory.create() + val scope = LifecycleScopeFactory.create(lifecycleOwner.lifecycle) scope.coroutineContext shouldEqualFolded originContext + mainDispatcher } @@ -87,13 +87,13 @@ internal class LifecycleScopeFactoryTest : HermitJUnit5() { LifecycleScopeFactory.set { MainImmediateCoroutineScope(originContext) } - val custom = LifecycleScopeFactory.create() + val custom = LifecycleScopeFactory.create(lifecycleOwner.lifecycle) custom.coroutineContext shouldEqualFolded originContext + mainDispatcher LifecycleScopeFactory.reset() - val default = LifecycleScopeFactory.create() + val default = LifecycleScopeFactory.create(lifecycleOwner.lifecycle) default.coroutineContext[DispatcherProvider]!!.shouldBeTypeOf() diff --git a/dispatch-android-lifecycle-extensions/src/test/java/dispatch/android/lifecycle/internal/AtomicGetOrPutTest.kt b/dispatch-android-lifecycle-extensions/src/test/java/dispatch/android/lifecycle/internal/AtomicGetOrPutTest.kt new file mode 100644 index 000000000..5a39884e7 --- /dev/null +++ b/dispatch-android-lifecycle-extensions/src/test/java/dispatch/android/lifecycle/internal/AtomicGetOrPutTest.kt @@ -0,0 +1,88 @@ +/* + * Copyright (C) 2020 Rick Busarow + * Licensed 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.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed 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 and + * limitations under the License. + */ + +package dispatch.android.lifecycle.internal + +import androidx.lifecycle.* +import dispatch.android.lifecycle.LifecycleCoroutineScope +import dispatch.core.* +import dispatch.internal.test.android.* +import dispatch.test.* +import io.kotest.core.spec.* +import io.kotest.core.spec.style.* +import io.kotest.matchers.* +import kotlinx.coroutines.* +import java.util.concurrent.* + +@ObsoleteCoroutinesApi +@ExperimentalCoroutinesApi +internal class AtomicGetOrPutTest : BehaviorSpec() { + + override fun isolationMode(): IsolationMode? = IsolationMode.InstancePerTest + + init { + + given("API level 23") { + + `when`("multiple threads access lifecycleScope at once") { + + then("all threads should get the same instance").config(invocations = 10) { + + val main = newSingleThreadContext("main") + + val storeMap = ConcurrentHashMap() + + val lifecycleOwner = FakeLifecycleOwner(Lifecycle.State.INITIALIZED) + + val androidLifecycle = lifecycleOwner.lifecycle + + val hugeExecutor = ThreadPoolExecutor( + 200, 200, 5000, TimeUnit.MILLISECONDS, LinkedBlockingQueue() + ).asCoroutineDispatcher() + + val lock = CompletableDeferred() + + val all = List(200) { + async(hugeExecutor) { + + lock.await() + + storeMap.atomicGetOrPut(androidLifecycle) { + val scope = LifecycleCoroutineScope( + lifecycle = androidLifecycle, + coroutineScope = MainImmediateCoroutineScope(Job(), TestDispatcherProvider(main)) + ) + withContext(main) { + + androidLifecycle.addObserver(LifecycleCoroutineScopeStore) + } + scope + } + } + } + + yield() + lock.complete(Unit) + + all.awaitAll().distinct().size shouldBe 1 + + delay(100) + + androidLifecycle.observerCount shouldBe 2 + } + } + } + } +} diff --git a/dispatch-android-lifecycle/README.md b/dispatch-android-lifecycle/README.md index eaac7edcb..cc3691aa9 100644 --- a/dispatch-android-lifecycle/README.md +++ b/dispatch-android-lifecycle/README.md @@ -1,4 +1,4 @@ -# MODULE dispatch-android-lifecycle +# Module dispatch-android-lifecycle [CoroutineScope] functionality linked with an [Android Lifecycle]. @@ -60,7 +60,6 @@ dependencies { implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.7") implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.7") implementation("com.rickbusarow.dispatch:dispatch-android-lifecycle:1.0.0-beta04") - implementation("androidx.lifecycle:lifecycle-common:2.2.0") } ``` diff --git a/dispatch-android-lifecycle/build.gradle.kts b/dispatch-android-lifecycle/build.gradle.kts index 799768914..9dd0b5408 100644 --- a/dispatch-android-lifecycle/build.gradle.kts +++ b/dispatch-android-lifecycle/build.gradle.kts @@ -47,7 +47,7 @@ dependencies { implementation(Libs.AndroidX.Fragment.core) implementation(Libs.AndroidX.Lifecycle.common) - testImplementation(Libs.AndroidX.Lifecycle.runtime) + implementation(Libs.AndroidX.Lifecycle.liveData) implementation(Libs.Kotlin.stdlib) @@ -78,6 +78,8 @@ dependencies { testImplementation(Libs.RickBusarow.Hermit.coroutines) testImplementation(Libs.RickBusarow.Hermit.junit5) - testImplementation(Libs.AndroidX.Test.runner) - testImplementation(Libs.AndroidX.Test.Espresso.core) + testImplementation(Libs.RickBusarow.Hermit.coroutines) + testImplementation(Libs.RickBusarow.Hermit.junit5) + + testImplementation(Libs.Robolectric.core) } diff --git a/dispatch-android-lifecycle/src/main/java/dispatch/android/lifecycle/LifecycleCoroutineScope.kt b/dispatch-android-lifecycle/src/main/java/dispatch/android/lifecycle/LifecycleCoroutineScope.kt index 472de2e66..cf82a8b36 100644 --- a/dispatch-android-lifecycle/src/main/java/dispatch/android/lifecycle/LifecycleCoroutineScope.kt +++ b/dispatch-android-lifecycle/src/main/java/dispatch/android/lifecycle/LifecycleCoroutineScope.kt @@ -19,6 +19,7 @@ import androidx.lifecycle.* import dispatch.android.lifecycle.internal.* import dispatch.core.* import kotlinx.coroutines.* +import java.util.concurrent.CancellationException /** * [MainImmediateCoroutineScope] instance which is tied to a [Lifecycle]. @@ -28,6 +29,9 @@ import kotlinx.coroutines.* * then automatically cancel upon the [lifecycle] dropping below that state. Reaching * that state again will start a new [Job]. * + * If this `CoroutineScope` has a [Job], it will be cancelled automatically + * as soon as the [lifecycle] reaches [DESTROYED][Lifecycle.State.DESTROYED]. + * * @sample samples.LifecycleCoroutineScopeSample.lifecycleCoroutineScopeSample * @param lifecycle the lifecycle to which this [MainImmediateCoroutineScope] is linked. */ @@ -36,6 +40,10 @@ class LifecycleCoroutineScope( private val coroutineScope: MainImmediateCoroutineScope ) : MainImmediateCoroutineScope by coroutineScope { + init { + LifecycleCoroutineScopeBinding(lifecycle, coroutineScope).bind() + } + /** * Lifecycle-aware function for launching a coroutine any time the [Lifecycle.State] * is at least [Lifecycle.State.CREATED]. @@ -129,3 +137,8 @@ class LifecycleCoroutineScope( RESTART_EVERY } } + +internal class LifecycleCancellationException( + lifecycle: Lifecycle, + minimumState: Lifecycle.State +) : CancellationException("Lifecycle $lifecycle dropped below minimum state: $minimumState") diff --git a/dispatch-android-lifecycle/src/main/java/dispatch/android/lifecycle/internal/LifecycleCoroutineScopeBinding.kt b/dispatch-android-lifecycle/src/main/java/dispatch/android/lifecycle/internal/LifecycleCoroutineScopeBinding.kt new file mode 100644 index 000000000..db44b7c65 --- /dev/null +++ b/dispatch-android-lifecycle/src/main/java/dispatch/android/lifecycle/internal/LifecycleCoroutineScopeBinding.kt @@ -0,0 +1,62 @@ +/* + * Copyright (C) 2020 Rick Busarow + * Licensed 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.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed 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 and + * limitations under the License. + */ + +package dispatch.android.lifecycle.internal + +import androidx.lifecycle.* +import dispatch.android.lifecycle.* +import dispatch.core.* +import kotlinx.coroutines.* + +internal class LifecycleCoroutineScopeBinding( + private val lifecycle: Lifecycle, + private val coroutineScope: CoroutineScope +) : LifecycleEventObserver { + + fun bind() { + + if (lifecycle.currentState == Lifecycle.State.DESTROYED) { + cancelDestroyed() + } else { + coroutineScope.launchMainImmediate { + + if (lifecycle.currentState == Lifecycle.State.DESTROYED) { + cancelDestroyed() + } else { + lifecycle.addObserver(this@LifecycleCoroutineScopeBinding) + } + } + } + coroutineScope.coroutineContext[Job]?.invokeOnCompletion { + lifecycle.removeObserver(this) + } + } + + private fun cancelDestroyed() { + lifecycle.removeObserver(this) + coroutineScope.coroutineContext.cancel( + LifecycleCancellationException( + lifecycle = lifecycle, + minimumState = Lifecycle.State.INITIALIZED + ) + ) + } + + override fun onStateChanged(source: LifecycleOwner, event: Lifecycle.Event) { + if (!lifecycle.currentState.isAtLeast(Lifecycle.State.INITIALIZED)) { + cancelDestroyed() + } + } +} diff --git a/dispatch-android-lifecycle/src/test/java/dispatch/android/lifecycle/LifecycleCoroutineScopeTest.kt b/dispatch-android-lifecycle/src/test/java/dispatch/android/lifecycle/LifecycleCoroutineScopeTest.kt index a9d436972..b0aab176b 100644 --- a/dispatch-android-lifecycle/src/test/java/dispatch/android/lifecycle/LifecycleCoroutineScopeTest.kt +++ b/dispatch-android-lifecycle/src/test/java/dispatch/android/lifecycle/LifecycleCoroutineScopeTest.kt @@ -27,7 +27,6 @@ import kotlinx.coroutines.flow.* import org.junit.jupiter.api.* @FlowPreview -@CoroutineTest @ExperimentalCoroutinesApi class LifecycleCoroutineScopeTest : HermitJUnit5() { @@ -37,6 +36,48 @@ class LifecycleCoroutineScopeTest : HermitJUnit5() { val lifecycle by resets { lifecycleOwner.lifecycle } val scope by resets { LifecycleCoroutineScope(lifecycle, testScope) } + @Nested + inner class cancellation { + + @Test + fun `scope with Job should cancel on init if lifecycle is destroyed`() = runBlocking { + + lifecycleOwner.destroy() + + val scope = LifecycleCoroutineScope(lifecycle, testScope) + + scope.isActive shouldBe false + } + + @Test + fun `scope should cancel when lifecycle is destroyed`() = runBlocking { + + lifecycleOwner.create() + + val scope = LifecycleCoroutineScope(lifecycle, testScope) + + scope.isActive shouldBe true + + lifecycleOwner.destroy() + + scope.isActive shouldBe false + } + + @Test + fun `lifecycle observer should be removed when scope is cancelled`() = runBlocking { + + lifecycleOwner.create() + + val scope = LifecycleCoroutineScope(lifecycle, testScope) + + lifecycle.observerCount shouldBe 1 + + scope.cancel() + + lifecycle.observerCount shouldBe 0 + } + } + @Nested inner class `launch on create` { @@ -55,8 +96,6 @@ class LifecycleCoroutineScopeTest : HermitJUnit5() { @Test fun `block should not immediately execute if screen is not created`() = runBlocking { - lifecycle.handleLifecycleEvent(Lifecycle.Event.ON_DESTROY) - var executed = false scope.launchOnCreate { executed = true } diff --git a/dispatch-detekt/src/test/java/dispatch/detekt/rules/AndroidxLifecycleScopeUsageTest.kt b/dispatch-detekt/src/test/java/dispatch/detekt/rules/AndroidxLifecycleScopeUsageTest.kt index 222256d44..5fe3ed15e 100644 --- a/dispatch-detekt/src/test/java/dispatch/detekt/rules/AndroidxLifecycleScopeUsageTest.kt +++ b/dispatch-detekt/src/test/java/dispatch/detekt/rules/AndroidxLifecycleScopeUsageTest.kt @@ -124,6 +124,3 @@ internal class AndroidXLifecycleScopeTest : FreeSpec( findings shouldBe emptyList() } }) - - - diff --git a/dispatch-internal-test-android/src/main/java/dispatch/internal/test/android/FakeLifecycle.kt b/dispatch-internal-test-android/src/main/java/dispatch/internal/test/android/FakeLifecycle.kt new file mode 100644 index 000000000..270db0a6f --- /dev/null +++ b/dispatch-internal-test-android/src/main/java/dispatch/internal/test/android/FakeLifecycle.kt @@ -0,0 +1,55 @@ +/* + * Copyright (C) 2020 Rick Busarow + * Licensed 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.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed 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 and + * limitations under the License. + */ + +package dispatch.internal.test.android + +import androidx.lifecycle.* +import kotlinx.coroutines.channels.* + +class FakeLifecycle(lifecycleOwner: FakeLifecycleOwner) : Lifecycle() { + + val delegate = LifecycleRegistry(lifecycleOwner) + + val observerEvents = Channel(1200) + + val observerCount: Int get() = delegate.observerCount + + sealed class ObserverEvent { + abstract val observer: LifecycleObserver + + data class Add(override val observer: LifecycleObserver) : ObserverEvent() + data class Remove(override val observer: LifecycleObserver) : ObserverEvent() + } + + override fun addObserver(observer: LifecycleObserver) { + delegate.addObserver(observer) + observerEvents.sendBlocking(ObserverEvent.Add(observer)) + } + + override fun removeObserver(observer: LifecycleObserver) { + delegate.removeObserver(observer) + observerEvents.sendBlocking(ObserverEvent.Remove(observer)) + } + + override fun getCurrentState(): State = delegate.currentState + fun setCurrentState(state: State) { + delegate.currentState = state + } + + fun handleLifecycleEvent(event: Event) { + delegate.handleLifecycleEvent(event) + } + +} diff --git a/dispatch-internal-test-android/src/main/java/dispatch/internal/test/android/FakeLifecycleOwner.kt b/dispatch-internal-test-android/src/main/java/dispatch/internal/test/android/FakeLifecycleOwner.kt index 4d6d1003d..6acdfe3c2 100644 --- a/dispatch-internal-test-android/src/main/java/dispatch/internal/test/android/FakeLifecycleOwner.kt +++ b/dispatch-internal-test-android/src/main/java/dispatch/internal/test/android/FakeLifecycleOwner.kt @@ -24,7 +24,7 @@ open class FakeLifecycleOwner( private val mainDispatcher: CoroutineDispatcher = fakeMainDispatcher() ) : LifecycleOwner { - private val registry: LifecycleRegistry by lazy { LifecycleRegistry(this) } + val fakeLifecycle: FakeLifecycle by lazy { FakeLifecycle(this) } init { when (initialState) { @@ -36,7 +36,7 @@ open class FakeLifecycleOwner( } } - override fun getLifecycle(): LifecycleRegistry = registry + override fun getLifecycle(): FakeLifecycle = fakeLifecycle fun stepDown() = when (lifecycle.currentState) { Lifecycle.State.DESTROYED -> throw IllegalArgumentException("already destroyed") @@ -84,7 +84,7 @@ open class FakeLifecycleOwner( lifecycle.handleLifecycleEvent(Lifecycle.Event.ON_DESTROY) } - fun getObserverCount(): Int = runBlocking(mainDispatcher) { registry.observerCount } + fun getObserverCount(): Int = runBlocking(mainDispatcher) { fakeLifecycle.observerCount } } @Suppress("EXPERIMENTAL_API_USAGE") diff --git a/dispatch-internal-test/build.gradle.kts b/dispatch-internal-test/build.gradle.kts index 44ea5875b..1d3197118 100644 --- a/dispatch-internal-test/build.gradle.kts +++ b/dispatch-internal-test/build.gradle.kts @@ -20,6 +20,7 @@ plugins { } dependencies { + implementation(Libs.Kotlin.reflect) implementation(Libs.Kotlin.stdlib) implementation(Libs.Kotlinx.Coroutines.core) diff --git a/dispatch-internal-test/src/main/java/dispatch/internal/test/reflect.kt b/dispatch-internal-test/src/main/java/dispatch/internal/test/reflect.kt new file mode 100644 index 000000000..cf13c3644 --- /dev/null +++ b/dispatch-internal-test/src/main/java/dispatch/internal/test/reflect.kt @@ -0,0 +1,45 @@ +/* + * Copyright (C) 2020 Rick Busarow + * Licensed 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.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed 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 and + * limitations under the License. + */ + +package dispatch.internal.test + +import kotlin.reflect.full.* +import kotlin.reflect.jvm.* + +inline fun T.getPrivateObjectFieldByName(name: String): R { + + val kClass = T::class + + val property = kClass.members.find { it.name == name } + + require(property != null) { "Cannot find a property named `$name` in ${kClass::qualifiedName}." } + + property.isAccessible = true + + return property.call() as R +} + +inline fun T.getPrivateFieldByName(name: String): R { + + val kClass = T::class + + val property = kClass.memberProperties.find { it.name == name } + + require(property != null) { "Cannot find a property named `$name` in ${kClass::qualifiedName}." } + + property.isAccessible = true + + return property.get(this) as R +} diff --git a/docs/dokka/dispatch-android-lifecycle-extensions/dispatch.android.lifecycle/-lifecycle-scope-factory/index.md b/docs/dokka/dispatch-android-lifecycle-extensions/dispatch.android.lifecycle/-lifecycle-scope-factory/index.md index a2cc498ba..d7325c705 100644 --- a/docs/dokka/dispatch-android-lifecycle-extensions/dispatch.android.lifecycle/-lifecycle-scope-factory/index.md +++ b/docs/dokka/dispatch-android-lifecycle-extensions/dispatch.android.lifecycle/-lifecycle-scope-factory/index.md @@ -2,7 +2,7 @@ # LifecycleScopeFactory -`object LifecycleScopeFactory` [(source)](https://github.com/RBusarow/Dispatch/tree/master/dispatch-android-lifecycle-extensions/src/main/java/dispatch/android/lifecycle/LifecycleScopeFactory.kt#L33) +`object LifecycleScopeFactory` [(source)](https://github.com/RBusarow/Dispatch/tree/master/dispatch-android-lifecycle-extensions/src/main/java/dispatch/android/lifecycle/LifecycleScopeFactory.kt#L34) Factory holder for [LifecycleCoroutineScope](https://rbusarow.github.io/Dispatch/dispatch-android-lifecycle/dispatch.android.lifecycle/-lifecycle-coroutine-scope/index.md)'s. diff --git a/docs/dokka/dispatch-android-lifecycle-extensions/dispatch.android.lifecycle/-lifecycle-scope-factory/reset.md b/docs/dokka/dispatch-android-lifecycle-extensions/dispatch.android.lifecycle/-lifecycle-scope-factory/reset.md index 336ba115e..8d1834205 100644 --- a/docs/dokka/dispatch-android-lifecycle-extensions/dispatch.android.lifecycle/-lifecycle-scope-factory/reset.md +++ b/docs/dokka/dispatch-android-lifecycle-extensions/dispatch.android.lifecycle/-lifecycle-scope-factory/reset.md @@ -2,7 +2,7 @@ # reset -`fun reset(): `[`Unit`](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-unit/index.html) [(source)](https://github.com/RBusarow/Dispatch/tree/master/dispatch-android-lifecycle-extensions/src/main/java/dispatch/android/lifecycle/LifecycleScopeFactory.kt#L57) +`fun reset(): `[`Unit`](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-unit/index.html) [(source)](https://github.com/RBusarow/Dispatch/tree/master/dispatch-android-lifecycle-extensions/src/main/java/dispatch/android/lifecycle/LifecycleScopeFactory.kt#L58) Immediately resets the factory function to its default. diff --git a/docs/dokka/dispatch-android-lifecycle-extensions/dispatch.android.lifecycle/-lifecycle-scope-factory/set.md b/docs/dokka/dispatch-android-lifecycle-extensions/dispatch.android.lifecycle/-lifecycle-scope-factory/set.md index 3c07f9f6d..00131d2d1 100644 --- a/docs/dokka/dispatch-android-lifecycle-extensions/dispatch.android.lifecycle/-lifecycle-scope-factory/set.md +++ b/docs/dokka/dispatch-android-lifecycle-extensions/dispatch.android.lifecycle/-lifecycle-scope-factory/set.md @@ -2,7 +2,7 @@ # set -`fun set(factory: () -> `[`MainImmediateCoroutineScope`](https://rbusarow.github.io/Dispatch/dispatch-core/dispatch.core/-main-immediate-coroutine-scope/index.md)`): `[`Unit`](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-unit/index.html) [(source)](https://github.com/RBusarow/Dispatch/tree/master/dispatch-android-lifecycle-extensions/src/main/java/dispatch/android/lifecycle/LifecycleScopeFactory.kt#L46) +`fun set(factory: () -> `[`MainImmediateCoroutineScope`](https://rbusarow.github.io/Dispatch/dispatch-core/dispatch.core/-main-immediate-coroutine-scope/index.md)`): `[`Unit`](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-unit/index.html) [(source)](https://github.com/RBusarow/Dispatch/tree/master/dispatch-android-lifecycle-extensions/src/main/java/dispatch/android/lifecycle/LifecycleScopeFactory.kt#L47) Override the default [MainImmediateCoroutineScope](https://rbusarow.github.io/Dispatch/dispatch-core/dispatch.core/-main-immediate-coroutine-scope/index.md) factory, for testing or to include a custom [CoroutineContext](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin.coroutines/-coroutine-context/index.html) in production code. This may be done in [Application.onCreate](https://developer.android.com/reference/android/app/Application.html#onCreate()) diff --git a/docs/dokka/dispatch-android-lifecycle-extensions/dispatch.android.lifecycle/androidx.lifecycle.-lifecycle-owner/lifecycle-scope.md b/docs/dokka/dispatch-android-lifecycle-extensions/dispatch.android.lifecycle/androidx.lifecycle.-lifecycle-owner/lifecycle-scope.md index dccaa185a..d2cde88d0 100644 --- a/docs/dokka/dispatch-android-lifecycle-extensions/dispatch.android.lifecycle/androidx.lifecycle.-lifecycle-owner/lifecycle-scope.md +++ b/docs/dokka/dispatch-android-lifecycle-extensions/dispatch.android.lifecycle/androidx.lifecycle.-lifecycle-owner/lifecycle-scope.md @@ -2,7 +2,7 @@ # lifecycleScope -`val `[`LifecycleOwner`](https://developer.android.com/reference/androidx/androidx/lifecycle/LifecycleOwner.html)`.lifecycleScope: `[`LifecycleCoroutineScope`](https://rbusarow.github.io/Dispatch/dispatch-android-lifecycle/dispatch.android.lifecycle/-lifecycle-coroutine-scope/index.md) [(source)](https://github.com/RBusarow/Dispatch/tree/master/dispatch-android-lifecycle-extensions/src/main/java/dispatch/android/lifecycle/LifecycleScope.kt#L35) +`val `[`LifecycleOwner`](https://developer.android.com/reference/androidx/androidx/lifecycle/LifecycleOwner.html)`.lifecycleScope: `[`LifecycleCoroutineScope`](https://rbusarow.github.io/Dispatch/dispatch-android-lifecycle/dispatch.android.lifecycle/-lifecycle-coroutine-scope/index.md) [(source)](https://github.com/RBusarow/Dispatch/tree/master/dispatch-android-lifecycle-extensions/src/main/java/dispatch/android/lifecycle/lifecycleScope.kt#L36) [CoroutineScope](https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-coroutine-scope/index.html) instance for the [LifecycleOwner](https://developer.android.com/reference/androidx/androidx/lifecycle/LifecycleOwner.html). By default, it uses the [Dispatchers.Main.immediate](https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-main-coroutine-dispatcher/immediate.html) dispatcher. @@ -12,7 +12,8 @@ from the factory set in [LifecycleScopeFactory](https://rbusarow.github.io/Dispa The type of [CoroutineScope](https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-coroutine-scope/index.html) created is configurable via [LifecycleScopeFactory.set](https://rbusarow.github.io/Dispatch/dispatch-android-lifecycle/dispatch.android.lifecycle/-lifecycle-scope-factory/set.md). -The `viewModelScope` is automatically cancelled when the [LifecycleOwner](https://developer.android.com/reference/androidx/androidx/lifecycle/LifecycleOwner.html)'s [lifecycle](https://developer.android.com/reference/androidx/androidx/lifecycle/LifecycleOwner.html#getLifecycle())'s [Lifecycle.State](https://developer.android.com/reference/androidx/androidx/lifecycle/Lifecycle/State.html) drops to [Lifecycle.State.DESTROYED](https://developer.android.com/reference/androidx/androidx/lifecycle/Lifecycle/State.html#DESTROYED). +The `viewModelScope` is automatically cancelled when the [LifecycleOwner](https://developer.android.com/reference/androidx/androidx/lifecycle/LifecycleOwner.html)'s +[lifecycle](https://developer.android.com/reference/androidx/androidx/lifecycle/LifecycleOwner.html#getLifecycle())'s [Lifecycle.State](https://developer.android.com/reference/androidx/androidx/lifecycle/Lifecycle/State.html) drops to [Lifecycle.State.DESTROYED](https://developer.android.com/reference/androidx/androidx/lifecycle/Lifecycle/State.html#DESTROYED). ``` kotlin // This could be any LifecycleOwner -- Fragments, Activities, Services... diff --git a/docs/dokka/dispatch-android-lifecycle/alltypes/index.md b/docs/dokka/dispatch-android-lifecycle/alltypes/index.md index 2ba32a4e7..febfd574d 100644 --- a/docs/dokka/dispatch-android-lifecycle/alltypes/index.md +++ b/docs/dokka/dispatch-android-lifecycle/alltypes/index.md @@ -1,5 +1,7 @@ +[CoroutineScope](https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/coroutine-scope.html) functionality linked with an [Android Lifecycle](https://developer.android.com/reference/androidx/lifecycle/Lifecycle.html). + ### All Types | Name | Summary | diff --git a/docs/dokka/dispatch-android-lifecycle/dispatch.android.lifecycle/-lifecycle-coroutine-scope/-init-.md b/docs/dokka/dispatch-android-lifecycle/dispatch.android.lifecycle/-lifecycle-coroutine-scope/-init-.md index 9f26c5960..9553837ce 100644 --- a/docs/dokka/dispatch-android-lifecycle/dispatch.android.lifecycle/-lifecycle-coroutine-scope/-init-.md +++ b/docs/dokka/dispatch-android-lifecycle/dispatch.android.lifecycle/-lifecycle-coroutine-scope/-init-.md @@ -11,6 +11,9 @@ which will automatically start upon reaching their associated [Lifecycle.Event]( then automatically cancel upon the [lifecycle](lifecycle.md) dropping below that state. Reaching that state again will start a new [Job](https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-job/index.html). +If this `CoroutineScope` has a [Job](https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-job/index.html), it will be cancelled automatically +as soon as the [lifecycle](lifecycle.md) reaches [DESTROYED](https://developer.android.com/reference/androidx/androidx/lifecycle/Lifecycle/State.html#DESTROYED). + ``` kotlin runBlocking { diff --git a/docs/dokka/dispatch-android-lifecycle/dispatch.android.lifecycle/-lifecycle-coroutine-scope/-minimum-state-policy/-c-a-n-c-e-l.md b/docs/dokka/dispatch-android-lifecycle/dispatch.android.lifecycle/-lifecycle-coroutine-scope/-minimum-state-policy/-c-a-n-c-e-l.md index 89943bb3d..fb84b15c0 100644 --- a/docs/dokka/dispatch-android-lifecycle/dispatch.android.lifecycle/-lifecycle-coroutine-scope/-minimum-state-policy/-c-a-n-c-e-l.md +++ b/docs/dokka/dispatch-android-lifecycle/dispatch.android.lifecycle/-lifecycle-coroutine-scope/-minimum-state-policy/-c-a-n-c-e-l.md @@ -2,7 +2,7 @@ # CANCEL -`CANCEL` [(source)](https://github.com/RBusarow/Dispatch/tree/master/dispatch-android-lifecycle/src/main/java/dispatch/android/lifecycle/LifecycleCoroutineScope.kt#L116) +`CANCEL` [(source)](https://github.com/RBusarow/Dispatch/tree/master/dispatch-android-lifecycle/src/main/java/dispatch/android/lifecycle/LifecycleCoroutineScope.kt#L124) When using `CANCEL`, a coroutine will be created the first time the [lifecycle](../lifecycle.md) meets the minimum state, and cancelled upon dropping below it. diff --git a/docs/dokka/dispatch-android-lifecycle/dispatch.android.lifecycle/-lifecycle-coroutine-scope/-minimum-state-policy/-r-e-s-t-a-r-t_-e-v-e-r-y.md b/docs/dokka/dispatch-android-lifecycle/dispatch.android.lifecycle/-lifecycle-coroutine-scope/-minimum-state-policy/-r-e-s-t-a-r-t_-e-v-e-r-y.md index d4aa696fb..c121e9425 100644 --- a/docs/dokka/dispatch-android-lifecycle/dispatch.android.lifecycle/-lifecycle-coroutine-scope/-minimum-state-policy/-r-e-s-t-a-r-t_-e-v-e-r-y.md +++ b/docs/dokka/dispatch-android-lifecycle/dispatch.android.lifecycle/-lifecycle-coroutine-scope/-minimum-state-policy/-r-e-s-t-a-r-t_-e-v-e-r-y.md @@ -2,7 +2,7 @@ # RESTART_EVERY -`RESTART_EVERY` [(source)](https://github.com/RBusarow/Dispatch/tree/master/dispatch-android-lifecycle/src/main/java/dispatch/android/lifecycle/LifecycleCoroutineScope.kt#L129) +`RESTART_EVERY` [(source)](https://github.com/RBusarow/Dispatch/tree/master/dispatch-android-lifecycle/src/main/java/dispatch/android/lifecycle/LifecycleCoroutineScope.kt#L137) When using `RESTART_EVERY`, a coroutine will be created every time the [lifecycle](../lifecycle.md) meets the minimum state, and will be cancelled upon dropping below it. diff --git a/docs/dokka/dispatch-android-lifecycle/dispatch.android.lifecycle/-lifecycle-coroutine-scope/-minimum-state-policy/index.md b/docs/dokka/dispatch-android-lifecycle/dispatch.android.lifecycle/-lifecycle-coroutine-scope/-minimum-state-policy/index.md index f6a98ece8..33878daad 100644 --- a/docs/dokka/dispatch-android-lifecycle/dispatch.android.lifecycle/-lifecycle-coroutine-scope/-minimum-state-policy/index.md +++ b/docs/dokka/dispatch-android-lifecycle/dispatch.android.lifecycle/-lifecycle-coroutine-scope/-minimum-state-policy/index.md @@ -2,7 +2,7 @@ # MinimumStatePolicy -`enum class MinimumStatePolicy` [(source)](https://github.com/RBusarow/Dispatch/tree/master/dispatch-android-lifecycle/src/main/java/dispatch/android/lifecycle/LifecycleCoroutineScope.kt#L106) +`enum class MinimumStatePolicy` [(source)](https://github.com/RBusarow/Dispatch/tree/master/dispatch-android-lifecycle/src/main/java/dispatch/android/lifecycle/LifecycleCoroutineScope.kt#L114) Describes the way a particular [Job](https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-job/index.html) will behave if the [lifecycle](../lifecycle.md) passes below the minimum state before said [Job](https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-job/index.html) has completed. diff --git a/docs/dokka/dispatch-android-lifecycle/dispatch.android.lifecycle/-lifecycle-coroutine-scope/index.md b/docs/dokka/dispatch-android-lifecycle/dispatch.android.lifecycle/-lifecycle-coroutine-scope/index.md index a1707b1f9..8ba3572ce 100644 --- a/docs/dokka/dispatch-android-lifecycle/dispatch.android.lifecycle/-lifecycle-coroutine-scope/index.md +++ b/docs/dokka/dispatch-android-lifecycle/dispatch.android.lifecycle/-lifecycle-coroutine-scope/index.md @@ -2,7 +2,7 @@ # LifecycleCoroutineScope -`class LifecycleCoroutineScope : `[`MainImmediateCoroutineScope`](https://rbusarow.github.io/Dispatch/dispatch-core/dispatch.core/-main-immediate-coroutine-scope/index.md) [(source)](https://github.com/RBusarow/Dispatch/tree/master/dispatch-android-lifecycle/src/main/java/dispatch/android/lifecycle/LifecycleCoroutineScope.kt#L34) +`class LifecycleCoroutineScope : `[`MainImmediateCoroutineScope`](https://rbusarow.github.io/Dispatch/dispatch-core/dispatch.core/-main-immediate-coroutine-scope/index.md) [(source)](https://github.com/RBusarow/Dispatch/tree/master/dispatch-android-lifecycle/src/main/java/dispatch/android/lifecycle/LifecycleCoroutineScope.kt#L38) [MainImmediateCoroutineScope](https://rbusarow.github.io/Dispatch/dispatch-core/dispatch.core/-main-immediate-coroutine-scope/index.md) instance which is tied to a [Lifecycle](https://developer.android.com/reference/androidx/androidx/lifecycle/Lifecycle.html). @@ -11,6 +11,9 @@ which will automatically start upon reaching their associated [Lifecycle.Event]( then automatically cancel upon the [lifecycle](lifecycle.md) dropping below that state. Reaching that state again will start a new [Job](https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-job/index.html). +If this `CoroutineScope` has a [Job](https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-job/index.html), it will be cancelled automatically +as soon as the [lifecycle](lifecycle.md) reaches [DESTROYED](https://developer.android.com/reference/androidx/androidx/lifecycle/Lifecycle/State.html#DESTROYED). + ``` kotlin runBlocking { diff --git a/docs/dokka/dispatch-android-lifecycle/dispatch.android.lifecycle/-lifecycle-coroutine-scope/launch-on-create.md b/docs/dokka/dispatch-android-lifecycle/dispatch.android.lifecycle/-lifecycle-coroutine-scope/launch-on-create.md index 80c0e22b9..ba1e75ded 100644 --- a/docs/dokka/dispatch-android-lifecycle/dispatch.android.lifecycle/-lifecycle-coroutine-scope/launch-on-create.md +++ b/docs/dokka/dispatch-android-lifecycle/dispatch.android.lifecycle/-lifecycle-coroutine-scope/launch-on-create.md @@ -2,7 +2,7 @@ # launchOnCreate -`fun launchOnCreate(minimumStatePolicy: MinimumStatePolicy = MinimumStatePolicy.RESTART_EVERY, block: suspend `[`CoroutineScope`](https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-coroutine-scope/index.html)`.() -> `[`Unit`](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-unit/index.html)`): `[`Job`](https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-job/index.html) [(source)](https://github.com/RBusarow/Dispatch/tree/master/dispatch-android-lifecycle/src/main/java/dispatch/android/lifecycle/LifecycleCoroutineScope.kt#L57) +`fun launchOnCreate(minimumStatePolicy: MinimumStatePolicy = MinimumStatePolicy.RESTART_EVERY, block: suspend `[`CoroutineScope`](https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-coroutine-scope/index.html)`.() -> `[`Unit`](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-unit/index.html)`): `[`Job`](https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-job/index.html) [(source)](https://github.com/RBusarow/Dispatch/tree/master/dispatch-android-lifecycle/src/main/java/dispatch/android/lifecycle/LifecycleCoroutineScope.kt#L65) Lifecycle-aware function for launching a coroutine any time the [Lifecycle.State](https://developer.android.com/reference/androidx/androidx/lifecycle/Lifecycle/State.html) is at least [Lifecycle.State.CREATED](https://developer.android.com/reference/androidx/androidx/lifecycle/Lifecycle/State.html#CREATED). diff --git a/docs/dokka/dispatch-android-lifecycle/dispatch.android.lifecycle/-lifecycle-coroutine-scope/launch-on-resume.md b/docs/dokka/dispatch-android-lifecycle/dispatch.android.lifecycle/-lifecycle-coroutine-scope/launch-on-resume.md index 57b557aac..c2eaaf680 100644 --- a/docs/dokka/dispatch-android-lifecycle/dispatch.android.lifecycle/-lifecycle-coroutine-scope/launch-on-resume.md +++ b/docs/dokka/dispatch-android-lifecycle/dispatch.android.lifecycle/-lifecycle-coroutine-scope/launch-on-resume.md @@ -2,7 +2,7 @@ # launchOnResume -`fun launchOnResume(minimumStatePolicy: MinimumStatePolicy = MinimumStatePolicy.RESTART_EVERY, block: suspend `[`CoroutineScope`](https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-coroutine-scope/index.html)`.() -> `[`Unit`](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-unit/index.html)`): `[`Job`](https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-job/index.html) [(source)](https://github.com/RBusarow/Dispatch/tree/master/dispatch-android-lifecycle/src/main/java/dispatch/android/lifecycle/LifecycleCoroutineScope.kt#L97) +`fun launchOnResume(minimumStatePolicy: MinimumStatePolicy = MinimumStatePolicy.RESTART_EVERY, block: suspend `[`CoroutineScope`](https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-coroutine-scope/index.html)`.() -> `[`Unit`](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-unit/index.html)`): `[`Job`](https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-job/index.html) [(source)](https://github.com/RBusarow/Dispatch/tree/master/dispatch-android-lifecycle/src/main/java/dispatch/android/lifecycle/LifecycleCoroutineScope.kt#L105) Lifecycle-aware function for launching a coroutine any time the [Lifecycle.State](https://developer.android.com/reference/androidx/androidx/lifecycle/Lifecycle/State.html) is at least [Lifecycle.State.RESUMED](https://developer.android.com/reference/androidx/androidx/lifecycle/Lifecycle/State.html#RESUMED). diff --git a/docs/dokka/dispatch-android-lifecycle/dispatch.android.lifecycle/-lifecycle-coroutine-scope/launch-on-start.md b/docs/dokka/dispatch-android-lifecycle/dispatch.android.lifecycle/-lifecycle-coroutine-scope/launch-on-start.md index 220a0ec17..796260208 100644 --- a/docs/dokka/dispatch-android-lifecycle/dispatch.android.lifecycle/-lifecycle-coroutine-scope/launch-on-start.md +++ b/docs/dokka/dispatch-android-lifecycle/dispatch.android.lifecycle/-lifecycle-coroutine-scope/launch-on-start.md @@ -2,7 +2,7 @@ # launchOnStart -`fun launchOnStart(minimumStatePolicy: MinimumStatePolicy = MinimumStatePolicy.RESTART_EVERY, block: suspend `[`CoroutineScope`](https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-coroutine-scope/index.html)`.() -> `[`Unit`](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-unit/index.html)`): `[`Job`](https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-job/index.html) [(source)](https://github.com/RBusarow/Dispatch/tree/master/dispatch-android-lifecycle/src/main/java/dispatch/android/lifecycle/LifecycleCoroutineScope.kt#L77) +`fun launchOnStart(minimumStatePolicy: MinimumStatePolicy = MinimumStatePolicy.RESTART_EVERY, block: suspend `[`CoroutineScope`](https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-coroutine-scope/index.html)`.() -> `[`Unit`](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-unit/index.html)`): `[`Job`](https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-job/index.html) [(source)](https://github.com/RBusarow/Dispatch/tree/master/dispatch-android-lifecycle/src/main/java/dispatch/android/lifecycle/LifecycleCoroutineScope.kt#L85) Lifecycle-aware function for launching a coroutine any time the [Lifecycle.State](https://developer.android.com/reference/androidx/androidx/lifecycle/Lifecycle/State.html) is at least [Lifecycle.State.STARTED](https://developer.android.com/reference/androidx/androidx/lifecycle/Lifecycle/State.html#STARTED). diff --git a/docs/dokka/dispatch-android-lifecycle/dispatch.android.lifecycle/-lifecycle-coroutine-scope/lifecycle.md b/docs/dokka/dispatch-android-lifecycle/dispatch.android.lifecycle/-lifecycle-coroutine-scope/lifecycle.md index 491e848ac..2958501ee 100644 --- a/docs/dokka/dispatch-android-lifecycle/dispatch.android.lifecycle/-lifecycle-coroutine-scope/lifecycle.md +++ b/docs/dokka/dispatch-android-lifecycle/dispatch.android.lifecycle/-lifecycle-coroutine-scope/lifecycle.md @@ -2,7 +2,7 @@ # lifecycle -`val lifecycle: `[`Lifecycle`](https://developer.android.com/reference/androidx/androidx/lifecycle/Lifecycle.html) [(source)](https://github.com/RBusarow/Dispatch/tree/master/dispatch-android-lifecycle/src/main/java/dispatch/android/lifecycle/LifecycleCoroutineScope.kt#L35) +`val lifecycle: `[`Lifecycle`](https://developer.android.com/reference/androidx/androidx/lifecycle/Lifecycle.html) [(source)](https://github.com/RBusarow/Dispatch/tree/master/dispatch-android-lifecycle/src/main/java/dispatch/android/lifecycle/LifecycleCoroutineScope.kt#L39) the lifecycle to which this [MainImmediateCoroutineScope](https://rbusarow.github.io/Dispatch/dispatch-core/dispatch.core/-main-immediate-coroutine-scope/index.md) is linked. diff --git a/docs/dokka/dispatch-android-lifecycle/index.md b/docs/dokka/dispatch-android-lifecycle/index.md index ed726e0d6..49f7bad3e 100644 --- a/docs/dokka/dispatch-android-lifecycle/index.md +++ b/docs/dokka/dispatch-android-lifecycle/index.md @@ -1,5 +1,65 @@ [dispatch-android-lifecycle](./index.md) +[CoroutineScope](https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/coroutine-scope.html) functionality linked with an [Android Lifecycle](https://developer.android.com/reference/androidx/lifecycle/Lifecycle.html). + +## Contents + +* [Types](#types) +* [Member functions](#member-functions) +* [Extension functions](#extension-functions) +* [Minimum Gradle Config](#minimum-gradle-config) + +## Types + +| **Name** | **Description** +| ------------- | --------------- | +| [LifecycleCoroutineScope](https://rbusarow.github.io/Dispatch/dispatch-android-lifecycle//dispatch.android.lifecycle/-lifecycle-coroutine-scope/index.html) | [MainCoroutineScope](https://rbusarow.github.io/Dispatch/dispatch-core//dispatch.core/-main-coroutine-scope.html) with a [Lifecycle](https://developer.android.com/reference/androidx/lifecycle/Lifecycle.html), capable of automatically cancelling and restarting coroutines along with that lifecycle. +| [MinimumStatePolicy](https://rbusarow.github.io/Dispatch/dispatch-android-lifecycle//dispatch.android.lifecycle/-lifecycle-coroutine-scope/-minimum-state-policy/index.html) | Defines the behavior of a [Lifecycle](https://developer.android.com/reference/androidx/lifecycle/Lifecycle.html)-aware [Job](https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-job/index.html) when it passes below its minimum [Lifecycle.State](https://developer.android.com/reference/androidx/lifecycle/Lifecycle.State.html) + +## Member functions + +| **Name** | **Description** +| ------------- | --------------- | +| [launchOnCreate](https://rbusarow.github.io/Dispatch/dispatch-android-lifecycle//dispatch.android.lifecycle/-lifecycle-coroutine-scope/launch-on-create.html) | Creates a coroutine tied to a [Lifecycle](https://developer.android.com/reference/androidx/lifecycle/Lifecycle.html) which will automatically enact a [MinimumStatePolicy](https://rbusarow.github.io/Dispatch/dispatch-android-lifecycle//dispatch.android.lifecycle/-lifecycle-coroutine-scope/-minimum-state-policy/index.html) upon dropping below [Lifecycle.State.CREATED](https://developer.android.com/reference/androidx/lifecycle/Lifecycle.State.html#CREATED) +| [launchOnStart](https://rbusarow.github.io/Dispatch/dispatch-android-lifecycle//dispatch.android.lifecycle/-lifecycle-coroutine-scope/launch-on-start.html) | Creates a coroutine tied to a [Lifecycle](https://developer.android.com/reference/androidx/lifecycle/Lifecycle.html) which will automatically enact a [MinimumStatePolicy](https://rbusarow.github.io/Dispatch/dispatch-android-lifecycle//dispatch.android.lifecycle/-lifecycle-coroutine-scope/-minimum-state-policy/index.html) upon dropping below [Lifecycle.State.STARTED](https://developer.android.com/reference/androidx/lifecycle/Lifecycle.State.html#STARTED) +| [launchOnResume](https://rbusarow.github.io/Dispatch/dispatch-android-lifecycle//dispatch.android.lifecycle/-lifecycle-coroutine-scope/launch-on-resume.html) | Creates a coroutine tied to a [Lifecycle](https://developer.android.com/reference/androidx/lifecycle/Lifecycle.html) which will automatically enact a [MinimumStatePolicy](https://rbusarow.github.io/Dispatch/dispatch-android-lifecycle//dispatch.android.lifecycle/-lifecycle-coroutine-scope/-minimum-state-policy/index.html) upon dropping below [Lifecycle.State.RESUMED](https://developer.android.com/reference/androidx/lifecycle/Lifecycle.State.html#RESUMED) + +## Extension functions + +[LifecycleOwner](https://developer.android.com/reference/androidx/lifecycle/LifecycleOwner.html) extension suspending functions: + +| **Name** | **Description** +| ------------------- | --------------- +| [onNextCreate](https://rbusarow.github.io/Dispatch/dispatch-android-lifecycle//dispatch.android.lifecycle/androidx.lifecycle.-lifecycle-owner/on-next-create.html) | Executes code one time upon reaching a state of [Lifecycle.State.CREATED](https://developer.android.com/reference/androidx/lifecycle/Lifecycle.State.html#CREATED) +| [onNextStart](https://rbusarow.github.io/Dispatch/dispatch-android-lifecycle//dispatch.android.lifecycle/androidx.lifecycle.-lifecycle-owner/on-next-start.html) | Executes code one time upon reaching a state of [Lifecycle.State.STARTED](https://developer.android.com/reference/androidx/lifecycle/Lifecycle.State.html#STARTED) +| [onNextResume](https://rbusarow.github.io/Dispatch/dispatch-android-lifecycle//dispatch.android.lifecycle/androidx.lifecycle.-lifecycle-owner/on-next-resume.html) | Executes code one time upon reaching a state of [Lifecycle.State.RESUMED](https://developer.android.com/reference/androidx/lifecycle/Lifecycle.State.html#RESUMED) + +[Lifecycle](https://developer.android.com/reference/androidx/lifecycle/Lifecycle.html) extension suspending functions: + +| **Name** | **Description** +| ------------------- | --------------- +| [onNextCreate](https://rbusarow.github.io/Dispatch/dispatch-android-lifecycle//dispatch.android.lifecycle/androidx.lifecycle.-lifecycle/on-next-create.html) | Executes code one time upon reaching a state of [Lifecycle.State.CREATED](https://developer.android.com/reference/androidx/lifecycle/Lifecycle.State.html#CREATED) +| [onNextStart](https://rbusarow.github.io/Dispatch/dispatch-android-lifecycle//dispatch.android.lifecycle/androidx.lifecycle.-lifecycle/on-next-start.html) | Executes code one time upon reaching a state of [Lifecycle.State.STARTED](https://developer.android.com/reference/androidx/lifecycle/Lifecycle.State.html#STARTED) +| [onNextResume](https://rbusarow.github.io/Dispatch/dispatch-android-lifecycle//dispatch.android.lifecycle/androidx.lifecycle.-lifecycle/on-next-resume.html) | Executes code one time upon reaching a state of [Lifecycle.State.RESUMED](https://developer.android.com/reference/androidx/lifecycle/Lifecycle.State.html#RESUMED) + +## Minimum Gradle Config + +Add to your module's `build.gradle.kts`: + +``` kotlin +repositories { + mavenCentral() +} + +dependencies { + + implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.7") + implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.7") + implementation("com.rickbusarow.dispatch:dispatch-android-lifecycle:1.0.0-beta04") + implementation("androidx.lifecycle:lifecycle-common:2.2.0") +} +``` + ### Packages | Name | Summary | diff --git a/docs/modules/dispatch-android-lifecycle.md b/docs/modules/dispatch-android-lifecycle.md index eaac7edcb..cc3691aa9 100644 --- a/docs/modules/dispatch-android-lifecycle.md +++ b/docs/modules/dispatch-android-lifecycle.md @@ -1,4 +1,4 @@ -# MODULE dispatch-android-lifecycle +# Module dispatch-android-lifecycle [CoroutineScope] functionality linked with an [Android Lifecycle]. @@ -60,7 +60,6 @@ dependencies { implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.7") implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.7") implementation("com.rickbusarow.dispatch:dispatch-android-lifecycle:1.0.0-beta04") - implementation("androidx.lifecycle:lifecycle-common:2.2.0") } ```