When developing apps for multiple platforms, providing a native experience is key to making users feel at home. Android users expect certain behaviors that differ from what iOS users are used to, and vice versa. One of these subtle, but noticeable differences is the ripple animation on button clicks.
In Android, the ripple effect is a well-established part of Material Design, giving users visual feedback on touch. However, on iOS, this animation feels foreign, as it doesn’t align with Apple's Human Interface Guidelines. Luckily, we are developing our cross-platform apps using Compose Multiplatform, and you can customize the behavior of your app to adapt to these platform-specific nuances.
In this post, we’ll walk you through how to conditionally show the ripple animation on Android and hide it on iOS, keeping your app feeling native on both platforms.
Create the Indication
The Indication
represents visual effects that occur when certain interactions happen. This is the ripple effect by default in Material Design (and by extension in Compose Multiplatform). We will define these PlatformClickIndication
and PlatformRippleTheme
variable to be able to override them in each platform later using Kotlins's expect/actual mechanism.
import androidx.compose.foundation.Indication
import androidx.compose.material.ripple.RippleTheme
import androidx.compose.runtime.Composable
@get:Composable
expect val PlatformClickIndication: Indication
@get:Composable
expect val PlatformRippleTheme: RippleTheme
commonMain/extensions/Indication.kt
Implement the Indication for each platform
Now we can implement the Indication
that feels "at home" for each OS. In Android, it's quite straightforward. For iOS, we pretty much remove the ripple effect from happening.
import androidx.compose.foundation.Indication
import androidx.compose.material.ripple.LocalRippleTheme
import androidx.compose.material.ripple.RippleTheme
import androidx.compose.material.ripple.rememberRipple
import androidx.compose.runtime.Composable
actual val PlatformClickIndication: Indication
@Composable
get() = rememberRipple()
actual val PlatformRippleTheme: RippleTheme
@Composable
get() = LocalRippleTheme.current
androidMain/extensions/Indication.android.kt
import androidx.compose.foundation.Indication
import androidx.compose.foundation.IndicationInstance
import androidx.compose.foundation.interaction.InteractionSource
import androidx.compose.material.ripple.RippleAlpha
import androidx.compose.material.ripple.RippleTheme
import androidx.compose.runtime.Composable
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.drawscope.ContentDrawScope
private object NoIndication : Indication {
private object NoIndicationInstance : IndicationInstance {
override fun ContentDrawScope.drawIndication() {
drawContent()
}
}
@Composable
override fun rememberUpdatedInstance(
interactionSource: InteractionSource): IndicationInstance {
return NoIndicationInstance
}
}
actual val PlatformClickIndication: Indication
@Composable
get() = NoIndication
actual val PlatformRippleTheme: RippleTheme
@Composable
get() = object: RippleTheme {
@Composable
override fun defaultColor(): Color = Color.Green
@Composable
override fun rippleAlpha(): RippleAlpha =
RippleAlpha(0f, 0f, 0f, 0f)
}
iosMain/extensions/Indication.ios.kt
Set it in the app-wide theme
The final step is to set these indications to the entire app so as not having to define them manually each time. We can achieve this by using a CompositionLocalProvider
in the MaterialTheme
we are (most probably) using in the app as follows.
MaterialTheme(
colorScheme = colorscheme,
typography = typography,
content = {
CompositionLocalProvider(
LocalIndication provides PlatformClickIndication,
LocalRippleTheme provides PlatformRippleTheme,
content = content
)
}
)
commonMain/Theme.kt
That's it. Now by default the ripple effect will only appear in the Android but not in the iOS build making each app to feel at home.
Happy coding!
Top comments (1)
Good point.