Black Magic for Off-Screen Rendering in Jetpack Compose (Explained Practically)

This article explains how and why Jetpack Compose UI can be rendered off-screen (for example, into a Bitmap) and what actually works in real projects today.
It avoids fantasy APIs and clearly separates supported approaches from fragile hacks.


The real problem (not the romantic one)

When developers say “off-screen rendering in Compose”, they usually want one of these:

  • Generate a Bitmap of a composable for sharing
  • Create thumbnails / previews
  • Export UI to PDF or printing
  • Apply post-processing effects (blur, filters)
  • Capture UI without showing it

Compose, however, was designed with this assumption:

UI exists to be drawn to a window, not to a file.

That single assumption is why this topic feels like “black magic”.


How Compose actually draws UI

In Jetpack Compose, rendering is a pipeline:

  1. Composition – what UI exists
  2. Layout – how big it is
  3. Draw – emit drawing commands to a Canvas

When UI is on screen:

  • Android creates the canvas
  • Compose draws into it
  • You never touch this process

Off-screen rendering means:

You must provide the canvas yourself
and still trigger all three phases correctly.


What does not work (important)

Let’s eliminate myths early:

❌ “Compose has a renderToBitmap API”
❌ “Just call draw() manually”
❌ “graphicsLayer automatically gives you a bitmap”

None of these are true.

Compose intentionally does not expose a stable off-screen renderer.


What does work reliably today

There are two practical approaches:

1. ComposeView + Bitmap (most reliable)

2. Offscreen buffering for effects (not capture)

This article focuses on #1, because it is:

  • Stable
  • Understandable
  • Used in production

Practical example: Render a composable into a Bitmap

Goal

Render this composable without showing it on screen:

@Composable
fun GreetingCard(name: String) {
    Box(
        modifier = Modifier
            .size(300.dp)
            .background(Color(0xFF6200EE)),
        contentAlignment = Alignment.Center
    ) {
        Text(
            text = "Hello, $name",
            color = Color.White,
            fontSize = 20.sp
        )
    }
}

Step-by-step: The working approach

Step 1: Create a ComposeView manually

ComposeView is still the bridge between Compose and Android’s rendering system.

val composeView = ComposeView(context).apply {
    setContent {
        GreetingCard("Compose")
    }
}

At this point:

  • Nothing is on screen
  • No drawing has happened yet

Step 2: Measure and layout manually

Compose will not draw unless layout has happened.

val width = 300
val height = 300

composeView.measure(
    View.MeasureSpec.makeMeasureSpec(width, View.MeasureSpec.EXACTLY),
    View.MeasureSpec.makeMeasureSpec(height, View.MeasureSpec.EXACTLY)
)

composeView.layout(0, 0, width, height)

This step is non-optional.


Step 3: Draw into a Bitmap

Now we provide our own canvas.

val bitmap = Bitmap.createBitmap(
    width,
    height,
    Bitmap.Config.ARGB_8888
)

val canvas = Canvas(bitmap)
composeView.draw(canvas)

That’s it.

The composable has now rendered off-screen.


Step 4: Use the Bitmap

FileOutputStream(file).use { out ->
    bitmap.compress(Bitmap.CompressFormat.PNG, 100, out)
}

You can now:

  • Share it
  • Save it
  • Print it
  • Process it

Why this works (and why it’s not fake magic)

This approach works because:

  • ComposeView is officially supported
  • Compose already knows how to draw into a Canvas
  • We are not touching internal APIs
  • Android Views already support off-screen drawing

This is boring magic — the good kind.


Where graphicsLayer(offscreen) fits (and where it doesn’t)

Modifier.graphicsLayer {
    compositingStrategy = CompositingStrategy.Offscreen
}

This:

  • Forces Compose to draw into an intermediate buffer
  • Enables blend modes and render effects

It does NOT:

  • Give you a bitmap
  • Capture pixels automatically

It is useful inside off-screen rendering, not as a replacement.


Why calls this “black magic”

Because once you go beyond the example above:

  • You fight lifecycle timing
  • You fight frame clocks
  • You fight performance
  • You fight Compose version changes


This is powerful, but fragile if abused.


When you should use this

✔ Share images
✔ Thumbnails
✔ Printing
✔ Static previews
✔ Export pipelines


When you should NOT

✘ Every frame
✘ Animations
✘ Lists
✘ Large surfaces
✘ Without profiling

jetpackcompose

Top Rated Book In Amazon

Leave a Comment

Your email address will not be published. Required fields are marked *