From b08b4939384ed3eff906cd92af8468270419b719 Mon Sep 17 00:00:00 2001 From: Manuel Vivo Date: Thu, 22 Apr 2021 12:04:32 +0000 Subject: [PATCH] Remove LifecycleOwner.addRepeatingJob API Relnote: """The `LifecycleOwner.addRepeatingJob` API is removed in favor of `Lifecycle.repeatOnLifecycle` that respects structured concurrency and is easier to reason about. """ Change-Id: I4a3a878686a1b2153dc97778f7942bb3624d6915 --- .../integration/testapp/PipActivity.kt | 10 +- .../lifecycle-runtime-ktx/api/current.txt | 2 +- .../api/public_plus_experimental_current.txt | 2 +- .../api/restricted_current.txt | 2 +- .../lifecycle/FlowWithLifecycleTest.kt | 33 ++++ ...ingJobTest.kt => RepeatOnLifecycleTest.kt} | 143 +++++++++++------- .../main/java/androidx/lifecycle/FlowExt.kt | 17 ++- .../main/java/androidx/lifecycle/Lifecycle.kt | 15 ++ .../androidx/lifecycle/RepeatOnLifecycle.kt | 84 +++++----- 9 files changed, 201 insertions(+), 107 deletions(-) rename lifecycle/lifecycle-runtime-ktx/src/androidTest/java/androidx/lifecycle/{AddRepeatingJobTest.kt => RepeatOnLifecycleTest.kt} (71%) diff --git a/activity/integration-tests/testapp/src/main/java/androidx/activity/integration/testapp/PipActivity.kt b/activity/integration-tests/testapp/src/main/java/androidx/activity/integration/testapp/PipActivity.kt index 0f1f268a21a95..056b797323647 100644 --- a/activity/integration-tests/testapp/src/main/java/androidx/activity/integration/testapp/PipActivity.kt +++ b/activity/integration-tests/testapp/src/main/java/androidx/activity/integration/testapp/PipActivity.kt @@ -28,8 +28,10 @@ import androidx.activity.ComponentActivity import androidx.activity.trackPipAnimationHintView import androidx.annotation.RequiresApi import androidx.lifecycle.Lifecycle -import androidx.lifecycle.addRepeatingJob +import androidx.lifecycle.lifecycleScope +import androidx.lifecycle.repeatOnLifecycle import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.launch @ExperimentalCoroutinesApi class PipActivity : ComponentActivity() { @@ -61,8 +63,10 @@ class PipActivity : ComponentActivity() { @ExperimentalCoroutinesApi private fun trackHintView() { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - addRepeatingJob(Lifecycle.State.STARTED) { - trackPipAnimationHintView(moveButton) + lifecycleScope.launch { + repeatOnLifecycle(Lifecycle.State.STARTED) { + trackPipAnimationHintView(moveButton) + } } } } diff --git a/lifecycle/lifecycle-runtime-ktx/api/current.txt b/lifecycle/lifecycle-runtime-ktx/api/current.txt index daba2bb2fc47e..13b58cdb735fd 100644 --- a/lifecycle/lifecycle-runtime-ktx/api/current.txt +++ b/lifecycle/lifecycle-runtime-ktx/api/current.txt @@ -34,8 +34,8 @@ package androidx.lifecycle { } public final class RepeatOnLifecycleKt { - method public static kotlinx.coroutines.Job addRepeatingJob(androidx.lifecycle.LifecycleOwner, androidx.lifecycle.Lifecycle.State state, optional kotlin.coroutines.CoroutineContext coroutineContext, kotlin.jvm.functions.Function2,?> block); method public static suspend Object? repeatOnLifecycle(androidx.lifecycle.Lifecycle, androidx.lifecycle.Lifecycle.State state, kotlin.jvm.functions.Function2,?> block, kotlin.coroutines.Continuation p); + method public static suspend Object? repeatOnLifecycle(androidx.lifecycle.LifecycleOwner, androidx.lifecycle.Lifecycle.State state, kotlin.jvm.functions.Function2,?> block, kotlin.coroutines.Continuation p); } public final class ViewKt { diff --git a/lifecycle/lifecycle-runtime-ktx/api/public_plus_experimental_current.txt b/lifecycle/lifecycle-runtime-ktx/api/public_plus_experimental_current.txt index daba2bb2fc47e..13b58cdb735fd 100644 --- a/lifecycle/lifecycle-runtime-ktx/api/public_plus_experimental_current.txt +++ b/lifecycle/lifecycle-runtime-ktx/api/public_plus_experimental_current.txt @@ -34,8 +34,8 @@ package androidx.lifecycle { } public final class RepeatOnLifecycleKt { - method public static kotlinx.coroutines.Job addRepeatingJob(androidx.lifecycle.LifecycleOwner, androidx.lifecycle.Lifecycle.State state, optional kotlin.coroutines.CoroutineContext coroutineContext, kotlin.jvm.functions.Function2,?> block); method public static suspend Object? repeatOnLifecycle(androidx.lifecycle.Lifecycle, androidx.lifecycle.Lifecycle.State state, kotlin.jvm.functions.Function2,?> block, kotlin.coroutines.Continuation p); + method public static suspend Object? repeatOnLifecycle(androidx.lifecycle.LifecycleOwner, androidx.lifecycle.Lifecycle.State state, kotlin.jvm.functions.Function2,?> block, kotlin.coroutines.Continuation p); } public final class ViewKt { diff --git a/lifecycle/lifecycle-runtime-ktx/api/restricted_current.txt b/lifecycle/lifecycle-runtime-ktx/api/restricted_current.txt index 72594e05bdd05..f721a9e0fa503 100644 --- a/lifecycle/lifecycle-runtime-ktx/api/restricted_current.txt +++ b/lifecycle/lifecycle-runtime-ktx/api/restricted_current.txt @@ -34,8 +34,8 @@ package androidx.lifecycle { } public final class RepeatOnLifecycleKt { - method public static kotlinx.coroutines.Job addRepeatingJob(androidx.lifecycle.LifecycleOwner, androidx.lifecycle.Lifecycle.State state, optional kotlin.coroutines.CoroutineContext coroutineContext, kotlin.jvm.functions.Function2,?> block); method public static suspend Object? repeatOnLifecycle(androidx.lifecycle.Lifecycle, androidx.lifecycle.Lifecycle.State state, kotlin.jvm.functions.Function2,?> block, kotlin.coroutines.Continuation p); + method public static suspend Object? repeatOnLifecycle(androidx.lifecycle.LifecycleOwner, androidx.lifecycle.Lifecycle.State state, kotlin.jvm.functions.Function2,?> block, kotlin.coroutines.Continuation p); } public final class ViewKt { diff --git a/lifecycle/lifecycle-runtime-ktx/src/androidTest/java/androidx/lifecycle/FlowWithLifecycleTest.kt b/lifecycle/lifecycle-runtime-ktx/src/androidTest/java/androidx/lifecycle/FlowWithLifecycleTest.kt index 1020f5d8ddc4d..d6e6ed39eb666 100644 --- a/lifecycle/lifecycle-runtime-ktx/src/androidTest/java/androidx/lifecycle/FlowWithLifecycleTest.kt +++ b/lifecycle/lifecycle-runtime-ktx/src/androidTest/java/androidx/lifecycle/FlowWithLifecycleTest.kt @@ -172,6 +172,39 @@ class FlowWithLifecycleTest { owner.setState(Lifecycle.State.DESTROYED) } + @Test + fun testOnEachBeforeOperatorOnlyExecutesInTheRightState() = runBlocking(Dispatchers.Main) { + owner.setState(Lifecycle.State.RESUMED) + val sharedFlow = MutableSharedFlow() + val resultList = mutableListOf() + + sharedFlow + .onEach { resultList.add(it) } + .flowWithLifecycle(owner.lifecycle, Lifecycle.State.RESUMED) + .launchIn(owner.lifecycleScope) + + sharedFlow.emit(1) + sharedFlow.emit(2) + yield() + assertThat(resultList).containsExactly(1, 2).inOrder() + + // Lifecycle is started again, onEach shouldn't be called + owner.setState(Lifecycle.State.STARTED) + yield() + sharedFlow.emit(3) + yield() + assertThat(resultList).containsExactly(1, 2).inOrder() + + // Lifecycle is resumed again, onEach should be called + owner.setState(Lifecycle.State.RESUMED) + yield() + sharedFlow.emit(4) + yield() + assertThat(resultList).containsExactly(1, 2, 4).inOrder() + + owner.setState(Lifecycle.State.DESTROYED) + } + @Test fun testExtensionFailsWithInitializedState() = runBlocking(Dispatchers.Main) { try { diff --git a/lifecycle/lifecycle-runtime-ktx/src/androidTest/java/androidx/lifecycle/AddRepeatingJobTest.kt b/lifecycle/lifecycle-runtime-ktx/src/androidTest/java/androidx/lifecycle/RepeatOnLifecycleTest.kt similarity index 71% rename from lifecycle/lifecycle-runtime-ktx/src/androidTest/java/androidx/lifecycle/AddRepeatingJobTest.kt rename to lifecycle/lifecycle-runtime-ktx/src/androidTest/java/androidx/lifecycle/RepeatOnLifecycleTest.kt index d7a0af431aaae..28cf06289856a 100644 --- a/lifecycle/lifecycle-runtime-ktx/src/androidTest/java/androidx/lifecycle/AddRepeatingJobTest.kt +++ b/lifecycle/lifecycle-runtime-ktx/src/androidTest/java/androidx/lifecycle/RepeatOnLifecycleTest.kt @@ -24,6 +24,7 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.Job import kotlinx.coroutines.awaitCancellation +import kotlinx.coroutines.launch import kotlinx.coroutines.runBlocking import kotlinx.coroutines.test.TestCoroutineDispatcher import kotlinx.coroutines.test.runBlockingTest @@ -32,7 +33,7 @@ import kotlinx.coroutines.yield import org.junit.Test @SmallTest -class AddRepeatingJobTest { +class RepeatOnLifecycleTest { private val expectations = Expectations() private val owner = FakeLifecycleOwner() @@ -42,8 +43,10 @@ class AddRepeatingJobTest { owner.setState(Lifecycle.State.CREATED) expectations.expect(1) - val repeatingWorkJob = owner.addRepeatingJob(Lifecycle.State.CREATED) { - expectations.expect(2) + val repeatingWorkJob = owner.lifecycleScope.launch { + owner.repeatOnLifecycle(Lifecycle.State.CREATED) { + expectations.expect(2) + } } expectations.expect(3) @@ -56,8 +59,10 @@ class AddRepeatingJobTest { owner.setState(Lifecycle.State.CREATED) expectations.expect(1) - val repeatingWorkJob = owner.addRepeatingJob(Lifecycle.State.STARTED) { - expectations.expect(2) + val repeatingWorkJob = owner.lifecycleScope.launch { + owner.repeatOnLifecycle(Lifecycle.State.STARTED) { + expectations.expect(2) + } } owner.setState(Lifecycle.State.STARTED) @@ -70,8 +75,10 @@ class AddRepeatingJobTest { owner.setState(Lifecycle.State.CREATED) expectations.expect(1) - val repeatingWorkJob = owner.addRepeatingJob(Lifecycle.State.RESUMED) { - expectations.expect(3) + val repeatingWorkJob = owner.lifecycleScope.launch { + owner.repeatOnLifecycle(Lifecycle.State.RESUMED) { + expectations.expect(3) + } } owner.setState(Lifecycle.State.STARTED) @@ -87,11 +94,13 @@ class AddRepeatingJobTest { var restarted = false expectations.expect(1) - val repeatingWorkJob = owner.addRepeatingJob(Lifecycle.State.RESUMED) { - if (!restarted) { - expectations.expect(2) - } else { - expectations.expect(5) + val repeatingWorkJob = owner.lifecycleScope.launch { + owner.repeatOnLifecycle(Lifecycle.State.RESUMED) { + if (!restarted) { + expectations.expect(2) + } else { + expectations.expect(5) + } } } @@ -112,12 +121,14 @@ class AddRepeatingJobTest { owner.setState(Lifecycle.State.RESUMED) expectations.expect(1) - val repeatingWorkJob = owner.addRepeatingJob(Lifecycle.State.STARTED) { - try { - expectations.expect(2) - awaitCancellation() - } catch (e: CancellationException) { - expectations.expect(4) + val repeatingWorkJob = owner.lifecycleScope.launch { + owner.repeatOnLifecycle(Lifecycle.State.STARTED) { + try { + expectations.expect(2) + awaitCancellation() + } catch (e: CancellationException) { + expectations.expect(4) + } } } @@ -134,8 +145,10 @@ class AddRepeatingJobTest { owner.setState(Lifecycle.State.RESUMED) expectations.expect(1) - val repeatingWorkJob = owner.addRepeatingJob(Lifecycle.State.STARTED) { - expectations.expect(2) + val repeatingWorkJob = owner.lifecycleScope.launch { + owner.repeatOnLifecycle(Lifecycle.State.STARTED) { + expectations.expect(2) + } } expectations.expect(3) @@ -148,8 +161,10 @@ class AddRepeatingJobTest { owner.setState(Lifecycle.State.DESTROYED) expectations.expect(1) - val repeatingWorkJob = owner.addRepeatingJob(Lifecycle.State.STARTED) { - expectations.expectUnreached() + val repeatingWorkJob = owner.lifecycleScope.launch { + owner.repeatOnLifecycle(Lifecycle.State.STARTED) { + expectations.expectUnreached() + } } expectations.expect(2) @@ -161,12 +176,14 @@ class AddRepeatingJobTest { owner.setState(Lifecycle.State.RESUMED) expectations.expect(1) - val repeatingWorkJob = owner.addRepeatingJob(Lifecycle.State.STARTED) { - try { - expectations.expect(2) - awaitCancellation() - } catch (e: CancellationException) { - expectations.expect(4) + val repeatingWorkJob = owner.lifecycleScope.launch { + owner.repeatOnLifecycle(Lifecycle.State.STARTED) { + try { + expectations.expect(2) + awaitCancellation() + } catch (e: CancellationException) { + expectations.expect(4) + } } } @@ -185,13 +202,15 @@ class AddRepeatingJobTest { expectations.expect(1) val customJob = Job() - val repeatingWorkJob = owner.addRepeatingJob(Lifecycle.State.STARTED) { - withContext(customJob) { - try { - expectations.expect(2) - awaitCancellation() - } catch (e: CancellationException) { - expectations.expect(4) + val repeatingWorkJob = owner.lifecycleScope.launch { + owner.repeatOnLifecycle(Lifecycle.State.STARTED) { + withContext(customJob) { + try { + expectations.expect(2) + awaitCancellation() + } catch (e: CancellationException) { + expectations.expect(4) + } } } } @@ -214,17 +233,19 @@ class AddRepeatingJobTest { expectations.expect(1) val customJob = Job() - val repeatingWorkJob = owner.addRepeatingJob(Lifecycle.State.RESUMED) { - if (!restarted) { - expectations.expect(2) - } else { - expectations.expect(6) - } - withContext(customJob) { + val repeatingWorkJob = owner.lifecycleScope.launch { + owner.repeatOnLifecycle(Lifecycle.State.RESUMED) { if (!restarted) { - expectations.expect(3) + expectations.expect(2) } else { - expectations.expectUnreached() + expectations.expect(6) + } + withContext(customJob) { + if (!restarted) { + expectations.expect(3) + } else { + expectations.expectUnreached() + } } } } @@ -248,11 +269,13 @@ class AddRepeatingJobTest { expectations.expect(1) var restarted = false - val repeatingWorkJob = owner.addRepeatingJob(Lifecycle.State.RESUMED) { - if (!restarted) { - expectations.expect(2) - } else { - expectations.expectUnreached() + val repeatingWorkJob = owner.lifecycleScope.launch { + owner.repeatOnLifecycle(Lifecycle.State.RESUMED) { + if (!restarted) { + expectations.expect(2) + } else { + expectations.expectUnreached() + } } } @@ -279,9 +302,11 @@ class AddRepeatingJobTest { val testDispatcher = TestCoroutineDispatcher().apply { runBlockingTest { - owner.addRepeatingJob(Lifecycle.State.CREATED) { - withContext(this@apply) { - expectations.expect(2) + owner.lifecycleScope.launch { + owner.repeatOnLifecycle(Lifecycle.State.CREATED) { + withContext(this@apply) { + expectations.expect(2) + } } } } @@ -297,8 +322,10 @@ class AddRepeatingJobTest { owner.setState(Lifecycle.State.STARTED) expectations.expect(1) - val repeatingWorkJob = owner.addRepeatingJob(Lifecycle.State.DESTROYED) { - expectations.expectUnreached() + val repeatingWorkJob = owner.lifecycleScope.launch { + owner.repeatOnLifecycle(Lifecycle.State.DESTROYED) { + expectations.expectUnreached() + } } expectations.expect(2) @@ -307,14 +334,16 @@ class AddRepeatingJobTest { } @Test - fun testAddRepeatingJobFailsWithInitializedState() = runBlocking(Dispatchers.Main) { + fun testExceptionWithInitializedState() = runBlocking(Dispatchers.Main) { val exceptions: MutableList = mutableListOf() val coroutineExceptionHandler = CoroutineExceptionHandler { _, exception -> exceptions.add(exception) } - owner.addRepeatingJob(Lifecycle.State.INITIALIZED, coroutineExceptionHandler) { - // IllegalArgumentException expected + owner.lifecycleScope.launch(coroutineExceptionHandler) { + owner.repeatOnLifecycle(Lifecycle.State.INITIALIZED) { + // IllegalArgumentException expected + } } assertThat(exceptions[0]).isInstanceOf(IllegalArgumentException::class.java) diff --git a/lifecycle/lifecycle-runtime-ktx/src/main/java/androidx/lifecycle/FlowExt.kt b/lifecycle/lifecycle-runtime-ktx/src/main/java/androidx/lifecycle/FlowExt.kt index bfaf4be68b2ff..d6345418db3e4 100644 --- a/lifecycle/lifecycle-runtime-ktx/src/main/java/androidx/lifecycle/FlowExt.kt +++ b/lifecycle/lifecycle-runtime-ktx/src/main/java/androidx/lifecycle/FlowExt.kt @@ -17,11 +17,11 @@ package androidx.lifecycle import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.launch import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.callbackFlow import kotlinx.coroutines.flow.collect import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.launch /** * Flow operator that emits values from `this` upstream Flow when the [lifecycle] is @@ -58,13 +58,22 @@ import kotlinx.coroutines.flow.launchIn * } * ``` * + * `flowWithLifecycle` cancels the upstream Flow when [lifecycle] falls below + * [minActiveState] state. However, the downstream Flow will be active without receiving any + * emissions as long as the scope used to collect the Flow is active. As such, please take care + * when using this function in an operator chain, as the order of the operators matters. For + * example, `flow1.flowWithLifecycle(lifecycle).combine(flow2)` behaves differently than + * `flow1.combine(flow2).flowWithLifecycle(lifecycle)`. The former continues to combine both + * flows even when [lifecycle] falls below [minActiveState] state whereas the combination is + * cancelled in the latter case. + * * Warning: [Lifecycle.State.INITIALIZED] is not allowed in this API. Passing it as a * parameter will throw an [IllegalArgumentException]. * * Tip: If multiple flows need to be collected using `flowWithLifecycle`, consider using - * the [LifecycleOwner.addRepeatingJob] API to collect from all of them using a different - * [launch] per flow instead. This will be more efficient as only one [LifecycleObserver] will be - * added to the [lifecycle] instead of one per flow. + * the [Lifecycle.repeatOnLifecycle] API to collect from all of them using a different + * [launch] per flow instead. That's more efficient and consumes less resources as no hot flows + * are created. * * @param lifecycle The [Lifecycle] where the restarting collecting from `this` flow work will be * kept alive. diff --git a/lifecycle/lifecycle-runtime-ktx/src/main/java/androidx/lifecycle/Lifecycle.kt b/lifecycle/lifecycle-runtime-ktx/src/main/java/androidx/lifecycle/Lifecycle.kt index 34ac453951b5e..1bb682ced9ae3 100644 --- a/lifecycle/lifecycle-runtime-ktx/src/main/java/androidx/lifecycle/Lifecycle.kt +++ b/lifecycle/lifecycle-runtime-ktx/src/main/java/androidx/lifecycle/Lifecycle.kt @@ -67,6 +67,11 @@ public abstract class LifecycleCoroutineScope internal constructor() : Coroutine * [LifecycleCoroutineScope] is at least in [Lifecycle.State.CREATED] state. * * The returned [Job] will be cancelled when the [Lifecycle] is destroyed. + * + * Caution: This API is not recommended to use as it can lead to wasted resources in some + * cases. Please, use the [Lifecycle.repeatOnLifecycle] API instead. This API will be removed + * in a future release. + * * @see Lifecycle.whenCreated * @see Lifecycle.coroutineScope */ @@ -79,6 +84,11 @@ public abstract class LifecycleCoroutineScope internal constructor() : Coroutine * [LifecycleCoroutineScope] is at least in [Lifecycle.State.STARTED] state. * * The returned [Job] will be cancelled when the [Lifecycle] is destroyed. + * + * Caution: This API is not recommended to use as it can lead to wasted resources in some + * cases. Please, use the [Lifecycle.repeatOnLifecycle] API instead. This API will be removed + * in a future release. + * * @see Lifecycle.whenStarted * @see Lifecycle.coroutineScope */ @@ -92,6 +102,11 @@ public abstract class LifecycleCoroutineScope internal constructor() : Coroutine * [LifecycleCoroutineScope] is at least in [Lifecycle.State.RESUMED] state. * * The returned [Job] will be cancelled when the [Lifecycle] is destroyed. + * + * Caution: This API is not recommended to use as it can lead to wasted resources in some + * cases. Please, use the [Lifecycle.repeatOnLifecycle] API instead. This API will be removed + * in a future release. + * * @see Lifecycle.whenResumed * @see Lifecycle.coroutineScope */ diff --git a/lifecycle/lifecycle-runtime-ktx/src/main/java/androidx/lifecycle/RepeatOnLifecycle.kt b/lifecycle/lifecycle-runtime-ktx/src/main/java/androidx/lifecycle/RepeatOnLifecycle.kt index 4e14e28b8ec91..5d792caaf2cd2 100644 --- a/lifecycle/lifecycle-runtime-ktx/src/main/java/androidx/lifecycle/RepeatOnLifecycle.kt +++ b/lifecycle/lifecycle-runtime-ktx/src/main/java/androidx/lifecycle/RepeatOnLifecycle.kt @@ -23,58 +23,34 @@ import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.launch import kotlinx.coroutines.suspendCancellableCoroutine import kotlinx.coroutines.withContext -import kotlin.coroutines.CoroutineContext -import kotlin.coroutines.EmptyCoroutineContext import kotlin.coroutines.resume /** - * Launches and runs the given [block] in a coroutine when `this` [LifecycleOwner]'s [Lifecycle] - * is at least at [state]. The launched coroutine will be cancelled when the lifecycle state falls - * below [state]. + * Runs the given [block] in a new coroutine when `this` [Lifecycle] is at least at [state] and + * suspends the execution until `this` [Lifecycle] is [Lifecycle.State.DESTROYED]. * * The [block] will cancel and re-launch as the lifecycle moves in and out of the target state. - * To permanently remove the work from the lifecycle, [Job.cancel] the returned [Job]. * * ``` - * // Runs the block of code in a coroutine when the lifecycleOwner is at least STARTED. - * // The coroutine will be cancelled when the ON_STOP event happens and will restart executing - * // if the lifecycleOwner's lifecycle receives the ON_START event again. - * lifecycleOwner.addRepeatingJob(Lifecycle.State.STARTED) { - * uiStateFlow.collect { uiState -> - * updateUi(uiState) + * class MyActivity : AppCompatActivity() { + * override fun onCreate(savedInstanceState: Bundle?) { + * /* ... */ + * // Runs the block of code in a coroutine when the lifecycle is at least STARTED. + * // The coroutine will be cancelled when the ON_STOP event happens and will + * // restart executing if the lifecycle receives the ON_START event again. + * lifecycleScope.launch { + * lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) { + * uiStateFlow.collect { uiState -> + * updateUi(uiState) + * } + * } * } * } * ``` * - * The best practice is to call this function when the lifecycleOwner is initialized. For + * The best practice is to call this function when the lifecycle is initialized. For * example, `onCreate` in an Activity, or `onViewCreated` in a Fragment. Otherwise, multiple - * repeating jobs doing the same could be registered and be executed at the same time. - * - * Warning: [Lifecycle.State.INITIALIZED] is not allowed in this API. Passing it as a - * parameter will throw an [IllegalArgumentException]. - * - * @see Lifecycle.repeatOnLifecycle for details - * - * @param state [Lifecycle.State] in which the coroutine running [block] starts. That coroutine - * will cancel if the lifecycle falls below that state, and will restart if it's in that state - * again. - * @param coroutineContext [CoroutineContext] used to execute [block]. - * @param block The block to run when the lifecycle is at least in [state] state. - * @return [Job] to manage the repeating work. - */ -public fun LifecycleOwner.addRepeatingJob( - state: Lifecycle.State, - coroutineContext: CoroutineContext = EmptyCoroutineContext, - block: suspend CoroutineScope.() -> Unit -): Job = lifecycleScope.launch(coroutineContext) { - lifecycle.repeatOnLifecycle(state, block) -} - -/** - * Runs the given [block] in a new coroutine when `this` [Lifecycle] is at least at [state] and - * suspends the execution until `this` [Lifecycle] is [Lifecycle.State.DESTROYED]. - * - * The [block] will cancel and re-launch as the lifecycle moves in and out of the target state. + * repeating coroutines doing the same could be created and be executed at the same time. * * Warning: [Lifecycle.State.INITIALIZED] is not allowed in this API. Passing it as a * parameter will throw an [IllegalArgumentException]. @@ -141,3 +117,31 @@ public suspend fun Lifecycle.repeatOnLifecycle( } } } + +/** + * [LifecycleOwner]'s extension function for [Lifecycle.repeatOnLifecycle] to allow an easier + * call to the API from LifecycleOwners such as Activities and Fragments. + * + * ``` + * class MyActivity : AppCompatActivity() { + * override fun onCreate(savedInstanceState: Bundle?) { + * /* ... */ + * // Runs the block of code in a coroutine when the lifecycle is at least STARTED. + * // The coroutine will be cancelled when the ON_STOP event happens and will + * // restart executing if the lifecycle receives the ON_START event again. + * lifecycleScope.launch { + * repeatOnLifecycle(Lifecycle.State.STARTED) { + * uiStateFlow.collect { uiState -> + * updateUi(uiState) + * } + * } + * } + * } + * ``` + * + * @see Lifecycle.repeatOnLifecycle + */ +public suspend fun LifecycleOwner.repeatOnLifecycle( + state: Lifecycle.State, + block: suspend CoroutineScope.() -> Unit +): Unit = lifecycle.repeatOnLifecycle(state, block)