1 /*
2 * Copyright 2023 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.compose.runtime.tooling
18
19 import androidx.compose.runtime.Composition
20 import androidx.compose.runtime.CompositionImplServiceKey
21 import androidx.compose.runtime.ExperimentalComposeRuntimeApi
22 import androidx.compose.runtime.RecomposeScope
23 import androidx.compose.runtime.RecomposeScopeImpl
24 import androidx.compose.runtime.Recomposer
25 import androidx.compose.runtime.getCompositionService
26
27 /**
28 * Observe when new compositions are added to a recomposer. This, combined with,
29 * [CompositionObserver], allows observing when any composition is being performed.
30 *
31 * This observer is registered with a [Recomposer] by calling [Recomposer.observe].
32 */
33 @ExperimentalComposeRuntimeApi
34 @Suppress("CallbackName")
35 interface CompositionRegistrationObserver {
36
37 /**
38 * Called whenever a [Composition] is registered with a [Recomposer] for which this is an
39 * observer. A Composition is registered with its Recomposer when it begins its initial
40 * composition, before any content is added. When a [CompositionRegistrationObserver] is
41 * registered, this method will be called for all the [Recomposer]'s currently known
42 * composition.
43 *
44 * This method is called on the same thread that the [Composition] being registered is being
45 * composed on. During the initial dispatch, it is invoked on the same thread that the callback
46 * is being registered on. Implementations of this method should be thread safe as they might be
47 * called on an arbitrary thread.
48 *
49 * @param recomposer The [Recomposer] the [composition] was registered with. This is always the
50 * instance of the [Recomposer] that `observe` was called.
51 * @param composition The [Composition] instance that is being registered with the recomposer.
52 */
onCompositionRegisterednull53 fun onCompositionRegistered(recomposer: Recomposer, composition: Composition)
54
55 /**
56 * Called whenever a [Composition] is unregistered with a [Recomposer] for which this is an
57 * observer. A Composition is unregistered from its Recomposer when the composition is
58 * [disposed][Composition.dispose]. This method is called on the same thread that the
59 * [Composition] being unregistered was composed on. Implementations of this method should be
60 * thread safe as they might be called on an arbitrary thread.
61 *
62 * @param recomposer The [Recomposer] the [composition] was registered with. This is always the
63 * instance of the [Recomposer] that `observe` was called.
64 * @param composition The [Composition] instance that is being unregistered with the recomposer.
65 */
66 fun onCompositionUnregistered(recomposer: Recomposer, composition: Composition)
67 }
68
69 /** Observe when the composition begins and ends. */
70 @ExperimentalComposeRuntimeApi
71 @Suppress("CallbackName")
72 interface CompositionObserver {
73 /**
74 * Called when the composition begins on the [composition]. The [invalidationMap] a map of
75 * invalid recompose scopes that are scheduled to be recomposed. The [CompositionObserver] will
76 * be called for the [composition].
77 *
78 * The scopes in the [invalidationMap] are not guaranteed to be composed. Some cases where they
79 * are not composed are 1) the scope is no longer part of the composition (e.g the parent scope
80 * no longer executed the code branch the scope was a part of) 2) the scope is part of movable
81 * content that was moved out of the composition.
82 *
83 * In the case of movable content, the scope will be recomposed as part of a different
84 * composition when it is moved to that composition or it might be discarded if no other
85 * composition claims it.
86 *
87 * @param composition the composition that is beginning to be recomposed
88 * @param invalidationMap the recompose scopes that will be recomposed by this composition. This
89 * list is empty for the initial composition.
90 */
91 fun onBeginComposition(composition: Composition, invalidationMap: Map<RecomposeScope, Set<Any>>)
92
93 /** Called after composition has been completed for [composition]. */
94 fun onEndComposition(composition: Composition)
95 }
96
97 /** Observer when a recompose scope is being recomposed or when the scope is disposed. */
98 @ExperimentalComposeRuntimeApi
99 @Suppress("CallbackName")
100 interface RecomposeScopeObserver {
101 /** Called just before the recompose scope's recompose lambda is invoked. */
onBeginScopeCompositionnull102 fun onBeginScopeComposition(scope: RecomposeScope)
103
104 /** Called just after the recompose scopes' recompose lambda returns. */
105 fun onEndScopeComposition(scope: RecomposeScope)
106
107 /** Called when the recompose scope is disposed. */
108 fun onScopeDisposed(scope: RecomposeScope)
109 }
110
111 /**
112 * The handle returned by [Composition.observe] and [RecomposeScope.observe]. Calling [dispose] will
113 * prevent further composition observation events from being sent to the registered observer.
114 */
115 @ExperimentalComposeRuntimeApi
116 interface CompositionObserverHandle {
117 /** Unregister the observer. */
118 fun dispose()
119 }
120
121 /**
122 * Register an observer to be notified when a composition is added to or removed from the given
123 * [Recomposer]. When this method is called, the observer will be notified of all currently
124 * registered compositions per the documentation in
125 * [CompositionRegistrationObserver.onCompositionRegistered].
126 *
127 * @param observer the observer that will be informed of new compositions registered with this
128 * [Recomposer].
129 * @return a handle that allows the observer to be disposed and detached from the [Recomposer].
130 */
131 @ExperimentalComposeRuntimeApi
observenull132 fun Recomposer.observe(observer: CompositionRegistrationObserver): CompositionObserverHandle {
133 return addCompositionRegistrationObserver(observer)
134 }
135
136 /**
137 * Observe the composition. Calling this twice on the same composition will implicitly dispose the
138 * previous observer. the [CompositionObserver] will be called for this composition and all
139 * sub-composition, transitively, for which this composition is a context. If, however, [observe] is
140 * called on a sub-composition, it will override the parent composition and notification for it and
141 * all sub-composition of it, will go to its observer instead of the one registered for the parent.
142 *
143 * @param observer the observer that will be informed of composition events for this composition and
144 * all sub-compositions for which this composition is the composition context. Observing a
145 * composition will prevent the parent composition's observer from receiving composition events
146 * about this composition.
147 * @return a handle that allows the observer to be disposed and detached from the composition.
148 * Disposing an observer for a composition with a parent observer will begin sending the events to
149 * the parent composition's observer. A `null` indicates the composition does not support being
150 * observed.
151 */
152 @ExperimentalComposeRuntimeApi
Compositionnull153 fun Composition.observe(observer: CompositionObserver): CompositionObserverHandle? =
154 getCompositionService(CompositionImplServiceKey)?.observe(observer)
155
156 /**
157 * Observer when this scope recomposes.
158 *
159 * @param observer the observer that will be informed of recompose events for this scope.
160 * @return a handle that allows the observer to be disposed and detached from the recompose scope.
161 */
162 @ExperimentalComposeRuntimeApi
163 fun RecomposeScope.observe(observer: RecomposeScopeObserver): CompositionObserverHandle =
164 (this as RecomposeScopeImpl).observe(observer)
165