• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
<lambda>null2  * Copyright (C) 2024 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 com.android.systemui.scene.session.ui.composable
18 
19 import androidx.compose.runtime.Composable
20 import androidx.compose.runtime.DisposableEffectResult
21 import androidx.compose.runtime.DisposableEffectScope
22 import androidx.compose.runtime.RememberObserver
23 import androidx.compose.runtime.SideEffect
24 import androidx.compose.runtime.currentCompositeKeyHash
25 import androidx.compose.runtime.getValue
26 import androidx.compose.runtime.mutableStateOf
27 import androidx.compose.runtime.rememberCompositionContext
28 import androidx.compose.runtime.saveable.Saver
29 import androidx.compose.runtime.saveable.SaverScope
30 import androidx.compose.runtime.saveable.mapSaver
31 import androidx.compose.runtime.saveable.rememberSaveable
32 import androidx.compose.runtime.setValue
33 import com.android.systemui.scene.session.shared.SessionStorage
34 import com.android.systemui.util.kotlin.mapValuesNotNullTo
35 import kotlin.coroutines.CoroutineContext
36 import kotlin.coroutines.EmptyCoroutineContext
37 import kotlinx.coroutines.CoroutineScope
38 import kotlinx.coroutines.Job
39 
40 /**
41  * An explicit storage for remembering composable state outside of the lifetime of a composition.
42  *
43  * Specifically, this allows easy conversion of standard
44  * [remember][androidx.compose.runtime.remember] invocations to ones that are preserved beyond the
45  * callsite's existence in the composition.
46  *
47  * ```kotlin
48  * @Composable
49  * fun Parent() {
50  *   val session = remember { Session() }
51  *   ...
52  *   if (someCondition) {
53  *     Child(session)
54  *   }
55  * }
56  *
57  * @Composable
58  * fun Child(session: Session) {
59  *   val state by session.rememberSession { mutableStateOf(0f) }
60  *   ...
61  * }
62  * ```
63  */
64 interface Session {
65     /**
66      * Remember the value returned by [init] if all [inputs] are equal (`==`) to the values they had
67      * in the previous composition, otherwise produce and remember a new value by calling [init].
68      *
69      * @param inputs A set of inputs such that, when any of them have changed, will cause the state
70      *   to reset and [init] to be rerun
71      * @param key An optional key to be used as a key for the saved value. If `null`, we use the one
72      *   automatically generated by the Compose runtime which is unique for the every exact code
73      *   location in the composition tree
74      * @param init A factory function to create the initial value of this state
75      * @see androidx.compose.runtime.remember
76      */
77     @Composable fun <T> rememberSession(key: String?, vararg inputs: Any?, init: () -> T): T
78 }
79 
80 /** Returns a new [Session], optionally backed by the provided [SessionStorage]. */
Sessionnull81 fun Session(storage: SessionStorage = SessionStorage()): Session = SessionImpl(storage)
82 
83 /**
84  * Remember the value returned by [init] if all [inputs] are equal (`==`) to the values they had in
85  * the previous composition, otherwise produce and remember a new value by calling [init].
86  *
87  * @param inputs A set of inputs such that, when any of them have changed, will cause the state to
88  *   reset and [init] to be rerun
89  * @param key An optional key to be used as a key for the saved value. If not provided we use the
90  *   one automatically generated by the Compose runtime which is unique for the every exact code
91  *   location in the composition tree
92  * @param init A factory function to create the initial value of this state
93  * @see androidx.compose.runtime.remember
94  */
95 @Composable
96 fun <T> Session.rememberSession(vararg inputs: Any?, key: String? = null, init: () -> T): T =
97     rememberSession(key, *inputs, init = init)
98 
99 /**
100  * A side effect of composition that must be reversed or cleaned up if the [Session] ends.
101  *
102  * @see androidx.compose.runtime.DisposableEffect
103  */
104 @Composable
105 fun Session.SessionDisposableEffect(
106     vararg inputs: Any?,
107     key: String? = null,
108     effect: DisposableEffectScope.() -> DisposableEffectResult,
109 ) {
110     rememberSession(inputs, key) {
111         object : RememberObserver {
112 
113             var onDispose: DisposableEffectResult? = null
114 
115             override fun onAbandoned() {
116                 // no-op
117             }
118 
119             override fun onForgotten() {
120                 onDispose?.dispose()
121                 onDispose = null
122             }
123 
124             override fun onRemembered() {
125                 onDispose = DisposableEffectScope().effect()
126             }
127         }
128     }
129 }
130 
131 /**
132  * Return a [CoroutineScope] bound to this [Session] using the optional [CoroutineContext] provided
133  * by [getContext]. [getContext] will only be called once and the same [CoroutineScope] instance
134  * will be returned for the duration of the [Session].
135  *
136  * @see androidx.compose.runtime.rememberCoroutineScope
137  */
138 @Composable
sessionCoroutineScopenull139 fun Session.sessionCoroutineScope(
140     getContext: () -> CoroutineContext = { EmptyCoroutineContext }
141 ): CoroutineScope {
142     val effectContext: CoroutineContext = rememberCompositionContext().effectCoroutineContext
<lambda>null143     val job = rememberSession { Job() }
<lambda>null144     SessionDisposableEffect { onDispose { job.cancel() } }
<lambda>null145     return rememberSession { CoroutineScope(effectContext + job + getContext()) }
146 }
147 
148 /**
149  * An explicit storage for remembering composable state outside of the lifetime of a composition.
150  *
151  * Specifically, this allows easy conversion of standard [rememberSession] invocations to ones that
152  * are preserved beyond the callsite's existence in the composition.
153  *
154  * ```kotlin
155  * @Composable
156  * fun Parent() {
157  *   val session = rememberSaveableSession()
158  *   ...
159  *   if (someCondition) {
160  *     Child(session)
161  *   }
162  * }
163  *
164  * @Composable
165  * fun Child(session: SaveableSession) {
166  *   val state by session.rememberSaveableSession { mutableStateOf(0f) }
167  *   ...
168  * }
169  * ```
170  */
171 interface SaveableSession : Session {
172     /**
173      * Remember the value produced by [init].
174      *
175      * It behaves similarly to [rememberSession], but the stored value will survive the activity or
176      * process recreation using the saved instance state mechanism (for example it happens when the
177      * screen is rotated in the Android application).
178      *
179      * @param inputs A set of inputs such that, when any of them have changed, will cause the state
180      *   to reset and [init] to be rerun
181      * @param saver The [Saver] object which defines how the state is saved and restored.
182      * @param key An optional key to be used as a key for the saved value. If not provided we use
183      *   the automatically generated by the Compose runtime which is unique for the every exact code
184      *   location in the composition tree
185      * @param init A factory function to create the initial value of this state
186      * @see rememberSaveable
187      */
188     @Composable
rememberSaveableSessionnull189     fun <T : Any> rememberSaveableSession(
190         vararg inputs: Any?,
191         saver: Saver<T, out Any>,
192         key: String?,
193         init: () -> T,
194     ): T
195 }
196 
197 /**
198  * Returns a new [SaveableSession] that is preserved across configuration changes.
199  *
200  * @param inputs A set of inputs such that, when any of them have changed, will cause the state to
201  *   reset.
202  * @param key An optional key to be used as a key for the saved value. If not provided we use the
203  *   automatically generated by the Compose runtime which is unique for the every exact code
204  *   location in the composition tree.
205  */
206 @Composable
207 fun rememberSaveableSession(vararg inputs: Any?, key: String? = null): SaveableSession =
208     rememberSaveable(*inputs, SaveableSessionImpl.SessionSaver, key) { SaveableSessionImpl() }
209 
210 private class SessionImpl(private val storage: SessionStorage = SessionStorage()) : Session {
211     @Composable
rememberSessionnull212     override fun <T> rememberSession(key: String?, vararg inputs: Any?, init: () -> T): T {
213         val storage = storage.storage
214         val compositeKey = currentCompositeKeyHash
215         // key is the one provided by the user or the one generated by the compose runtime
216         val finalKey =
217             if (!key.isNullOrEmpty()) {
218                 key
219             } else {
220                 compositeKey.toString(MAX_SUPPORTED_RADIX)
221             }
222         if (finalKey !in storage) {
223             val value = init()
224             SideEffect {
225                 storage[finalKey] = SessionStorage.StorageEntry(inputs, value)
226                 if (value is RememberObserver) {
227                     value.onRemembered()
228                 }
229             }
230             return value
231         }
232         val entry = storage[finalKey]!!
233         if (!inputs.contentEquals(entry.keys)) {
234             val value = init()
235             SideEffect {
236                 val oldValue = entry.stored
237                 if (oldValue is RememberObserver) {
238                     oldValue.onForgotten()
239                 }
240                 entry.stored = value
241                 if (value is RememberObserver) {
242                     value.onRemembered()
243                 }
244             }
245             return value
246         }
247         @Suppress("UNCHECKED_CAST")
248         return entry.stored as T
249     }
250 }
251 
252 private class SaveableSessionImpl(
253     saveableStorage: MutableMap<String, StorageEntry> = mutableMapOf(),
254     sessionStorage: SessionStorage = SessionStorage(),
<lambda>null255 ) : SaveableSession, Session by Session(sessionStorage) {
256 
257     var saveableStorage: MutableMap<String, StorageEntry> by mutableStateOf(saveableStorage)
258 
259     @Composable
260     override fun <T : Any> rememberSaveableSession(
261         vararg inputs: Any?,
262         saver: Saver<T, out Any>,
263         key: String?,
264         init: () -> T,
265     ): T {
266         val compositeKey = currentCompositeKeyHash
267         // key is the one provided by the user or the one generated by the compose runtime
268         val finalKey =
269             if (!key.isNullOrEmpty()) {
270                 key
271             } else {
272                 compositeKey.toString(MAX_SUPPORTED_RADIX)
273             }
274 
275         @Suppress("UNCHECKED_CAST") (saver as Saver<T, Any>)
276 
277         if (finalKey !in saveableStorage) {
278             val value = init()
279             SideEffect { saveableStorage[finalKey] = StorageEntry.Restored(inputs, value, saver) }
280             return value
281         }
282         when (val entry = saveableStorage[finalKey]!!) {
283             is StorageEntry.Unrestored -> {
284                 val value = saver.restore(entry.unrestored) ?: init()
285                 SideEffect {
286                     saveableStorage[finalKey] = StorageEntry.Restored(inputs, value, saver)
287                 }
288                 return value
289             }
290             is StorageEntry.Restored<*> -> {
291                 if (!inputs.contentEquals(entry.inputs)) {
292                     val value = init()
293                     SideEffect {
294                         saveableStorage[finalKey] = StorageEntry.Restored(inputs, value, saver)
295                     }
296                     return value
297                 }
298                 @Suppress("UNCHECKED_CAST")
299                 return entry.stored as T
300             }
301         }
302     }
303 
304     sealed class StorageEntry {
305         class Unrestored(val unrestored: Any) : StorageEntry()
306 
307         class Restored<T>(val inputs: Array<out Any?>, var stored: T, val saver: Saver<T, Any>) :
308             StorageEntry() {
309             fun SaverScope.saveEntry() {
310                 with(saver) { stored?.let { save(it) } }
311             }
312         }
313     }
314 
315     object SessionSaver :
316         Saver<SaveableSessionImpl, Any> by mapSaver(
317             save = { sessionScope: SaveableSessionImpl ->
318                 sessionScope.saveableStorage.mapValues { (k, v) ->
319                     when (v) {
320                         is StorageEntry.Unrestored -> v.unrestored
321                         is StorageEntry.Restored<*> -> {
322                             with(v) { saveEntry() }
323                         }
324                     }
325                 }
326             },
327             restore = { savedMap: Map<String, Any?> ->
328                 SaveableSessionImpl(
329                     saveableStorage =
330                         savedMap.mapValuesNotNullTo(mutableMapOf()) { (k, v) ->
331                             v?.let { StorageEntry.Unrestored(v) }
332                         }
333                 )
334             },
335         )
336 }
337 
338 private const val MAX_SUPPORTED_RADIX = 36
339