본문으로 건너뛰기
cccskills
무료GitHub에 공개

android-compose-patterns

Use when building native Android with Jetpack Compose - stateless composables, state hoisting, ViewModel-owned state, edge-to-edge, predictive back, adaptive layout, atomic components, and Material theming over hardcoded values

한국어 요약은 준비 중입니다. 원문 설명을 표시하고 있습니다.

설치 방법 보기

포함된 파일(1)

  • SKILL.md7.4 KB

SKILL.md(원문)

설치하기 전에 에이전트에게 전달되는 지시 내용을 확인할 수 있습니다.

Jetpack Compose Patterns

Overview

Native Android (Jetpack Compose) is chosen when a task needs deep Android integration or the app is already native (see native-vs-flutter-decision). Same discipline as Flutter/SwiftUI: small stateless composables, state hoisted out, reusable atomic components.

Core principle: Composables are stateless functions of state. State is hoisted to a ViewModel; composables emit events upward.

Rules

  • Stateless composables + state hoisting. A composable takes its state as parameters and exposes callbacks (onXxx) — it does not own app state. The screen-level composable connects a ViewModel to the stateless UI.
  • ViewModel owns state and logic, exposed as StateFlow/UiState; the composable collects it with collectAsStateWithLifecycle(). Networking/business rules live in the ViewModel + repository, never in the composable.
  • Model UI state as a sealed hierarchy: Loading | Data | Empty | Error — when over it renders every branch. No happy-path-only screens.
  • Atomic components: shared library of reusable composables (atoms → molecules → organisms). Reuse before building; grep before adding.
  • Material theme, not hardcoded values: MaterialTheme.colorScheme / typography / a spacing tokens object (MaterialTheme.spacing, see mobile-design-system-foundation) — never scattered Color(0xFF...) and magic dp. Support dark theme + dynamic color.
  • remember/derivedStateOf to avoid recomputation; hoist remembered state only when it's genuinely local (scroll, text field).
  • @Preview reusable composables in their key states, and across the matrix (see below).

Edge-to-edge (enforced from targetSdk 35)

  • Call enableEdgeToEdge() before setContent in the activity — the opt-out is gone at targetSdk 35+.
  • Consume insets at the Scaffold: use its innerPadding and Modifier.consumeWindowInsets(innerPadding) on the content so a nested scrollable doesn't double-apply it.
  • IME padding: apply Modifier.imePadding() once, at the container that should move with the keyboard — applying it a second time further down the tree produces extra empty space.
  • android:windowSoftInputMode="adjustResize" in the manifest for screens with text input.
  • NavigationSuiteScaffold does not automatically propagate its padding to the content slot — apply insets explicitly inside it, don't assume it's handled.

Predictive back (default at targetSdk 36)

  • onBackPressed() / OnBackPressedCallback without isEnabled toggling is not called the same way once predictive back is the default — use BackHandler (simple) or PredictiveBackHandler (custom transition) in Compose, or Navigation 3's built-in handling.
  • Verify back navigation in this run rather than assuming the old override still fires.

Adaptive layout

  • currentWindowAdaptiveInfo().windowSizeClass to branch layout by available width/height — never by Configuration.orientation or a hardcoded device check.
  • NavigationSuiteScaffold switches a bottom nav bar to a nav rail automatically at the window-size-class breakpoint — prefer it over hand-rolling the switch.
  • GridCells.Adaptive(minWidth) for grids that should reflow by available width.
  • Android 16 (opt-out via PROPERTY_COMPAT_ALLOW_RESTRICTED_RESIZABILITY) and Android 17/API 37 (no opt-out) ignore screenOrientation/resizeableActivity restrictions on screens ≥600dp — design every screen to work in both orientations and at tablet width; don't rely on an orientation lock to avoid building the wider layout.

