Cómo migrar a las APIs de Indication y Ripple

Para mejorar el rendimiento de composición de los componentes interactivos que usan Modifier.clickable, incorporamos nuevas APIs. Estas APIs permiten más implementaciones eficientes de Indication, como ripples.

androidx.compose.foundation:foundation:1.7.0+ y androidx.compose.material:material-ripple:1.7.0+ incluyen la siguiente API cambios:

Obsoleto

Reemplazo

Indication#rememberUpdatedInstance

IndicationNodeFactory

rememberRipple()

En su lugar, se proporcionan nuevas APIs de ripple() en las bibliotecas de Material.

Nota: En este contexto, "Bibliotecas de Material" hace referencia a androidx.compose.material:material, androidx.compose.material3:material3, androidx.wear.compose:compose-material y androidx.wear.compose:compose-material3.

RippleTheme

Por ejemplo, puedes hacer lo siguiente:

  • Usa las APIs de RippleConfiguration de la biblioteca de Material.
  • Crea tu propia implementación de ondas del sistema de diseño

En esta página, se describe el impacto del cambio de comportamiento y las instrucciones para migrar a las nuevas APIs.

Cambio de comportamiento

Las siguientes versiones de la biblioteca incluyen un cambio en el comportamiento de las ondas:

  • androidx.compose.material:material:1.7.0+
  • androidx.compose.material3:material3:1.3.0+
  • androidx.wear.compose:compose-material:1.4.0+

Estas versiones de bibliotecas de Material ya no usan rememberRipple(). en su lugar, usan las nuevas APIs de ripple. Como resultado, no consultan LocalRippleTheme. Por lo tanto, si configuras LocalRippleTheme en tu aplicación, Material componentes no usarán estos valores.

En la siguiente sección, se describe cómo volver temporalmente al comportamiento anterior sin migrar; sin embargo, te recomendamos migrar a las APIs nuevas. Para instrucciones de migración, consulta Cómo migrar de rememberRipple a ripple y en las secciones posteriores.

Actualiza la versión de la biblioteca de Material sin migrar

Para desbloquear las versiones actualizadas de las bibliotecas, puedes usar el API de LocalUseFallbackRippleImplementation CompositionLocal para configurar Componentes de Material para volver al comportamiento anterior:

CompositionLocalProvider(LocalUseFallbackRippleImplementation provides true) {
    MaterialTheme {
        App()
    }
}

Asegúrate de proporcionar esto fuera de MaterialTheme para que los ondas anteriores puedan proporcionarse a través de LocalIndication.

En las siguientes secciones, se describe cómo migrar a las APIs nuevas.

Migra de rememberRipple a ripple

Cómo usar una biblioteca de Material

Si usas una biblioteca de Material, reemplaza directamente rememberRipple() por un llamada a ripple() desde la biblioteca correspondiente. Esta API crea un ripple, con valores derivados de las APIs de tema de Material. Luego, pasa los valores de objeto a Modifier.clickable o a otros componentes.

Por ejemplo, en el siguiente fragmento, se usan las APIs obsoletas:

Box(
    Modifier.clickable(
        onClick = {},
        interactionSource = remember { MutableInteractionSource() },
        indication = rememberRipple()
    )
) {
    // ...
}

Deberías modificar el fragmento anterior de la siguiente manera:

@Composable
private fun RippleExample() {
    Box(
        Modifier.clickable(
            onClick = {},
            interactionSource = remember { MutableInteractionSource() },
            indication = ripple()
        )
    ) {
        // ...
    }
}

Ten en cuenta que ripple() ya no es una función de componibilidad y no es necesario recordarse. También se puede reutilizar en varios componentes, de manera similar a modificadores, así que considera extraer la creación de ondas a un valor de nivel superior para y guardar asignaciones.

Implementación de un sistema de diseño personalizado

Si implementas tu propio sistema de diseño y antes usabas rememberRipple() junto con un RippleTheme personalizado para configurar el efecto de ondas, debes proporcionar tu propia API de ripple que delega al nodo de ripple, APIs expuestas en material-ripple. Luego, tus componentes pueden usar tu propio efecto que consuma los valores de tu tema directamente. Para obtener más información, consulta Cómo migrar desde RippleTheme.

Migra desde RippleTheme

Cómo inhabilitar temporalmente el cambio de comportamiento

Las bibliotecas de Material tienen un CompositionLocal temporal, LocalUseFallbackRippleImplementation, que puedes usar para configurar todo Componentes de Material a los que volver a usar rememberRipple De esta manera, rememberRipple continúa realizando consultas a LocalRippleTheme.

En el siguiente fragmento de código, se muestra cómo usar la API de LocalUseFallbackRippleImplementation CompositionLocal:

CompositionLocalProvider(LocalUseFallbackRippleImplementation provides true) {
    MaterialTheme {
        App()
    }
}

Si usas un tema de app personalizado que se basa en Material, puedes Proporciona de manera segura la composición local como parte del tema de tu app:

