Compose Multiplatform: Create Animations Across Platforms

Jetpack Compose has revolutionized UI development on Android, and with Compose Multiplatform, that power extends to other platforms like iOS, desktop, and web. One of the key elements of engaging UIs is animation, and Compose Multiplatform provides robust tools to create visually appealing and performant animations across different platforms.

What are Compose Multiplatform Animations?

Compose Multiplatform Animations involve using Jetpack Compose’s animation APIs in a way that they work seamlessly across different platforms supported by Compose Multiplatform. This means animations defined once can be used on Android, iOS, desktop (JVM), and the web without platform-specific modifications.

Why Use Animations?

  • Enhance User Experience: Animations make the UI feel more interactive and responsive.
  • Provide Visual Feedback: Help users understand state changes and guide them through the UI.
  • Improve App Polish: A well-animated app looks more professional and refined.

Core Concepts of Jetpack Compose Animations

Before diving into Multiplatform animations, let’s review some key Compose animation concepts:

1. AnimatedVisibility

This is used for animating the appearance and disappearance of composables.


import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.material.Button
import androidx.compose.material.Text
import androidx.compose.runtime.*

@Composable
fun VisibilityAnimation() {
    var isVisible by remember { mutableStateOf(false) }

    Button(onClick = { isVisible = !isVisible }) {
        Text(text = "Toggle Visibility")
    }

    AnimatedVisibility(
        visible = isVisible,
        enter = fadeIn(),
        exit = fadeOut()
    ) {
        Text(text = "This text is animated.")
    }
}

2. animate*AsState Functions

Functions like animateFloatAsState, animateColorAsState, etc., are used for smoothly animating property changes.


import androidx.compose.animation.core.*
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.material.Button
import androidx.compose.material.Text
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp

@Composable
fun ColorAnimation() {
    var isRed by remember { mutableStateOf(false) }
    val animatedColor by animateColorAsState(
        targetValue = if (isRed) Color.Red else Color.Blue,
        animationSpec = tween(durationMillis = 500, easing = LinearEasing)
    )

    Button(onClick = { isRed = !isRed }) {
        Text(text = "Toggle Color")
    }

    Box(
        modifier = Modifier
            .size(100.dp)
            .background(animatedColor)
    )
}

3. Transition API

For more complex animations that involve multiple states, the Transition API is highly effective.


import androidx.compose.animation.AnimatedContent
import androidx.compose.animation.ExperimentalAnimationApi
import androidx.compose.animation.SizeTransform
import androidx.compose.animation.core.*
import androidx.compose.animation.slideInVertically
import androidx.compose.animation.slideOutVertically
import androidx.compose.animation.togetherWith
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.material.Button
import androidx.compose.material.Text
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.IntSize
import androidx.compose.ui.unit.dp

enum class BoxState {
    SMALL,
    LARGE
}

@OptIn(ExperimentalAnimationApi::class)
@Composable
fun SizeAnimation() {
    var boxState by remember { mutableStateOf(BoxState.SMALL) }

    val transition = updateTransition(targetState = boxState, label = "boxSizeTransition")

    val size by transition.animateDp(
        transitionSpec = {
            tween(durationMillis = 500)
        }, label = "boxSize"
    ) { state ->
        when (state) {
            BoxState.SMALL -> 50.dp
            BoxState.LARGE -> 150.dp
        }
    }

    val buttonText by remember {
        derivedStateOf {
            if (boxState == BoxState.SMALL) "Make Large" else "Make Small"
        }
    }

    Column(horizontalAlignment = Alignment.CenterHorizontally,
           modifier = Modifier.fillMaxSize()) {
        
        Spacer(Modifier.height(50.dp))

        Button(onClick = {
            boxState = when (boxState) {
                BoxState.SMALL -> BoxState.LARGE
                BoxState.LARGE -> BoxState.SMALL
            }
        }) {
            Text(text = buttonText)
        }

        Spacer(Modifier.height(20.dp))

        androidx.compose.foundation.Box(
            modifier = Modifier
                .size(size)
                .background(androidx.compose.ui.graphics.Color.Green)
        )
    }
}

