ViewCompositionStrategy.android.kt

/*
 * Copyright 2020 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package androidx.compose.ui.platform

import android.view.View
import androidx.compose.ui.platform.ViewCompositionStrategy.DisposeOnDetachedFromWindow
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleEventObserver
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.ViewTreeLifecycleOwner

/**
 * A strategy for managing the underlying Composition of Compose UI [View]s such as
 * [ComposeView] and [AbstractComposeView]. See [AbstractComposeView.setViewCompositionStrategy].
 *
 * Compose views involve ongoing work and registering the composition with external
 * event sources. These registrations can cause the composition to remain live and
 * ineligible for garbage collection for long after the host View may have been abandoned.
 * These resources and registrations can be released manually at any time by calling
 * [AbstractComposeView.disposeComposition] and a new composition will be created automatically
 * when needed. A [ViewCompositionStrategy] defines a strategy for disposing the composition
 * automatically at an appropriate time.
 *
 * By default, Compose UI views are configured to [DisposeOnDetachedFromWindow]. The composition
 * will be disposed automatically when the view is detached from a window. For use cases that
 * involve frequent remove/add operations such as children of a `RecyclerView` it may be more
 * appropriate to allow the composition to persist across removals for efficiency.
 */
interface ViewCompositionStrategy {

    /**
     * Install this strategy for [view] and return a function that will uninstall it later.
     * This function should not be called directly; it is called by
     * [AbstractComposeView.setViewCompositionStrategy] after uninstalling the previous strategy.
     */
    fun installFor(view: AbstractComposeView): () -> Unit

    /**
     * This companion object may be used to define extension factory functions for other
     * strategies to aid in discovery via autocomplete. e.g.:
     * `fun ViewCompositionStrategy.Companion.MyStrategy(): MyStrategy`
     */
    companion object

    /**
     * [ViewCompositionStrategy] that disposes the composition whenever the view becomes detached
     * from a window. If the user of a Compose UI view never explicitly calls
     * [AbstractComposeView.createComposition], this strategy is always safe and will always
     * clean up composition resources with no explicit action required - just use the view like
     * any other View and let garbage collection do the rest. (If
     * [AbstractComposeView.createComposition] is called while the view is detached from a window,
     * [AbstractComposeView.disposeComposition] must be called manually if the view is not later
     * attached to a window.)
     *
     * [DisposeOnDetachedFromWindow] is the default strategy for [AbstractComposeView] and
     * [ComposeView].
     */
    object DisposeOnDetachedFromWindow : ViewCompositionStrategy {
        override fun installFor(view: AbstractComposeView): () -> Unit {
            val listener = object : View.OnAttachStateChangeListener {
                override fun onViewAttachedToWindow(v: View) {}

                override fun onViewDetachedFromWindow(v: View?) {
                    view.disposeComposition()
                }
            }
            view.addOnAttachStateChangeListener(listener)
            return { view.removeOnAttachStateChangeListener(listener) }
        }
    }

    /**
     * [ViewCompositionStrategy] that disposes the composition when [lifecycle] is
     * [destroyed][Lifecycle.Event.ON_DESTROY]. This strategy is appropriate for Compose UI views
     * that share a 1-1 relationship with a known [LifecycleOwner].
     */
    class DisposeOnLifecycleDestroyed(
        private val lifecycle: Lifecycle
    ) : ViewCompositionStrategy {
        constructor(lifecycleOwner: LifecycleOwner) : this(lifecycleOwner.lifecycle)

        override fun installFor(view: AbstractComposeView): () -> Unit =
            installForLifecycle(view, lifecycle)
    }

    /**
     * [ViewCompositionStrategy] that disposes the composition when the [ViewTreeLifecycleOwner]
     * of the next window the view is attached to is [destroyed][Lifecycle.Event.ON_DESTROY].
     * This strategy is appropriate for Compose UI views that share a 1-1 relationship with
     * their closest [ViewTreeLifecycleOwner], such as a Fragment view.
     */
    object DisposeOnViewTreeLifecycleDestroyed : ViewCompositionStrategy {
        override fun installFor(view: AbstractComposeView): () -> Unit {
            if (view.isAttachedToWindow) {
                val lco = checkNotNull(ViewTreeLifecycleOwner.get(view)) {
                    "View tree for $view has no ViewTreeLifecycleOwner"
                }
                return installForLifecycle(view, lco.lifecycle)
            } else {
                // We change this reference after we successfully attach
                var disposer: () -> Unit
                val listener = object : View.OnAttachStateChangeListener {
                    override fun onViewAttachedToWindow(v: View?) {
                        val lco = checkNotNull(ViewTreeLifecycleOwner.get(view)) {
                            "View tree for $view has no ViewTreeLifecycleOwner"
                        }
                        disposer = installForLifecycle(view, lco.lifecycle)

                        // Ensure this runs only once
                        view.removeOnAttachStateChangeListener(this)
                    }

                    override fun onViewDetachedFromWindow(v: View?) {}
                }
                view.addOnAttachStateChangeListener(listener)
                disposer = { view.removeOnAttachStateChangeListener(listener) }
                return { disposer() }
            }
        }
    }
}

private fun installForLifecycle(view: AbstractComposeView, lifecycle: Lifecycle): () -> Unit {
    check(lifecycle.currentState > Lifecycle.State.DESTROYED) {
        "Cannot configure $view to disposeComposition at Lifecycle ON_DESTROY: $lifecycle" +
            "is already destroyed"
    }
    val observer = LifecycleEventObserver { _, event ->
        if (event == Lifecycle.Event.ON_DESTROY) {
            view.disposeComposition()
        }
    }
    lifecycle.addObserver(observer)
    return { lifecycle.removeObserver(observer) }
}