@OptIn(ExperimentalMaterialApi::class)
@Composable
fun MyAppTheme(content: @Composable () -> Unit) {
    CompositionLocalProvider(LocalUseFallbackRippleImplementation provides true) {
        MaterialTheme(content = content)
    }
}

Para obtener más información, consulta la actualización de la versión de la biblioteca de Material sin Migración.

Cómo usar RippleTheme para inhabilitar un ripple para un componente determinado

Las bibliotecas material y material3 exponen RippleConfiguration y LocalRippleConfiguration, que te permiten configurar la apariencia de ondas en un subárbol. Ten en cuenta que RippleConfiguration y Las LocalRippleConfiguration son experimentales y solo están diseñadas por componente personalización. La personalización global o de todo el tema no es compatible con estos APIs; consulta Usar RippleTheme para cambiar globalmente todos los ondas en una aplicación para obtener más información sobre ese caso de uso.

Por ejemplo, en el siguiente fragmento, se usan las APIs obsoletas:

private object DisabledRippleTheme : RippleTheme {

    @Composable
    override fun defaultColor(): Color = Color.Transparent

    @Composable
    override fun rippleAlpha(): RippleAlpha = RippleAlpha(0f, 0f, 0f, 0f)
}

// ...
    CompositionLocalProvider(LocalRippleTheme provides DisabledRippleTheme) {
        Button {
            // ...
        }
    }

Deberías modificar el fragmento anterior de la siguiente manera:

CompositionLocalProvider(LocalRippleConfiguration provides null) {
    Button {
        // ...
    }
}

Uso de RippleTheme para cambiar el color/alfa de un ripple para un componente determinado

Como se describió en la sección anterior, RippleConfiguration y LocalRippleConfiguration son APIs experimentales y están diseñadas solo para personalización por componente.

Por ejemplo, en el siguiente fragmento, se usan las APIs obsoletas:

private object DisabledRippleThemeColorAndAlpha : RippleTheme {

    @Composable
    override fun defaultColor(): Color = Color.Red

    @Composable
    override fun rippleAlpha(): RippleAlpha = MyRippleAlpha
}

// ...
    CompositionLocalProvider(LocalRippleTheme provides DisabledRippleThemeColorAndAlpha) {
        Button {
            // ...
        }
    }

Deberías modificar el fragmento anterior de la siguiente manera:

@OptIn(ExperimentalMaterialApi::class)
private val MyRippleConfiguration =
    RippleConfiguration(color = Color.Red, rippleAlpha = MyRippleAlpha)

// ...
    CompositionLocalProvider(LocalRippleConfiguration provides MyRippleConfiguration) {
        Button {
            // ...
        }
    }

Cómo usar RippleTheme para cambiar globalmente todos los ecos en una aplicación

Anteriormente, se podía usar LocalRippleTheme para definir el comportamiento de la onda en un en todo el tema. Básicamente, esto era un punto de integración de la composición del sistema de diseño local y de ripple. En lugar de exponer una dirección primitivo de temas, material-ripple ahora expone un createRippleModifierNode() . Esta función permite que las bibliotecas de sistemas de diseño ordenar la implementación de wrapper, que consultan sus valores de tema y, luego, delegan la implementación de ripple en el nodo creado por esta función.

Esto permite que los sistemas de diseño consulten directamente lo que necesitan y expongan y las capas de temas configurables por el usuario necesarias lo que se proporciona en la capa material-ripple. Este cambio también hace más explícito a qué tema/especificación se ajusta la onda, ya que es el La API de ripple que define ese contrato, en lugar de ser implícitamente derivadas del tema.

Para obtener orientación, consulta la implementación de la API de ripple en Material. y reemplazar las llamadas a los elementos locales de composición de Material según sea necesario para tu propio sistema de diseño.

Migra de Indication a IndicationNodeFactory

Pasando por Indication

Si solo estás creando un Indication para pasar, por ejemplo, si creas un ripple para pasar a Modifier.clickable o Modifier.indication, no no necesitas hacer ningún cambio. IndicationNodeFactory hereda de Indication, por lo que todo seguirá compilando y funcionando.

Creando Indication

Si creas tu propia implementación de Indication, la migración debe puede ser simple en la mayoría de los casos. Por ejemplo, considera un Indication que aplica un efecto de escala al presionar:

object ScaleIndication : Indication {
    @Composable
    override fun rememberUpdatedInstance(interactionSource: InteractionSource): IndicationInstance {
        // key the remember against interactionSource, so if it changes we create a new instance
        val instance = remember(interactionSource) { ScaleIndicationInstance() }

        LaunchedEffect(interactionSource) {
            interactionSource.interactions.collectLatest { interaction ->
                when (interaction) {
                    is PressInteraction.Press -> instance.animateToPressed(interaction.pressPosition)
                    is PressInteraction.Release -> instance.animateToResting()
                    is PressInteraction.Cancel -> instance.animateToResting()
                }
            }
        }

        return instance
    }
}

