ModifierNodeElement.kt

/*
 * Copyright 2022 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.node

import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.InspectableValue
import androidx.compose.ui.platform.InspectorInfo
import androidx.compose.ui.platform.ValueElement
import androidx.compose.ui.util.fastForEach
import kotlin.reflect.KProperty1
import kotlin.reflect.jvm.isAccessible

/**
 * A [Modifier.Element] which manages an instance of a particular [Modifier.Node] implementation. A
 * given [Modifier.Node] implementation can only be used when a [ModifierNodeElement] which creates
 * and updates that implementation is applied to a Layout.
 *
 * A [ModifierNodeElement] should be very lightweight, and do little more than hold the information
 * necessary to create and maintain an instance of the associated [Modifier.Node] type.
 *
 * @sample androidx.compose.ui.samples.ModifierNodeElementSample
 * @sample androidx.compose.ui.samples.SemanticsModifierNodeSample
 *
 * @see Modifier.Node
 * @see Modifier.Element
 */
@ExperimentalComposeUiApi
abstract class ModifierNodeElement<N : Modifier.Node> : Modifier.Element, InspectableValue {

    /**
     * If this property returns `true`, then nodes will be automatically invalidated after the
     * [update] callback completes (For example, if the returned Node is a [DrawModifierNode], its
     * [DrawModifierNode.invalidateDraw] function will be invoked automatically as part of
     * auto invalidation).
     *
     * This is enabled by default, and provides a convenient mechanism to schedule invalidation
     * and apply changes made to the modifier. You may choose to set this to `false` if your
     * modifier has auto-invalidatable properties that do not frequently require invalidation to
     * improve performance by skipping unnecessary invalidation. If `autoInvalidate` is set to
     * `false`, you must call the appropriate invalidate functions manually in [update] for the
     * new attributes to become visible.
     */
    open val autoInvalidate: Boolean
        get() = true

    private var _inspectorValues: InspectorInfo? = null
    private val inspectorValues: InspectorInfo
        get() = _inspectorValues ?: InspectorInfo()
            .apply {
                name = this@ModifierNodeElement::class.simpleName
                inspectableProperties()
            }
            .also { _inspectorValues = it }

    final override val nameFallback: String?
        get() = inspectorValues.name

    final override val valueOverride: Any?
        get() = inspectorValues.value

    final override val inspectableElements: Sequence<ValueElement>
        get() = inspectorValues.properties

    /**
     * This will be called the first time the modifier is applied to the Layout and it should
     * construct and return the corresponding [Modifier.Node] instance.
     */
    abstract fun create(): N

    /**
     * Called when a modifier is applied to a Layout whose inputs have changed from the previous
     * application. This function will have the current node instance passed in as a parameter, and
     * it is expected that the node will be brought up to date.
     */
    abstract fun update(node: N): N

    /**
     * Populates an [InspectorInfo] object with attributes to display in the layout inspector. This
     * is called by tooling to resolve the properties of this modifier. By convention, implementors
     * should set the [name][InspectorInfo.name] to the function name of the modifier.
     *
     * The default implementation will attempt to reflectively populate the inspector info with the
     * properties declared on the subclass. It will also set the [name][InspectorInfo.name] property
     * to the name of this instance's class by default (not the name of the modifier function).
     * Modifier property population depends on the kotlin-reflect library. If it is not in the
     * classpath at runtime, the default implementation of this function will populate the
     * properties with an error message.
     *
     * If you override this function and provide the properties you wish to display, you do not need
     * to call `super`. Doing so may result in duplicate properties appearing in the layout
     * inspector.
     */
    open fun InspectorInfo.inspectableProperties() {
        val element = this@ModifierNodeElement
        val elementClass = element::class
        try {
            elementClass.members
                // Properties declared in the constructor will appear after ones defined in the
                // class, so sort by the property name to make the result more well-defined.
                .sortedBy { it.name }
                .fastForEach { member ->
                    if (member is KProperty1<*, *> && member.name !in builtInProperties) {
                        try {
                            @Suppress("UNCHECKED_CAST")
                            val property = (member as KProperty1<ModifierNodeElement<N>, Any?>)
                            property.isAccessible = true
                            properties[property.name] = property.get(element)
                        } catch (e: Exception) {
                            // Do nothing. Just ignore the field and prevent the error from crashing
                            // the application and ending the debugging session.
                        }
                    }
                }
        } catch (e: KotlinReflectionNotSupportedError) {
            properties["inspector error"] = "Can't automatically resolve properties of $element " +
                "because Kotlin reflection is unavailable. Consider adding" +
                "'debugImplementation \"org.jetbrains.kotlin:kotlin-reflect:\$kotlin_version\"' " +
                "to your module's gradle dependencies block."
        }
    }

    // Require hashCode() to be implemented. Using a data class is sufficient. Singletons and
    // modifiers with no parameters may implement this function by returning an arbitrary constant.
    abstract override fun hashCode(): Int

    // Require equals() to be implemented. Using a data class is sufficient. Singletons may
    // implement this function with referential equality (`this === other`). Modifiers with no
    // inputs may implement this function by checking the type of the other object.
    abstract override fun equals(other: Any?): Boolean

    private companion object {
        /**
         * A list of properties defined by [ModifierNodeElement], computed with reflection at
         * runtime. We use this list in the default implementation of [inspectableElements] as a
         * way to hide properties defined by `ModifierNodeElement` from the layout inspector. Just
         * looking at the name is acceptable because you can't have multiple properties with the
         * same name and can't create a property that's been defined in `ModifierNodeElement`
         * without overriding it.
         */
        private val builtInProperties: Set<String> by lazy(LazyThreadSafetyMode.NONE) {
            try {
                buildSet {
                    ModifierNodeElement::class.members.forEach { member ->
                        if (member is KProperty1<*, *>) {
                            add(member.name)
                        }
                    }
                }
            } catch (e: Exception) {
                emptySet()
            } catch (e: KotlinReflectionNotSupportedError) {
                emptySet()
            }
        }
    }
}