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