private class ScaleIndicationInstance : IndicationInstance {
    var currentPressPosition: Offset = Offset.Zero
    val animatedScalePercent = Animatable(1f)

    suspend fun animateToPressed(pressPosition: Offset) {
        currentPressPosition = pressPosition
        animatedScalePercent.animateTo(0.9f, spring())
    }

    suspend fun animateToResting() {
        animatedScalePercent.animateTo(1f, spring())
    }

    override fun ContentDrawScope.drawIndication() {
        scale(
            scale = animatedScalePercent.value,
            pivot = currentPressPosition
        ) {
            this@drawIndication.drawContent()
        }
    }
}

Puedes migrarla en dos pasos:

  1. Migra ScaleIndicationInstance para que sea DrawModifierNode. La plataforma de la API para DrawModifierNode es muy similar a IndicationInstance: expone un función ContentDrawScope#draw() que es funcionalmente equivalente a IndicationInstance#drawContent() Debes cambiar esa función y, luego, implementa la lógica collectLatest directamente dentro del nodo, en lugar del Indication

    Por ejemplo, en el siguiente fragmento, se usan las APIs obsoletas:

    private class ScaleIndicationInstance : IndicationInstance {
        var currentPressPosition: Offset = Offset.Zero
        val animatedScalePercent = Animatable(1f)
    
        suspend fun animateToPressed(pressPosition: Offset) {
            currentPressPosition = pressPosition
            animatedScalePercent.animateTo(0.9f, spring())
        }
    
        suspend fun animateToResting() {
            animatedScalePercent.animateTo(1f, spring())
        }
    
        override fun ContentDrawScope.drawIndication() {
            scale(
                scale = animatedScalePercent.value,
                pivot = currentPressPosition
            ) {
                this@drawIndication.drawContent()
            }
        }
    }

    Deberías modificar el fragmento anterior de la siguiente manera:

    private class ScaleIndicationNode(
        private val interactionSource: InteractionSource
    ) : Modifier.Node(), DrawModifierNode {
        var currentPressPosition: Offset = Offset.Zero
        val animatedScalePercent = Animatable(1f)
    
        private suspend fun animateToPressed(pressPosition: Offset) {
            currentPressPosition = pressPosition
            animatedScalePercent.animateTo(0.9f, spring())
        }
    
        private suspend fun animateToResting() {
            animatedScalePercent.animateTo(1f, spring())
        }
    
        override fun onAttach() {
            coroutineScope.launch {
                interactionSource.interactions.collectLatest { interaction ->
                    when (interaction) {
                        is PressInteraction.Press -> animateToPressed(interaction.pressPosition)
                        is PressInteraction.Release -> animateToResting()
                        is PressInteraction.Cancel -> animateToResting()
                    }
                }
            }
        }
    
        override fun ContentDrawScope.draw() {
            scale(
                scale = animatedScalePercent.value,
                pivot = currentPressPosition
            ) {
                this@draw.drawContent()
            }
        }
    }

  2. Migra ScaleIndication para implementar IndicationNodeFactory. Debido a que el la lógica de recopilación se traslada al nodo, es una forma sencilla de su única responsabilidad es crear una instancia de nodo.

    Por ejemplo, en el siguiente fragmento, se usan las APIs obsoletas:

    object ScaleIndication : Indication {
        @Composable
        override fun rememberUpdatedInstance(interactionSource: InteractionSource): IndicationInstance {
            // key the remember against interactionSource, so if it changes we create a new instance
            val instance = remember(interactionSource) { ScaleIndicationInstance() }
    
            LaunchedEffect(interactionSource) {
                interactionSource.interactions.collectLatest { interaction ->
                    when (interaction) {
                        is PressInteraction.Press -> instance.animateToPressed(interaction.pressPosition)
                        is PressInteraction.Release -> instance.animateToResting()
                        is PressInteraction.Cancel -> instance.animateToResting()
                    }
                }
            }
    
            return instance
        }
    }

    Deberías modificar el fragmento anterior de la siguiente manera:

    object ScaleIndicationNodeFactory : IndicationNodeFactory {
        override fun create(interactionSource: InteractionSource): DelegatableNode {
            return ScaleIndicationNode(interactionSource)
        }
    
        override fun hashCode(): Int = -1
    
        override fun equals(other: Any?) = other === this
    }

Usa Indication para crear un IndicationInstance

En la mayoría de los casos, debes usar Modifier.indication para mostrar Indication para una este componente. Sin embargo, en el raro caso de que crees manualmente un IndicationInstance usando rememberUpdatedInstance, debes actualizar tu implementación para verificar si Indication es un IndicationNodeFactory, de modo que puedes usar una implementación más ligera. Por ejemplo, Modifier.indication delegar internamente al nodo creado si es un IndicationNodeFactory. Si no, usará Modifier.composed para llamar a rememberUpdatedInstance.