To improve composition performance of interactive components that use
Modifier.clickable
, we've introduced new APIs. These APIs allow for more
efficient Indication
implementations, such as ripples.
androidx.compose.foundation:foundation:1.7.0+
and
androidx.compose.material:material-ripple:1.7.0+
include the following API
changes:
Deprecated |
Replacement |
---|---|
|
|
|
New Note: In this context, "Material libraries" refers to |
|
Either:
|
This page describes the behavior change impact and instructions for migrating to the new APIs.
Behavior change
The following library versions include a ripple behavior change:
androidx.compose.material:material:1.7.0+
androidx.compose.material3:material3:1.3.0+
androidx.wear.compose:compose-material:1.4.0+
These versions of Material libraries no longer use rememberRipple()
; instead,
they use the new ripple APIs. As a result, they do not query LocalRippleTheme
.
Therefore, if you set LocalRippleTheme
in your application, Material
components will not use these values.
The following section describes how to temporarily fall back to the old behavior
without migrating; however, we recommend migrating to the new APIs. For
migration instructions, see Migrate from rememberRipple
to ripple
and the subsequent sections.
Upgrade Material library version without migrating
To unblock upgrading library versions, you can use the temporary
LocalUseFallbackRippleImplementation CompositionLocal
API to configure
Material components to fall back to the old behavior:
CompositionLocalProvider(LocalUseFallbackRippleImplementation provides true) { MaterialTheme { App() } }
Make sure to provide this outside the MaterialTheme
so the old ripples can
be provided through LocalIndication
.
The following sections describe how to migrate to the new APIs.
Migrate from rememberRipple
to ripple
Using a Material library
If you are using a Material library, directly replace rememberRipple()
with a
call to ripple()
from the corresponding library. This API creates a ripple
using values derived from the Material theme APIs. Then, pass the returned
object to Modifier.clickable
and/or other components.
For example, the following snippet uses the deprecated APIs:
Box( Modifier.clickable( onClick = {}, interactionSource = remember { MutableInteractionSource() }, indication = rememberRipple() ) ) { // ... }
You should modify the above snippet to:
@Composable private fun RippleExample() { Box( Modifier.clickable( onClick = {}, interactionSource = remember { MutableInteractionSource() }, indication = ripple() ) ) { // ... } }
Note that ripple()
is no longer a composable function and does not need to be
remembered. It can also be reused across multiple components, similar to
modifiers, so consider extracting the ripple creation to a top-level value to
save allocations.
Implementing custom design system
If you're implementing your own design system, and you were previously using
rememberRipple()
along with a custom RippleTheme
to configure the ripple,
you should instead provide your own ripple API that delegates to the ripple node
APIs exposed in material-ripple
. Then, your components can use your own ripple
that consumes your theme values directly. For more information, see Migrate
fromRippleTheme
.
Migrate from RippleTheme
Temporarily opt out of behavior change
Material libraries have a temporary CompositionLocal
,
LocalUseFallbackRippleImplementation
, which you can use to configure all
Material components to fall back to using rememberRipple
. This way,
rememberRipple
continues to query LocalRippleTheme
.
The following code snippet demonstrates how to use the
LocalUseFallbackRippleImplementation CompositionLocal
API:
CompositionLocalProvider(LocalUseFallbackRippleImplementation provides true) { MaterialTheme { App() } }
If you're using a custom app theme that is built on top of Material, you can safely provide the composition local as part of your app’s theme:
@OptIn(ExperimentalMaterialApi::class) @Composable fun MyAppTheme(content: @Composable () -> Unit) { CompositionLocalProvider(LocalUseFallbackRippleImplementation provides true) { MaterialTheme(content = content) } }
For more information, see the Upgrade Material library version without migrating section.
Using RippleTheme
to disable a ripple for a given component
The material
and material3
libraries expose RippleConfiguration
and
LocalRippleConfiguration
, which allow you to configure the appearance of
ripples within a subtree. Note that RippleConfiguration
and
LocalRippleConfiguration
are experimental, and only intended for per-component
customization. Global/theme-wide customization is not supported with these
APIs; see Using RippleTheme
to globally change all ripples in an
application for more information on that use case.
For example, the following snippet uses the deprecated APIs:
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 { // ... } }
You should modify the above snippet to:
CompositionLocalProvider(LocalRippleConfiguration provides null) { Button { // ... } }
Using RippleTheme
to change the color/alpha of a ripple for a given component
As described in the previous section, RippleConfiguration
and
LocalRippleConfiguration
are experimental APIs and are only intended for
per-component customization.
For example, the following snippet uses the deprecated APIs:
private object DisabledRippleThemeColorAndAlpha : RippleTheme { @Composable override fun defaultColor(): Color = Color.Red @Composable override fun rippleAlpha(): RippleAlpha = MyRippleAlpha } // ... CompositionLocalProvider(LocalRippleTheme provides DisabledRippleThemeColorAndAlpha) { Button { // ... } }
You should modify the above snippet to:
@OptIn(ExperimentalMaterialApi::class) private val MyRippleConfiguration = RippleConfiguration(color = Color.Red, rippleAlpha = MyRippleAlpha) // ... CompositionLocalProvider(LocalRippleConfiguration provides MyRippleConfiguration) { Button { // ... } }
Using RippleTheme
to globally change all ripples in an application
Previously, you could use LocalRippleTheme
to define ripple behavior at a
theme-wide level. This was essentially an integration point between custom
design system composition locals and ripple. Instead of exposing a generic
theming primitive, material-ripple
now exposes a createRippleModifierNode()
function. This function allows for design system libraries to create higher
order wrapper
implementation, that query their theme values and then delegate
the ripple implementation to the node created by this function.
This allows for design systems to directly query what they need, and expose any
required user-configurable theming layers on top without having to conform to
what is provided at the material-ripple
layer. This change also makes more
explicit what theme/specification the ripple is conforming to, as it is the
ripple API itself that defines that contract, rather than being implicitly
derived from the theme.
For guidance, see the ripple API implementation in Material libraries, and replace the calls to Material composition locals as needed for your own design system.
Migrate from Indication
to IndicationNodeFactory
Passing around Indication
If you are just creating an Indication
to pass around, such as creating a
ripple to pass to Modifier.clickable
or Modifier.indication
, you don't
need to make any changes. IndicationNodeFactory
inherits from Indication
,
so everything will continue to compile and work.
Creating Indication
If you are creating your own Indication
implementation, the migration should
be simple in most cases. For example, consider an Indication
that applies a
scale effect on press:
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() } } }
You can migrate this in two steps:
Migrate
ScaleIndicationInstance
to be aDrawModifierNode
. The API surface forDrawModifierNode
is very similar toIndicationInstance
: it exposes aContentDrawScope#draw()
function that is functionally equivalent toIndicationInstance#drawContent()
. You need to change that function, and then implement thecollectLatest
logic inside the node directly, instead of theIndication
.For example, the following snippet uses the deprecated APIs:
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() } } }
You should modify the above snippet to:
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() } } }
Migrate
ScaleIndication
to implementIndicationNodeFactory
. Because the collection logic is now moved into the node, this is a very simple factory object whose only responsibility is to create a node instance.For example, the following snippet uses the deprecated APIs:
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 } }
You should modify the above snippet to:
object ScaleIndicationNodeFactory : IndicationNodeFactory { override fun create(interactionSource: InteractionSource): DelegatableNode { return ScaleIndicationNode(interactionSource) } override fun hashCode(): Int = -1 override fun equals(other: Any?) = other === this }
Using Indication
to create an IndicationInstance
In most cases, you should use Modifier.indication
to display Indication
for a
component. However, in the rare case that you are manually creating an
IndicationInstance
using rememberUpdatedInstance
, you need to update your
implementation to check if the Indication
is an IndicationNodeFactory
so you
can use a lighter implementation. For example, Modifier.indication
will
internally delegate to the created node if it is an IndicationNodeFactory
. If
not, it will use Modifier.composed
to call rememberUpdatedInstance
.