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, presentamos nuevas APIs. Estas APIs permiten implementaciones de Indication más eficientes, como ondas.

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

Obsoleto

Reemplazo

Indication#rememberUpdatedInstance

IndicationNodeFactory

rememberRipple()

En su lugar, se proporcionaron 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 APIs nuevas.

Cambio de comportamiento

Las siguientes versiones de la biblioteca incluyen un cambio de comportamiento de 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 las bibliotecas de Material ya no usan rememberRipple(), sino que usan las nuevas APIs de ripple. Como resultado, no consultan LocalRippleTheme. Por lo tanto, si configuras LocalRippleTheme en tu aplicación, los componentes de Material no usarán estos valores.

En la siguiente sección, se describe cómo recurrir de manera temporal al comportamiento anterior sin migrar. Sin embargo, te recomendamos que migres a las APIs nuevas. Para obtener instrucciones de migración, consulta Migra de rememberRipple a ripple y las secciones posteriores.

Cómo actualizar la versión de la biblioteca de Material sin migrar

Para desbloquear las versiones actualizadas de la biblioteca, puedes usar la API temporal de LocalUseFallbackRippleImplementation CompositionLocal para configurar los componentes de Material y volver al comportamiento anterior:

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

Asegúrate de proporcionar esto fuera del MaterialTheme para que las ondas antiguas se puedan proporcionar a través de LocalIndication.

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

Migra de rememberRipple a ripple

Cómo usar 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 que se muestra a Modifier.clickable o a otros componentes.

Por ejemplo, el siguiente fragmento usa 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 recordarla. También se puede volver a usar en varios componentes, de manera similar a los modificadores, por lo que debes considerar extraer la creación de ondas a un valor de nivel superior para guardar asignaciones.

Implementación de un sistema de diseño personalizado

Si estás implementando tu propio sistema de diseño y antes usabas rememberRipple() junto con un RippleTheme personalizado para configurar el ripple, debes proporcionar tu propia API de ripple que se delegue a las APIs del nodo de ripple expuestas en material-ripple. Luego, tus componentes pueden usar tu propia onda que consume directamente los valores de tu tema. 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 todos los componentes de Material y recurrir a rememberRipple. De esta manera, rememberRipple continúa realizando consultas en 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 compilado sobre Material, puedes proporcionar, de forma segura, el elemento local de composición 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 sección Cómo actualizar la versión de la biblioteca de Material sin migrar.

Cómo usar RippleTheme para inhabilitar un efecto de ondas para un componente determinado

Las bibliotecas material y material3 exponen RippleConfiguration y LocalRippleConfiguration, lo que te permite configurar la apariencia de ondas dentro de un subárbol. Ten en cuenta que RippleConfiguration y LocalRippleConfiguration son experimentales y solo están destinados a la personalización por componente. La personalización global o de todo el tema no es compatible con estas APIs. Consulta Cómo usar RippleTheme para cambiar de forma global todas las ondas de una aplicación si quieres obtener más información sobre ese caso de uso.

Por ejemplo, el siguiente fragmento usa 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:

@OptIn(ExperimentalMaterialApi::class)
private val DisabledRippleConfiguration =
    RippleConfiguration(isEnabled = false)

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

Cómo usar RippleTheme para cambiar el color o el valor alfa de una onda para un componente determinado

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

Por ejemplo, el siguiente fragmento usa 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 todas las ondas en una aplicación

Anteriormente, se podía usar LocalRippleTheme para definir el comportamiento de ondas a nivel de todo el tema. En esencia, este era un punto de integración entre los entornos locales de composición del sistema de diseño personalizado y el efecto de ondas. En lugar de exponer un primitivo de tema genérico, material-ripple ahora expone una función createRippleModifierNode(). Esta función permite que las bibliotecas del sistema de diseño creen implementaciones de wrapper de orden superior, que consulten sus valores de tema y luego deleguen la implementación de ripple al nodo creado por esta función.

Esto permite que los sistemas de diseño consulten directamente lo que necesitan y expongan en la parte superior cualquier capa de temas configurable por el usuario 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 a qué tema o especificación cumple la onda, ya que es la API de ripple en sí misma la que define ese contrato, en lugar de derivar de manera implícita del tema.

Para obtener orientación, consulta la implementación de la API de ripple en las bibliotecas de Material y reemplaza las llamadas a 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, como una ondulación que pasará a Modifier.clickable o Modifier.indication, no es necesario que realices ningún cambio. IndicationNodeFactory hereda de Indication, por lo que todo seguirá compilándose y funcionando.

Creando Indication

Si creas tu propia implementación de Indication, la migración debería ser simple en la mayoría de los casos. Por ejemplo, considera una 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 migrarlo en dos pasos:

  1. Migra ScaleIndicationInstance para que sea un DrawModifierNode. La plataforma de la API para DrawModifierNode es muy similar a 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 collectLatest directamente dentro del nodo, en lugar de Indication.

    Por ejemplo, el siguiente fragmento usa 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 la lógica de recopilación ahora se traslada al nodo, se trata de un objeto de fábrica muy simple cuya única responsabilidad es crear una instancia de nodo.

    Por ejemplo, el siguiente fragmento usa 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 de un componente. Sin embargo, en el caso poco probable de que crees manualmente un IndicationInstance con rememberUpdatedInstance, debes actualizar tu implementación para verificar si 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.