Skip to content

Commit

Permalink
Remove LifecycleOwner.addRepeatingJob API
Browse files Browse the repository at this point in the history
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
  • Loading branch information
Manuel Vivo committed May 12, 2021
1 parent 62f5314 commit b08b493
Show file tree
Hide file tree
Showing 9 changed files with 201 additions and 107 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand Down Expand Up @@ -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)
}
}
}
}
Expand Down
2 changes: 1 addition & 1 deletion lifecycle/lifecycle-runtime-ktx/api/current.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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<? super kotlinx.coroutines.CoroutineScope,? super kotlin.coroutines.Continuation<? super kotlin.Unit>,?> block);
method public static suspend Object? repeatOnLifecycle(androidx.lifecycle.Lifecycle, androidx.lifecycle.Lifecycle.State state, kotlin.jvm.functions.Function2<? super kotlinx.coroutines.CoroutineScope,? super kotlin.coroutines.Continuation<? super kotlin.Unit>,?> block, kotlin.coroutines.Continuation<? super kotlin.Unit> p);
method public static suspend Object? repeatOnLifecycle(androidx.lifecycle.LifecycleOwner, androidx.lifecycle.Lifecycle.State state, kotlin.jvm.functions.Function2<? super kotlinx.coroutines.CoroutineScope,? super kotlin.coroutines.Continuation<? super kotlin.Unit>,?> block, kotlin.coroutines.Continuation<? super kotlin.Unit> p);
}

public final class ViewKt {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<? super kotlinx.coroutines.CoroutineScope,? super kotlin.coroutines.Continuation<? super kotlin.Unit>,?> block);
method public static suspend Object? repeatOnLifecycle(androidx.lifecycle.Lifecycle, androidx.lifecycle.Lifecycle.State state, kotlin.jvm.functions.Function2<? super kotlinx.coroutines.CoroutineScope,? super kotlin.coroutines.Continuation<? super kotlin.Unit>,?> block, kotlin.coroutines.Continuation<? super kotlin.Unit> p);
method public static suspend Object? repeatOnLifecycle(androidx.lifecycle.LifecycleOwner, androidx.lifecycle.Lifecycle.State state, kotlin.jvm.functions.Function2<? super kotlinx.coroutines.CoroutineScope,? super kotlin.coroutines.Continuation<? super kotlin.Unit>,?> block, kotlin.coroutines.Continuation<? super kotlin.Unit> p);
}

public final class ViewKt {
Expand Down
2 changes: 1 addition & 1 deletion lifecycle/lifecycle-runtime-ktx/api/restricted_current.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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<? super kotlinx.coroutines.CoroutineScope,? super kotlin.coroutines.Continuation<? super kotlin.Unit>,?> block);
method public static suspend Object? repeatOnLifecycle(androidx.lifecycle.Lifecycle, androidx.lifecycle.Lifecycle.State state, kotlin.jvm.functions.Function2<? super kotlinx.coroutines.CoroutineScope,? super kotlin.coroutines.Continuation<? super kotlin.Unit>,?> block, kotlin.coroutines.Continuation<? super kotlin.Unit> p);
method public static suspend Object? repeatOnLifecycle(androidx.lifecycle.LifecycleOwner, androidx.lifecycle.Lifecycle.State state, kotlin.jvm.functions.Function2<? super kotlinx.coroutines.CoroutineScope,? super kotlin.coroutines.Continuation<? super kotlin.Unit>,?> block, kotlin.coroutines.Continuation<? super kotlin.Unit> p);
}

public final class ViewKt {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<Int>()
val resultList = mutableListOf<Int>()

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 {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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()
Expand All @@ -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)
Expand All @@ -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)
Expand All @@ -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)
Expand All @@ -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)
}
}
}

Expand All @@ -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)
}
}
}

Expand All @@ -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)
Expand All @@ -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)
Expand All @@ -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)
}
}
}

Expand All @@ -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)
}
}
}
}
Expand All @@ -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()
}
}
}
}
Expand All @@ -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()
}
}
}

Expand All @@ -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)
}
}
}
}
Expand All @@ -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)
Expand All @@ -307,14 +334,16 @@ class AddRepeatingJobTest {
}

@Test
fun testAddRepeatingJobFailsWithInitializedState() = runBlocking(Dispatchers.Main) {
fun testExceptionWithInitializedState() = runBlocking(Dispatchers.Main) {
val exceptions: MutableList<Throwable> = 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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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.
Expand Down
Loading

0 comments on commit b08b493

Please sign in to comment.