What You Will Build

A full-screen starfield with 200 twinkling stars drifting horizontally and a shooting star trailing across the sky. The entire scene is rendered on a single Canvas with no individual composables for each star, making it highly performant.

Why This Pattern Matters

Particle systems and ambient background animations add polish to apps. Drawing 200+ moving objects as composables would tank performance, but a single Canvas handles them efficiently. This same pattern applies to snow, rain, confetti, and any ambient particle effect.

Step 1: Star Data Class

data class Star(
    val x: Float,     // normalized 0-1
    val y: Float,     // normalized 0-1
    val size: Float,  // radius in pixels
    val speed: Float  // drift speed multiplier
)

val stars = remember {
    List(200) {
        Star(
            Random.nextFloat(),
            Random.nextFloat(),
            Random.nextFloat() * 3f + 0.5f,
            Random.nextFloat() * 2f + 0.5f
        )
    }
}

Step 2: Animate Time

val infiniteTransition = rememberInfiniteTransition(label = "space")
val time by infiniteTransition.animateFloat(
    0f, 1f,
    infiniteRepeatable(tween(5000, easing = LinearEasing)),
    label = "t"
)

Step 3: Draw Stars and Shooting Star

Canvas(Modifier.fillMaxSize().background(Color(0xFF000011))) {
    // Twinkling stars
    stars.forEach { star ->
        val x = ((star.x + time * star.speed * 0.1f) % 1f) * size.width
        val y = star.y * size.height
        val twinkle = (0.5f + 0.5f * kotlin.math.sin(
            (time * 10f + star.x * 100f).toDouble()
        )).toFloat()
        drawCircle(
            Color.White.copy(alpha = twinkle),
            star.size,
            Offset(x, y)
        )
    }

    // Shooting star with trail
    val shootX = (time * size.width * 2f) % (size.width * 1.5f)
    val shootY = size.height * 0.2f + shootX * 0.3f
    if (shootX < size.width) {
        for (t in 0..20) {
            drawCircle(
                Color.White.copy(alpha = (1f - t / 20f) * 0.8f),
                2f - t * 0.08f,
                Offset(shootX - t * 8f, shootY - t * 2.4f)
            )
        }
    }
}

Tips and Pitfalls

  • Normalized coordinates (0-1) for star positions let you multiply by size.width/height at draw time, making the animation resolution-independent.
  • sin() for twinkling creates a smooth oscillation. The star.x offset staggers the phase so all stars do not twinkle in sync.
  • Modulo wrapping ((x + offset) % 1f) makes stars loop seamlessly from right to left.
  • Shooting star trail: 20 circles with decreasing alpha and size create a gradient tail effect.

Min SDK: 21 | Compose BOM: 2024.01.00+

Get New Tutorials by Email

No spam. Just clear, practical breakdowns you can apply right away.

Enjoy this tutorial?

Get new practical tech tutorials in your inbox.