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