Cómo migrar a las APIs de Indication y Ripple

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

androidx.compose.foundation:foundation:1.7.0+ y androidx.compose.material:material-ripple:1.7.0+ incluyen los siguientes cambios en la API:

Funciones obsoletas

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

Realiza una de las siguientes acciones:

  • 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 se proporcionan instrucciones para migrar a las APIs nuevas.

Cambio de comportamiento

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

  • 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 las bibliotecas de Material ya no usan rememberRipple(), sino que usan las nuevas APIs de ondas. Por lo tanto, no consultan LocalRippleTheme. Por lo tanto, si configuras LocalRippleTheme en tu aplicación, los componentes de Material no usarán estos valores.

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

Migra de rememberRipple a ripple

Usa una biblioteca de Material

Si usas una biblioteca de Material, reemplaza directamente rememberRipple() por una llamada a ripple() desde la biblioteca correspondiente. Esta API crea una onda con valores derivados de las APIs de temas de Material. Luego, pasa el objeto devuelto 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()
    )
) {
    // ...
}

Debes 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 recordarla. También se puede reutilizar en varios componentes, de manera similar a los modificadores, por lo que se recomienda extraer la creación de ondas a un valor de nivel superior para ahorrar asignaciones.

Implementa 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 la onda, debes proporcionar tu propia API de ondas que delegue a las APIs de nodos de ondas expuestas en material-ripple. Luego, tus componentes pueden usar tu propia onda que consuma los valores de tu tema directamente. Si quieres obtener más información, consulta la documentación para migrar desde RippleTheme.

Migra desde RippleTheme

Usa RippleTheme para inhabilitar una onda en un componente determinado

Las bibliotecas material y material3 exponen RippleConfiguration y LocalRippleConfiguration, que te permiten configurar la apariencia de las ondas dentro de un subárbol. Ten en cuenta que RippleConfiguration y LocalRippleConfiguration solo están diseñados para la personalización por componente. Estas APIs no admiten la personalización global o de todo el tema. Consulta Cómo usar RippleTheme para cambiar de forma global todas las ondas de 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 {
            // ...
        }
    }

Debes modificar el fragmento anterior de la siguiente manera:

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

Usa RippleTheme para cambiar el color o el alfa de una onda para un componente determinado

Como se describió en la sección anterior, RippleConfiguration y LocalRippleConfiguration solo están diseñados para la 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 {
            // ...
        }
    }

Debes 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 {
            // ...
        }
    }

Usa RippleTheme para cambiar de forma global todas las ondas de una aplicación

Anteriormente, podías usar LocalRippleTheme para definir el comportamiento de las ondas en todo el tema. Básicamente, este era un punto de integración entre los datos locales de composición del sistema de diseño personalizado y la onda. En lugar de exponer una primitiva de temas genérica, material-ripple ahora expone una función createRippleModifierNode(). Esta función permite que las bibliotecas del sistema de diseño creen una implementación de wrapper de orden superior, que consulte sus valores de tema y, luego, delegue la implementación de ondas al nodo creado por esta función.

Esto permite que los sistemas de diseño consulten directamente lo que necesitan y expongan las capas de temas configurables por el usuario que sean necesarias, sin tener que ajustarse a lo que se proporciona en la capa material-ripple. Este cambio también hace que sea más explícito el tema o la especificación a los que se ajusta la onda, ya que es la propia API de ondas la que define ese contrato, en lugar de derivarse de forma implícita del tema.

Para obtener orientación, consulta la implementación de la API de ondas en las bibliotecas de Material y reemplaza 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 creas un Indication para pasarlo, como crear un efecto de onda para pasarlo a Modifier.clickable o Modifier.indication, no necesitas realizar ningún cambio. IndicationNodeFactory hereda de Indication, por lo que todo se seguirá compilando y funcionando.

Creando Indication

Si creas tu propia implementación de Indication, la migración debería ser sencilla en la mayoría de los casos. Por ejemplo, considera un Indication que aplica un efecto de escala cuando se presiona:

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 migrar esto en dos pasos:

  1. Migra ScaleIndicationInstance para que sea DrawModifierNode. La plataforma de la API para DrawModifierNode es muy similar a la de IndicationInstance: expone una función ContentDrawScope#draw() que es funcionalmente equivalente a IndicationInstance#drawContent(). Debes cambiar esa función y, luego, implementar la lógica de collectLatest dentro del nodo directamente, en lugar de 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()
            }
        }
    }

    Debes 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 la lógica de recopilación ahora se traslada al nodo, este es un objeto de fábrica muy simple cuya ú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
        }
    }

    Debes 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 en un componente. Sin embargo, en el caso poco frecuente de que crees un IndicationInstance de forma manual con rememberUpdatedInstance, deberás actualizar tu implementación para verificar si el Indication es un IndicationNodeFactory, de modo que puedas usar una implementación más ligera. Por ejemplo, Modifier.indication delegará de forma interna al nodo creado si es un IndicationNodeFactory. De lo contrario, usará Modifier.composed para llamar a rememberUpdatedInstance.