Previews and tests across the matrix

  • @PreviewScreenSizes, @PreviewFontScale, @PreviewLightDark, or a custom multi-preview with Devices.PHONE/Devices.TABLET — see mobile-visual-self-review for the full size/text-scale/dark matrix.
  • DeviceConfigurationOverride(ForcedSize(...) then FontScale(...) then DarkMode(...)) for instrumented matrix tests.
  • rule.enableAccessibilityChecks() on an API 34+ device/emulator before UI-mutating test actions (inconclusive on Robolectric — say so if that's all you ran); see mobile-accessibility.
  • "Run screenshot tests but do not update reference images" to "fix" a failure — a changed golden is a visual regression until a human confirms it's intended.

Semantics

  • contentDescription on every Image/Icon that carries meaning; null explicitly for a purely decorative one.
  • Modifier.semantics(mergeDescendants = true) on a composite row so a screen reader announces it once.
  • Touch targets ≥48dp — Modifier.size/minimumInteractiveComponentSize().

Worked Example

// ViewModel owns state + logic (unit-testable, no Compose)
class TaskListViewModel(private val repo: TaskRepository) : ViewModel() {
    private val _state = MutableStateFlow<UiState<List<Task>>>(UiState.Loading)
    val state: StateFlow<UiState<List<Task>>> = _state
    fun load() = viewModelScope.launch {
        _state.value = UiState.Loading
        _state.value = try {
            val items = repo.list()
            if (items.isEmpty()) UiState.Empty else UiState.Data(items)
        } catch (e: CancellationException) {
            throw e                              // never swallow cancellation
        } catch (e: Exception) {
            UiState.Error(e)
        }
    }
}

// stateless composable renders state, emits events
@Composable
fun TaskListScreen(state: UiState<List<Task>>, onRetry: () -> Unit) = when (state) {
    is UiState.Loading -> AppSpinner()
    is UiState.Error   -> AppErrorView(message = state.cause.message, onRetry = onRetry)
    is UiState.Data    -> TaskList(tasks = state.value)
    is UiState.Empty   -> EmptyView()
}

The fix from the previous version: runCatching { } wrapped around a suspend call inside viewModelScope.launch catches CancellationException along with real errors, which breaks structured concurrency (a cancelled screen keeps "loading" instead of stopping) — use try/catch and rethrow CancellationException explicitly. The Empty branch is now actually emitted when the list comes back empty, not just declared and never reached.

Testing

  • Unit-test the ViewModel with a fake/mockk repository and a test dispatcher — no Compose.
  • Compose UI tests (createComposeRule) assert visible behavior and clicks. Test-first.

Common Mistakes

  • A composable holding app state or calling the repository.
  • Business logic in the composable instead of the ViewModel.
  • Hardcoded colors/dp instead of MaterialTheme + tokens.
  • Collecting flows without collectAsStateWithLifecycle (leaks/waste).
  • Only the data branch rendered — Empty declared but never emitted.
  • runCatching around a suspend call, swallowing CancellationException.
  • Assuming edge-to-edge/predictive back still behave like targetSdk <35/<36 after a target bump.

Red Flags

  • A @Composable that imports a repository or Retrofit service.
  • Color(0xFF...)/magic dp outside the theme.
  • No error/empty branch in the when.
  • enableEdgeToEdge() missing from a new or recently targetSdk-bumped activity.
  • A screen that only renders correctly in one orientation.

리뷰

아직 리뷰가 없습니다. 사용해 본 소감을 남겨 주세요.

댓글

로그인 없이 쓸 수 있습니다. 사용 후기, 질문, 작동하지 않은 환경 등을 편하게 남겨 주세요.

아직 댓글이 없습니다. 첫 댓글을 남겨 보세요.

보내면 바로 공개됩니다. 이메일 주소나 전화번호 같은 개인 정보는 쓰지 마세요. URL은 2개까지입니다.

0 / 1,000자

이 양식은 reCAPTCHA로 보호되며 Google 개인정보처리방침 및 서비스 약관이 적용됩니다.

같은 저장소의 스킬

Use when writing acceptance criteria for a task - express each as an observable Given/When/Then that QA can execute, including negative cases

한국어 요약은 준비 중입니다. 원문 설명을 표시하고 있습니다.

makifbaysal/tasktrooper1172026년 10월 20일 업데이트

Use when the diff adds or changes an endpoint, resolver, RPC, job or query that takes an object id, a role check, a request binding or a tenant filter - BOLA/IDOR, function-level authorization, mass assignment and tenant scoping

한국어 요약은 준비 중입니다. 원문 설명을 표시하고 있습니다.

makifbaysal/tasktrooper1172026년 10월 20일 업데이트

Use on every UI change - semantic HTML, labels for controls, keyboard-navigable dialogs/menus, visible focus, and never color as the only signal

한국어 요약은 준비 중입니다. 원문 설명을 표시하고 있습니다.

makifbaysal/tasktrooper1172026년 10월 20일 업데이트

Use when a task changes any screen, form, dialog, menu or control - Lighthouse/axe scan of the changed screens, a keyboard walk, and the thresholds that fail a task

한국어 요약은 준비 중입니다. 원문 설명을 표시하고 있습니다.

makifbaysal/tasktrooper1172026년 10월 20일 업데이트

How to work a task returned with review, QA or UAT findings. Use when a task is in need_revision or PR review comments are in your context.

한국어 요약은 준비 중입니다. 원문 설명을 표시하고 있습니다.

makifbaysal/tasktrooper1172026년 10월 20일 업데이트

Use when the repository keeps its alerting in code, or a person asks for alerts outside TaskTrooper - PrometheusRule YAML, a Cloud Monitoring alert policy in Terraform, a Datadog monitor, each routed to TaskTrooper's alert ingest URL so it opens an incident here

한국어 요약은 준비 중입니다. 원문 설명을 표시하고 있습니다.

makifbaysal/tasktrooper1172026년 10월 20일 업데이트

makifbaysal의 스킬 모두 보기

이 스킬의 문제 신고