1 /*
<lambda>null2 * Copyright 2021 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17 package androidx.lifecycle
18
19 import kotlin.coroutines.resume
20 import kotlinx.coroutines.CoroutineScope
21 import kotlinx.coroutines.Dispatchers
22 import kotlinx.coroutines.Job
23 import kotlinx.coroutines.coroutineScope
24 import kotlinx.coroutines.launch
25 import kotlinx.coroutines.suspendCancellableCoroutine
26 import kotlinx.coroutines.sync.Mutex
27 import kotlinx.coroutines.sync.withLock
28 import kotlinx.coroutines.withContext
29
30 /**
31 * Runs the given [block] in a new coroutine when `this` [Lifecycle] is at least at [state] and
32 * suspends the execution until `this` [Lifecycle] is [Lifecycle.State.DESTROYED].
33 *
34 * The [block] will cancel and re-launch as the lifecycle moves in and out of the target state.
35 *
36 * ```
37 * class MyActivity : AppCompatActivity() {
38 * override fun onCreate(savedInstanceState: Bundle?) {
39 * /* ... */
40 * // Runs the block of code in a coroutine when the lifecycle is at least STARTED.
41 * // The coroutine will be cancelled when the ON_STOP event happens and will
42 * // restart executing if the lifecycle receives the ON_START event again.
43 * lifecycleScope.launch {
44 * lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) {
45 * uiStateFlow.collect { uiState ->
46 * updateUi(uiState)
47 * }
48 * }
49 * }
50 * }
51 * }
52 * ```
53 *
54 * The best practice is to call this function when the lifecycle is initialized. For example,
55 * `onCreate` in an Activity, or `onViewCreated` in a Fragment. Otherwise, multiple repeating
56 * coroutines doing the same could be created and be executed at the same time.
57 *
58 * Repeated invocations of `block` will run serially, that is they will always wait for the previous
59 * invocation to fully finish before re-starting execution as the state moves in and out of the
60 * required state.
61 *
62 * Warning: [Lifecycle.State.INITIALIZED] is not allowed in this API. Passing it as a parameter will
63 * throw an [IllegalArgumentException].
64 *
65 * @param state [Lifecycle.State] in which `block` runs in a new coroutine. That coroutine will
66 * cancel if the lifecycle falls below that state, and will restart if it's in that state again.
67 * @param block The block to run when the lifecycle is at least in [state] state.
68 */
69 public suspend fun Lifecycle.repeatOnLifecycle(
70 state: Lifecycle.State,
71 block: suspend CoroutineScope.() -> Unit
72 ) {
73 require(state !== Lifecycle.State.INITIALIZED) {
74 "repeatOnLifecycle cannot start work with the INITIALIZED lifecycle state."
75 }
76
77 if (currentState === Lifecycle.State.DESTROYED) {
78 return
79 }
80
81 // This scope is required to preserve context before we move to Dispatchers.Main
82 coroutineScope {
83 withContext(Dispatchers.Main.immediate) {
84 // Check the current state of the lifecycle as the previous check is not guaranteed
85 // to be done on the main thread.
86 if (currentState === Lifecycle.State.DESTROYED) return@withContext
87
88 // Instance of the running repeating coroutine
89 var launchedJob: Job? = null
90
91 // Registered observer
92 var observer: LifecycleEventObserver? = null
93 try {
94 // Suspend the coroutine until the lifecycle is destroyed or
95 // the coroutine is cancelled
96 suspendCancellableCoroutine<Unit> { cont ->
97 // Lifecycle observers that executes `block` when the lifecycle reaches certain
98 // state, and
99 // cancels when it falls below that state.
100 val startWorkEvent = Lifecycle.Event.upTo(state)
101 val cancelWorkEvent = Lifecycle.Event.downFrom(state)
102 val mutex = Mutex()
103 observer = LifecycleEventObserver { _, event ->
104 if (event == startWorkEvent) {
105 // Launch the repeating work preserving the calling context
106 launchedJob =
107 this@coroutineScope.launch {
108 // Mutex makes invocations run serially,
109 // coroutineScope ensures all child coroutines finish
110 mutex.withLock { coroutineScope { block() } }
111 }
112 return@LifecycleEventObserver
113 }
114 if (event == cancelWorkEvent) {
115 launchedJob?.cancel()
116 launchedJob = null
117 }
118 if (event == Lifecycle.Event.ON_DESTROY) {
119 cont.resume(Unit)
120 }
121 }
122 this@repeatOnLifecycle.addObserver(observer as LifecycleEventObserver)
123 }
124 } finally {
125 launchedJob?.cancel()
126 observer?.let { this@repeatOnLifecycle.removeObserver(it) }
127 }
128 }
129 }
130 }
131
132 /**
133 * [LifecycleOwner]'s extension function for [Lifecycle.repeatOnLifecycle] to allow an easier call
134 * to the API from LifecycleOwners such as Activities and Fragments.
135 *
136 * ```
137 * class MyActivity : AppCompatActivity() {
138 * override fun onCreate(savedInstanceState: Bundle?) {
139 * /* ... */
140 * // Runs the block of code in a coroutine when the lifecycle is at least STARTED.
141 * // The coroutine will be cancelled when the ON_STOP event happens and will
142 * // restart executing if the lifecycle receives the ON_START event again.
143 * lifecycleScope.launch {
144 * repeatOnLifecycle(Lifecycle.State.STARTED) {
145 * uiStateFlow.collect { uiState ->
146 * updateUi(uiState)
147 * }
148 * }
149 * }
150 * }
151 * }
152 * ```
153 *
154 * @see Lifecycle.repeatOnLifecycle
155 */
repeatOnLifecyclenull156 public suspend fun LifecycleOwner.repeatOnLifecycle(
157 state: Lifecycle.State,
158 block: suspend CoroutineScope.() -> Unit
159 ): Unit = lifecycle.repeatOnLifecycle(state, block)
160