跳到正文
DEV Community· Prem Singh·· 3 小時前AI 評分46

Jetpack Compose AI 回應卡片設計:不只顯示載入指示器

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

AI 導讀

Jetpack Compose 範例示範為 AI 寫作助手設計回應卡片,以閒置、載入、答案和錯誤四種狀態,交代目前狀況及可採取的操作。範例只實作介面,不涉及模型、API 或網絡請求;卡片接收不可變狀態並發出事件,並涵蓋重試、動畫和無障礙測試等設計要點。程式碼片段未有以獨立專案方式編譯或在裝置上測試。

正文

An AI response can take a moment. During that pause, 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.

Start with four explicit states

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

sealed interface AssistantState {
    data object Idle : AssistantState
    data object Loading : 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?
Loading Did my action start?
Answer What did I get back?
Error How can I recover?

Build the card around state and events

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.Loading -> 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.Loading,
                modifier = Modifier.fillMaxWidth(),
            ) {
                Text(
                    when (state) {
                        AssistantState.Loading -> "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.

The details that make it feel finished

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.

Try it in your next Compose experiment

Render the card in all four states before connecting a model. If the loading 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

GitHub · LinkedIn

來源:DEV Community · dev.to