1 /*
2 * Copyright 2019 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 @file:JvmName("PausingDispatcherKt")
18
19 package androidx.lifecycle
20
21 import kotlin.coroutines.CoroutineContext
22 import kotlinx.coroutines.CoroutineDispatcher
23 import kotlinx.coroutines.CoroutineScope
24 import kotlinx.coroutines.Dispatchers
25 import kotlinx.coroutines.Job
26 import kotlinx.coroutines.Runnable
27 import kotlinx.coroutines.withContext
28
29 /**
30 * Runs the given block when the [LifecycleOwner]'s [Lifecycle] is at least in
31 * [Lifecycle.State.CREATED] state.
32 *
33 * @see Lifecycle.whenStateAtLeast for details
34 */
35 @Deprecated(
36 "whenCreated has been deprecated because it runs the block on a " +
37 "pausing dispatcher that suspends, rather than cancels work when the " +
38 "lifecycle state goes below the given state. Use withCreated for " +
39 "non-suspending work that needs to run only once when the Lifecycle changes."
40 )
41 @Suppress("DEPRECATION")
whenCreatednull42 public suspend fun <T> LifecycleOwner.whenCreated(block: suspend CoroutineScope.() -> T): T =
43 lifecycle.whenCreated(block)
44
45 /**
46 * Runs the given block when the [Lifecycle] is at least in [Lifecycle.State.CREATED] state.
47 *
48 * @see Lifecycle.whenStateAtLeast for details
49 */
50 @Deprecated(
51 "whenCreated has been deprecated because it runs the block on a " +
52 "pausing dispatcher that suspends, rather than cancels work when the " +
53 "lifecycle state goes below the given state. Use withCreated for " +
54 "non-suspending work that needs to run only once when the Lifecycle changes."
55 )
56 @Suppress("DEPRECATION")
57 public suspend fun <T> Lifecycle.whenCreated(block: suspend CoroutineScope.() -> T): T {
58 return whenStateAtLeast(Lifecycle.State.CREATED, block)
59 }
60
61 /**
62 * Runs the given block when the [LifecycleOwner]'s [Lifecycle] is at least in
63 * [Lifecycle.State.STARTED] state.
64 *
65 * @see Lifecycle.whenStateAtLeast for details
66 */
67 @Deprecated(
68 "whenStarted has been deprecated because it runs the block on a " +
69 "pausing dispatcher that suspends, rather than cancels work when the " +
70 "lifecycle state goes below the given state. Use withStarted for " +
71 "non-suspending work that needs to run only once when the Lifecycle changes."
72 )
73 @Suppress("DEPRECATION")
whenStartednull74 public suspend fun <T> LifecycleOwner.whenStarted(block: suspend CoroutineScope.() -> T): T =
75 lifecycle.whenStarted(block)
76
77 /**
78 * Runs the given block when the [Lifecycle] is at least in [Lifecycle.State.STARTED] state.
79 *
80 * @see Lifecycle.whenStateAtLeast for details
81 */
82 @Deprecated(
83 "whenStarted has been deprecated because it runs the block on a " +
84 "pausing dispatcher that suspends, rather than cancels work when the " +
85 "lifecycle state goes below the given state. Use withStarted for " +
86 "non-suspending work that needs to run only once when the Lifecycle changes."
87 )
88 @Suppress("DEPRECATION")
89 public suspend fun <T> Lifecycle.whenStarted(block: suspend CoroutineScope.() -> T): T {
90 return whenStateAtLeast(Lifecycle.State.STARTED, block)
91 }
92
93 /**
94 * Runs the given block when the [LifecycleOwner]'s [Lifecycle] is at least in
95 * [Lifecycle.State.RESUMED] state.
96 *
97 * @see Lifecycle.whenStateAtLeast for details
98 */
99 @Deprecated(
100 "whenResumed has been deprecated because it runs the block on a " +
101 "pausing dispatcher that suspends, rather than cancels work when the " +
102 "lifecycle state goes below the given state. Use withResumed for " +
103 "non-suspending work that needs to run only once when the Lifecycle changes."
104 )
105 @Suppress("DEPRECATION")
whenResumednull106 public suspend fun <T> LifecycleOwner.whenResumed(block: suspend CoroutineScope.() -> T): T =
107 lifecycle.whenResumed(block)
108
109 /**
110 * Runs the given block when the [Lifecycle] is at least in [Lifecycle.State.RESUMED] state.
111 *
112 * @see Lifecycle.whenStateAtLeast for details
113 */
114 @Deprecated(
115 "whenResumed has been deprecated because it runs the block on a " +
116 "pausing dispatcher that suspends, rather than cancels work when the " +
117 "lifecycle state goes below the given state. Use withResumed for " +
118 "non-suspending work that needs to run only once when the Lifecycle changes."
119 )
120 @Suppress("DEPRECATION")
121 public suspend fun <T> Lifecycle.whenResumed(block: suspend CoroutineScope.() -> T): T {
122 return whenStateAtLeast(Lifecycle.State.RESUMED, block)
123 }
124
125 /**
126 * Runs the given [block] on a [CoroutineDispatcher] that executes the [block] on the main thread
127 * and suspends the execution unless the [Lifecycle]'s state is at least [minState].
128 *
129 * If the [Lifecycle] moves to a lesser state while the [block] is running, the [block] will be
130 * suspended until the [Lifecycle] reaches to a state greater or equal to [minState].
131 *
132 * Note that this won't effect any sub coroutine if they use a different [CoroutineDispatcher].
133 * However, the [block] will not resume execution when the sub coroutine finishes unless the
134 * [Lifecycle] is at least in [minState].
135 *
136 * If the [Lifecycle] is destroyed while the [block] is suspended, the [block] will be cancelled
137 * which will also cancel any child coroutine launched inside the [block].
138 *
139 * If you have a `try finally` block in your code, the `finally` might run after the [Lifecycle]
140 * moves outside the desired state. It is recommended to check the [Lifecycle.getCurrentState]
141 * before accessing the UI. Similarly, if you have a `catch` statement that might catch
142 * `CancellationException`, you should check the [Lifecycle.getCurrentState] before accessing the
143 * UI. See the sample below for more details.
144 *
145 * ```
146 * // running a block of code only if lifecycle is STARTED
147 * viewLifecycle.whenStateAtLeast(Lifecycle.State.STARTED) {
148 * // here, we are on the main thread and view lifecycle is guaranteed to be STARTED or RESUMED.
149 * // We can safely access our views.
150 * loadingBar.visibility = View.VISIBLE
151 * try {
152 * // we can call any suspend function
153 * val data = withContext(Dispatchers.IO) {
154 * // this will run in IO thread pool. It will keep running as long as Lifecycle
155 * // is not DESTROYED. If it is destroyed, this coroutine will be cancelled as well.
156 * // However, we CANNOT access Views here.
157 *
158 * // We are using withContext(Dispatchers.IO) here just for demonstration purposes.
159 * // Such code should live in your business logic classes and your UI should use a
160 * // ViewModel (or similar) to access it.
161 * api.getUser()
162 * }
163 * // this line will execute on the main thread and only if the lifecycle is in at least
164 * // STARTED state (STARTED is the parameter we've passed to whenStateAtLeast)
165 * // Because of this guarantee, we can safely access the UI again.
166 * loadingBar.visibility = View.GONE
167 * nameTextView.text = user.name
168 * lastNameTextView.text = user.lastName
169 * } catch(ex : UserNotFoundException) {
170 * // same as above, this code can safely access UI elements because it only runs if
171 * // view lifecycle is at least STARTED
172 * loadingBar.visibility = View.GONE
173 * showErrorDialog(ex)
174 * } catch(th : Throwable) {
175 * // Unlike the catch statement above, this catch statements it too generic and might
176 * // also catch the CancellationException. Before accessing UI, you should check isActive
177 * // or lifecycle state
178 * if (viewLifecycle.currentState >= Lifecycle.State.STARTED) {
179 * // here you can access the view because you've checked the coroutine is active
180 * }
181 * } finally {
182 * // in case of cancellation, this line might run even if the Lifecycle is not DESTROYED.
183 * // You cannot access Views here unless you check `isActive` or lifecycle state
184 * if (viewLifecycle.currentState >= Lifecycle.State.STARTED) {
185 * // safe to access views
186 * } else {
187 * // not safe to access views
188 * }
189 * }
190 * }
191 * ```
192 *
193 * @param minState The desired minimum state to run the [block].
194 * @param block The block to run when the lifecycle is at least in [minState].
195 * @return <T> The return value of the [block]
196 */
197 @Deprecated(
198 "whenStateAtLeast has been deprecated because it runs the block on a " +
199 "pausing dispatcher that suspends, rather than cancels work when the " +
200 "lifecycle state goes below the given state. Use withStateAtLeast for " +
201 "non-suspending work that needs to run only once when the Lifecycle changes."
202 )
whenStateAtLeastnull203 public suspend fun <T> Lifecycle.whenStateAtLeast(
204 minState: Lifecycle.State,
205 block: suspend CoroutineScope.() -> T
206 ): T =
207 withContext(Dispatchers.Main.immediate) {
208 val job = coroutineContext[Job] ?: error("when[State] methods should have a parent job")
209 val dispatcher = PausingDispatcher()
210 val controller =
211 LifecycleController(this@whenStateAtLeast, minState, dispatcher.dispatchQueue, job)
212 try {
213 withContext(dispatcher, block)
214 } finally {
215 controller.finish()
216 }
217 }
218
219 /**
220 * A [CoroutineDispatcher] implementation that maintains a dispatch queue to be able to pause
221 * execution of coroutines.
222 *
223 * @see [DispatchQueue] and [Lifecycle.whenStateAtLeast] for details.
224 */
225 internal class PausingDispatcher : CoroutineDispatcher() {
226 /** helper class to maintain state and enqueued continuations. */
227 @JvmField internal val dispatchQueue = DispatchQueue()
228
isDispatchNeedednull229 override fun isDispatchNeeded(context: CoroutineContext): Boolean {
230 if (Dispatchers.Main.immediate.isDispatchNeeded(context)) {
231 return true
232 }
233 // It's safe to call dispatchQueue.canRun() here because
234 // Dispatchers.Main.immediate.isDispatchNeeded returns true if we're not on the main thread
235 // If the queue is paused right now we need to dispatch so that the block is added to the
236 // the queue
237 return !dispatchQueue.canRun()
238 }
239
dispatchnull240 override fun dispatch(context: CoroutineContext, block: Runnable) {
241 dispatchQueue.dispatchAndEnqueue(context, block)
242 }
243 }
244