@@ -7,6 +7,7 @@ import android.view.View.OnAttachStateChangeListener
77import androidx.activity.OnBackPressedCallback
88import androidx.activity.OnBackPressedDispatcherOwner
99import androidx.lifecycle.DefaultLifecycleObserver
10+ import androidx.lifecycle.Lifecycle
1011import androidx.lifecycle.LifecycleOwner
1112import androidx.lifecycle.ViewTreeLifecycleOwner
1213
@@ -21,79 +22,123 @@ public typealias BackPressHandler = () -> Unit
2122 * A function to be called if the device back button is pressed while this
2223 * view is attached to a window.
2324 *
24- * Implemented via [OnBackPressedDispatcher][androidx.activity.OnBackPressedDispatcher],
25- * making this a last-registered-first-served mechanism.
25+ * Implemented via [OnBackPressedDispatcher][androidx.activity.OnBackPressedDispatcher].
26+ * That means that this is a last-registered-first-served mechanism, and that it is
27+ * compatible with Compose back button handling.
2628 */
2729@WorkflowUiExperimentalApi
2830public var View .backPressedHandler: BackPressHandler ?
29- get() = handlerWrapperOrNull ?.handler
31+ get() = observerOrNull ?.handler
3032 set(value) {
31- handlerWrapperOrNull ?.stop()
33+ observerOrNull ?.stop()
3234
33- val wrapper = value?.let {
34- HandleBackPressWhenAttached (this , it).apply { start() }
35+ observerOrNull = value?.let {
36+ AttachStateAndLifecycleObserver (this , it).apply { start() }
3537 }
36- setTag(R .id.view_back_handler, wrapper)
3738 }
3839
3940@WorkflowUiExperimentalApi
40- private val View .handlerWrapperOrNull
41- get() = getTag(R .id.view_back_handler) as HandleBackPressWhenAttached ?
41+ private var View .observerOrNull: AttachStateAndLifecycleObserver ?
42+ get() = getTag(R .id.view_back_handler) as AttachStateAndLifecycleObserver ?
43+ set(value) {
44+ setTag(R .id.view_back_handler, value)
45+ }
4246
4347/* *
44- * Uses the [androidx.activity.OnBackPressedDispatcher] to associate a [BackPressHandler]
45- * with a [View].
48+ * This is more complicated than one would hope because [Lifecycle] and memory leaks.
49+ *
50+ * - We need to claim our spot in the
51+ * [OnBackPressedDispatcher][androidx.activity.OnBackPressedDispatcher] immediately,
52+ * to ensure our [onBackPressedCallback] shadows upstream ones, and can be shadowed
53+ * appropriately itself
54+ * - The whole point of this mechanism is to be active only while the view is active
55+ * - That's what [ViewTreeLifecycleOwner] is for, but we can't really find that until
56+ * we're attached -- which often does not happen in the order required for registering
57+ * with the dispatcher
58+ *
59+ * So, our [start] is called immediately, to get [onBackPressedCallback] listed at the right
60+ * spot in the dispatcher's stack. But the [onBackPressedCallback]'s enabled / disabled state
61+ * is tied to whether the [view] is attached or not.
4662 *
47- * - Registers [handler] on [start]
48- * - Enables and disables it as [view] is attached to or detached from its window
49- * - De-registers it on [stop], or when its [lifecycle][ViewTreeLifecycleOwner] is destroyed
63+ * Also note that we expect to find a [ViewTreeLifecycleOwner] at attach time,
64+ * so that we can know when it's time to remove the [onBackPressedCallback] from
65+ * the dispatch stack
66+ * ([no memory leaks please](https://github.com/square/workflow-kotlin/issues/889)).
67+ *
68+ * Why is it okay to wait for the [ViewTreeLifecycleOwner] to be destroyed before we
69+ * remove [onBackPressedCallback] from the dispatcher? In normal apps that's
70+ * the `Activity` or a `Fragment`, which will live a very long time, but Workflow UI
71+ * is more controlling than that. `WorkflowViewStub` and the rest of the stock container
72+ * classes use `WorkflowLifecycleOwner` to provide a short lived [ViewTreeLifecycleOwner]
73+ * for each [View] they create, and tear it down before moving to the next one.
74+ *
75+ * None the less, as a belt-and-suspenders guard against leaking,
76+ * we also take care to null out the pointer from the [onBackPressedCallback] to the
77+ * actual [handler] while the [view] is detached. We can't be confident that the
78+ * [ViewTreeLifecycleOwner] we find will be a well behaved one that was put in place
79+ * by `WorkflowLifecycleOwner`. Who knows what adventures our clients will get up to.
5080 */
5181@WorkflowUiExperimentalApi
52- private class HandleBackPressWhenAttached (
82+ private class AttachStateAndLifecycleObserver (
5383 private val view : View ,
5484 val handler : BackPressHandler
5585) : OnAttachStateChangeListener, DefaultLifecycleObserver {
56- private val onBackPressedCallback = object : OnBackPressedCallback (false ) {
57- override fun handleOnBackPressed () {
58- handler.invoke()
59- }
60- }
86+ private val onBackPressedCallback = NullableOnBackPressedCallback ()
87+ private var lifecycleOrNull: Lifecycle ? = null
6188
6289 fun start () {
6390 view.context.onBackPressedDispatcherOwnerOrNull()
6491 ?.let { owner ->
6592 owner.onBackPressedDispatcher.addCallback(owner, onBackPressedCallback)
6693 view.addOnAttachStateChangeListener(this )
6794 if (view.isAttachedToWindow) onViewAttachedToWindow(view)
68-
69- // We enable the handler only while its view is attached to a window.
70- // This ensures that a temporarily removed view (e.g. for caching)
71- // does not participate in back button handling.
72- ViewTreeLifecycleOwner .get(view)?.lifecycle?.addObserver(this )
7395 }
7496 }
7597
7698 fun stop () {
7799 onBackPressedCallback.remove()
78100 view.removeOnAttachStateChangeListener(this )
79- ViewTreeLifecycleOwner .get(view)?.lifecycle ?.removeObserver(this )
101+ lifecycleOrNull ?.removeObserver(this )
80102 }
81103
82104 override fun onViewAttachedToWindow (attachedView : View ) {
83105 require(view == = attachedView)
84- onBackPressedCallback.isEnabled = true
106+ lifecycleOrNull?.let { lifecycle ->
107+ lifecycle.removeObserver(this )
108+ lifecycleOrNull = null
109+ }
110+ ViewTreeLifecycleOwner .get(view)?.lifecycle?.let { lifecycle ->
111+ lifecycleOrNull = lifecycle
112+ onBackPressedCallback.handlerOrNull = handler
113+ onBackPressedCallback.isEnabled = true
114+ lifecycle.addObserver(this )
115+ }
116+ ? : error(
117+ " Expected to find a ViewTreeLifecycleOwner to manage the " +
118+ " backPressedHandler ($handler ) for $view "
119+ )
85120 }
86121
87122 override fun onViewDetachedFromWindow (detachedView : View ) {
88123 require(view == = detachedView)
89124 onBackPressedCallback.isEnabled = false
125+ onBackPressedCallback.handlerOrNull = null
90126 }
91127
92128 override fun onDestroy (owner : LifecycleOwner ) {
93129 stop()
94130 }
95131}
96132
133+ @WorkflowUiExperimentalApi
134+ internal class NullableOnBackPressedCallback : OnBackPressedCallback (false ) {
135+ var handlerOrNull: BackPressHandler ? = null
136+
137+ override fun handleOnBackPressed () {
138+ handlerOrNull?.invoke()
139+ }
140+ }
141+
97142@WorkflowUiExperimentalApi
98143public tailrec fun Context.onBackPressedDispatcherOwnerOrNull (): OnBackPressedDispatcherOwner ? =
99144 when (this ) {
0 commit comments