cd /news/developer-tools/your-ai-feature-deserves-better-than… · home › topics › developer-tools › article
[ARTICLE · art-152222] src=dev.to ↗ pub= topic=developer-tools verified=true sentiment=· neutral

Your AI Feature Deserves Better Than a Spinner: A Jetpack Compose Response Card

A developer published a Jetpack Compose implementation example for an Android writing-assistant response card that models AI output states with a sealed interface (Idle, Loading, Answer, Error) instead of independent booleans, driving an AnimatedContent block and a primary button that disables during loading. The snippet uses Compose Foundation, Animation, and Material 3, and the author notes it is illustrative UI code with no model, API key, or network request involved.

by read4 min views1 publishedOct 18, 2026

An AI response can take a moment. During that , the interface has a job: explain what is happening, preserve context, and give the user a way forward.

Let's design a compact response card for an Android writing assistant. The visual direction is simple: a rounded Material surface, a clear heading, generous spacing, and one primary action. The interesting part is how the card behaves when the answer isn't ready.

This is a UI implementation example, not an AI integration. The response text is supplied by the caller. No model, API key, or network request is involved.

Avoid a collection of independent booleans such as is, hasError, and hasResult. Those can accidentally describe contradictory combinations.

sealed interface AssistantState {
    data object Idle : AssistantState
    data object  : AssistantState
    data class Answer(val text: String) : AssistantState
    data object Error : AssistantState
}

Each state should answer a user question:

State What the interface explains
Idle What can I do here?
Did my action start?
Answer What did I get back?
Error How can I recover?

The following snippet uses Compose Foundation, Animation, and Material 3 inside an existing Compose app. Include the standard imports from those libraries. It is an illustrative snippet; it has not been compiled or device-tested as a standalone project.

@Composable
fun AssistantCard(
    state: AssistantState,
    onGenerate: () -> Unit,
    modifier: Modifier = Modifier,
) {
    val colors = MaterialTheme.colorScheme

    Surface(
        modifier = modifier.fillMaxWidth(),
        shape = RoundedCornerShape(28.dp),
        color = colors.surfaceContainer,
        tonalElevation = 2.dp,
    ) {
        Column(
            modifier = Modifier.padding(24.dp),
            verticalArrangement = Arrangement.spacedBy(16.dp),
        ) {
            Text(
                text = "Writing assistant",
                style = MaterialTheme.typography.titleLarge,
                color = colors.onSurface,
            )
            Text(
                text = "Turn a rough note into a clearer draft.",
                style = MaterialTheme.typography.bodyMedium,
                color = colors.onSurfaceVariant,
            )

            AnimatedContent(
                targetState = state,
                label = "assistant_content",
                modifier = Modifier.fillMaxWidth(),
            ) { current ->
                when (current) {
                    AssistantState.Idle -> Text(
                        "Ready when you are.",
                        color = colors.onSurface,
                    )
                    AssistantState. -> Row(
                        horizontalArrangement = Arrangement.spacedBy(12.dp),
                        verticalAlignment = Alignment.CenterVertically,
                    ) {
                        CircularProgressIndicator(
                            modifier = Modifier.size(24.dp),
                            strokeWidth = 2.dp,
                        )
                        Text("Preparing your draft…", color = colors.onSurface)
                    }
                    is AssistantState.Answer -> Text(
                        text = current.text,
                        style = MaterialTheme.typography.bodyLarge,
                        color = colors.onSurface,
                    )
                    AssistantState.Error -> Text(
                        text = "Couldn't prepare a draft. Please try again.",
                        color = colors.error,
                    )
                }
            }

            Button(
                onClick = onGenerate,
                enabled = state != AssistantState.,
                modifier = Modifier.fillMaxWidth(),
            ) {
                Text(
                    when (state) {
                        AssistantState. -> "Preparing…"
                        AssistantState.Error -> "Try again"
                        is AssistantState.Answer -> "Generate another draft"
                        AssistantState.Idle -> "Generate draft"
                    }
                )
            }
        }
    }
}

The card receives immutable state and emits an event. It doesn't own the request. This makes it easier to preview each state and connect the same interface to a fake response or a real service. Choose the state owner based on the logic involved; business logic typically belongs in a screen-level state holder such as a ViewModel. See the Android state-hoisting guidance.

AnimatedContent uses its lambda parameter (current) to render the transitioning state. Keep that detail: reading the outer state instead can break the intended transition behavior. The official animation documentation explains the API.

Use motion to explain change. A gentle content transition is enough here. Constant shimmer or a dramatic glow can compete with the answer. For streamed responses, keep the answer container stable rather than restarting a transition for every token.

Be honest about progress. This example shows indeterminate progress. Display a percentage only when the underlying work can actually measure it.

Design the recovery path. An error message without an action is a dead end. Preserve the original input outside this card so retry doesn't require typing it again. A real request owner should also reject duplicate requests and handle cancellation.

Check more than the screenshot. Test large font sizes, dark mode, long answers, and TalkBack. This sample leaves announcements to the surrounding screen; consider a concise polite live-region announcement when a result becomes available. Avoid announcing every streamed token. Read the Compose semantics guidance.

Keep the result identifiable. Label AI-generated output and let the user review it before using it elsewhere. A polished surface should never imply the answer has been verified.

Render the card in all four states before connecting a model. If the and error views feel like afterthoughts, the feature isn't finished yet.

What would you add next: cancellation, editable output, or a copy action?

Prem Singh · Lead Android Engineer · Kotlin, Jetpack Compose, and practical Android engineering

── more in #developer-tools 4 stories · sorted by recency
── more on @jetpack compose 3 stories trending now
sponsored brought to you by zahid.host 4,200+ EU-deployed projects
reading about agents? ship yours in a single git push.

Run your AI side-project on zahid.host

EU-based hosting, git-push deploys, automatic HTTPS, no cold starts. Free tier with a custom domain — perfect for shipping the agent you just read about.

$git push zahid main
→ Live at https://your-agent.zahid.host ✓
Get free account → Pricing
from €0/mo · no card required
LIVE [news/your-ai-feature-dese…] indexed:0 read:4min 2026-10-18 · —