Implementing Multiplatform Animations

To implement animations that work across multiple platforms, focus on using Compose’s animation APIs in a way that doesn’t rely on platform-specific features.

1. Using Common Code

Put animation logic in your common code module. This ensures that the animation code is shared between all platforms.


// In commonMain
import androidx.compose.animation.core.*
import androidx.compose.runtime.*

enum class AnimationState {
    START,
    END
}

@Composable
fun rememberMultiplatformAnimation(): State {
    val animationState = remember { mutableStateOf(AnimationState.START) }

    val animatedProgress by animateFloatAsState(
        targetValue = if (animationState.value == AnimationState.END) 1f else 0f,
        animationSpec = tween(durationMillis = 1000, easing = LinearEasing),
        label = "multiplatformAnimation"
    )

    LaunchedEffect(Unit) {
        animationState.value = AnimationState.END
    }

    return animatedProgress.collectAsState()
}

2. Platform-Agnostic Properties

Stick to animating properties that are universally supported. Common properties include size, color, alpha, and position.

3. Example Animation

Let’s create a simple fading animation that can be used in a Multiplatform project.


// In commonMain
import androidx.compose.animation.*
import androidx.compose.foundation.layout.*
import androidx.compose.material.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp

@Composable
fun FadingText(isVisible: Boolean) {
    AnimatedVisibility(
        visible = isVisible,
        enter = fadeIn(animationSpec = tween(durationMillis = 1000)),
        exit = fadeOut(animationSpec = tween(durationMillis = 1000))
    ) {
        Text("Hello, Multiplatform!", modifier = Modifier.padding(16.dp))
    }
}

@Composable
fun AnimationExample() {
    var isVisible by remember { mutableStateOf(true) }

    Column(
        modifier = Modifier.fillMaxSize().padding(16.dp),
        horizontalAlignment = Alignment.CenterHorizontally,
        verticalArrangement = Arrangement.Center
    ) {
        Button(onClick = { isVisible = !isVisible }) {
            Text("Toggle Visibility")
        }
        Spacer(modifier = Modifier.height(16.dp))
        FadingText(isVisible = isVisible)
    }
}

4. Platform-Specific Implementations (If Necessary)

Sometimes, minor tweaks may be needed for specific platforms. Use expect/actual declarations to handle platform-specific code if absolutely necessary.


// In commonMain
expect fun platformSpecificAnimation(): @Composable () -> Unit

// In androidMain
actual fun platformSpecificAnimation(): @Composable () -> Unit = {
    Text("Android Specific Animation")
}

// In iosMain
actual fun platformSpecificAnimation(): @Composable () -> Unit = {
    Text("iOS Specific Animation")
}

Example Project Setup

Create a new Compose Multiplatform project using the Kotlin Multiplatform wizard in IntelliJ IDEA.

  • Project Name: ComposeAnimationExample
  • Platforms: Android, iOS, Desktop

Add the shared animation code in the commonMain source set and then use it across the platforms.

Best Practices

  • Keep Animations Simple: Complex animations can be resource-intensive and may not perform well on all platforms.
  • Test on Multiple Platforms: Always test animations on all target platforms to ensure they look and perform as expected.
  • Use Standard Animation APIs: Stick to Compose’s standard animation APIs for maximum compatibility.
  • Optimize for Performance: Use tools like the Compose Profiler to identify and fix performance bottlenecks.

Conclusion

Compose Multiplatform animations offer a powerful way to enhance the user experience across various platforms with a single codebase. By understanding the core animation concepts in Jetpack Compose and adhering to best practices, you can create engaging and performant UIs that delight users on Android, iOS, desktop, and beyond.