Skip to main content

android-compose-patterns

Guides expert-level android compose patterns implementation: kotlin and frameworks decision frameworks, production-ready patterns, and concrete templates for android compose patterns workflows. Use when the user asks about android compose patterns, android compose patterns configuration, or mobile best practices for android projects. Do NOT use when the user needs a different mobile development capability -- check sibling skills in the mobile development subcategory.

معلومات المصدر

المستودع
FerroxLabs/murage
آخر نشاط في المصدر
١ سبتمبر ٢٠٢٦ في ١٣:٢٦
لغة SKILL.md المكتشفة
الإنجليزية
النجوم
٩
التفرعات
٢

خيارات التثبيت

يُحدَّد Prompt الذي يراجع المصدر أولًا بشكل افتراضي. يمكنك التبديل إلى أمر مباشر أو تنزيل نسخة محلية.

مراجعة ملفات المصدر

اقرأ SKILL.md وأي ملفات مرافقة يعرضها SkillsMP قبل أن تقرر التثبيت.

مستكشف الملفات
2 ملفات

عرض SKILL.md

SKILL.md
تعليمات المصدر · معاينة للقراءة فقط
name
android-compose-patterns
description
Guides expert-level android compose patterns implementation: kotlin and frameworks decision frameworks, production-ready patterns, and concrete templates for android compose patterns workflows. Use when the user asks about android compose patterns, android compose patterns configuration, or mobile best practices for android projects. Do NOT use when the user needs a different mobile development capability -- check sibling skills in the mobile development subcategory.
license
Apache-2.0
metadata
{"author":"foundry-skills","version":"1.0.0","tags":"mobile kotlin design-patterns","category":"software-engineering","subcategory":"mobile-development","depends":"","disclaimer":"none","difficulty":"intermediate"}
# Android Compose Patterns ## When to Use **Use this skill when:** - User is building a new Android screen or feature using Jetpack Compose and needs guidance on structuring composables, state management, or data flow - User asks how to handle side effects, recomposition control, or state hoisting in Compose - User wants to implement a specific UI pattern (bottom sheets, lazy lists, navigation, theming) using Compose best practices - User is migrating an existing View-based Android screen to Compose and needs an incremental interop strategy - User asks about performance optimization in Compose -- understanding when recomposition happens, how to use `remember`, `derivedStateOf`, or `stable` annotations - User wants to implement a production-grade MVVM or MVI architecture layered with Compose - User asks about testing Compose UI, including semantics trees, `ComposeTestRule`, or screenshot testing **Do NOT use this skill when:** - User is building a Flutter or React Native application -- check the cross-platform mobile skills - User is asking about Android View system (XML layouts, RecyclerView, ViewBinding) without Compose involvement - User needs Kotlin coroutines or Flow fundamentals that are not Compose-specific -- use the Kotlin concurrency skill - User is asking about app distribution, signing, or Play Store submission -- use the Android deployment skill - User needs general Kotlin language patterns not tied to Compose -- use the Kotlin patterns skill - User is working on an iOS application -- check the SwiftUI or UIKit skills - User is asking about backend API design that the Compose app will consume -- use the appropriate API design skill - User needs Android-specific non-UI topics (WorkManager, notifications, sensors) not involving Compose UI -- check the Android platform skill --- ## Process ### 1. Establish the Compose Architecture Tier Before writing a single composable, identify which architectural pattern fits the project: - **MVVM with StateFlow:** Best default for most teams. `ViewModel` exposes `StateFlow<UiState>` or multiple `StateFlow` properties. Composables collect state using `collectAsStateWithLifecycle()` from `androidx.lifecycle:lifecycle-runtime-compose`. Use this when the team is already familiar with Android ViewModel and wants minimal conceptual overhead. - **MVI (Model-View-Intent):** Use when screens have high interaction complexity -- multiple concurrent user actions that can conflict, complex optimistic UI, or undo/redo flows. The ViewModel exposes a single `StateFlow<UiState>` and accepts `UiIntent` sealed classes via a `processIntent(intent: UiIntent)` function. - **Unidirectional Data Flow (UDF) without ViewModel:** Suitable for small, isolated, self-contained composable components (color pickers, animated counters) that are embedded in a larger screen. State is owned by the parent and passed down -- pure state hoisting. - Reject "ViewModel per composable" anti-pattern. ViewModels should correspond to screens or logical feature units, not individual UI widgets. - Confirm early whether `Hilt` is the DI framework (standard for new Android projects). `hiltViewModel()` provides scope-correct ViewModel injection directly in composable functions. ### 2. Define the UiState Contract The `UiState` data class is the central contract between ViewModel and UI: - Model `UiState` as a sealed class or a flat data class with nullable/optional fields depending on complexity. For simple screens, a flat data class is preferable. For screens with fundamentally different modes (loading, content, error), use a sealed class. - Include a `isLoading: Boolean` field for loading overlays rather than a separate state variant when the screen can show content and loading simultaneously (e.g., pull-to-refresh over existing content). - Separate `UiState` (rendered snapshot of the screen) from `UiEffect` (one-shot events like navigation, snackbars, toasts). Effects are delivered via `Channel<UiEffect>` consumed as `Flow` with `consumeAsFlow()`, never via `StateFlow` (which would replay the event on recomposition). - Use `@Immutable` or `@Stable` annotations on `UiState` data classes to inform the Compose compiler that the class is safe to skip recomposition if reference equality holds. Only apply `@Stable` when you can guarantee the contract -- mutable classes annotated `@Stable` cause silent recomposition bugs. - Include `errorMessage: String?` as a nullable field. Map domain exceptions to user-readable strings in the ViewModel, never in composables. ### 3. Design the Composable Hierarchy Structure composables in three tiers to maximize reusability and testability: - **Screen composable** (e.g., `ProfileScreen`): Connects to the ViewModel. Calls `hiltViewModel()` or receives an injected ViewModel. Collects `uiState` and `uiEffects`. Passes data and callbacks down. This composable is NOT unit-testable in isolation -- it requires a full Hilt test component. - **Content composable** (e.g., `ProfileContent`): Stateless, receives `uiState: ProfileUiState` and lambdas for all actions. This IS unit-testable with `ComposeTestRule` because it has no ViewModel dependency. The screen composable delegates to this composable. - **Atomic/reusable composables** (e.g., `AvatarWithBadge`, `EditableTextField`): Pure UI components with no business logic. Accept primitive types or simple data classes. Live in a shared `designsystem` or `ui-components` module. Key design rules for the hierarchy: - Never call `hiltViewModel()` or any DI accessor inside a non-screen composable. - Prefer passing lambdas typed as `() -> Unit` or `(T) -> Unit` over passing the full ViewModel reference down the tree. - Limit composable function parameters to 7 or fewer. If a composable needs more, introduce a state class or split the composable. - Use `@Preview` annotations on the Content composable (not the Screen composable) with `@PreviewParameter` for multiple state variations. ### 4. Apply State Hoisting and `remember` Correctly State hoisting and memoization are the two most commonly misused Compose concepts: - **State hoisting rule:** Lift state to the lowest common ancestor that needs it. If only one composable reads and writes a piece of state, keep it local. If two sibling composables need the same state, hoist to their parent. If the ViewModel needs to act on it, hoist all the way to the ViewModel. - Use `remember { mutableStateOf(...) }` for purely transient UI state: text field focus, dropdown expanded, animation trigger. This state should NOT survive process death and does NOT belong in the ViewModel. - Use `rememberSaveable { mutableStateOf(...) }` for UI state that should survive configuration changes but not business-level persistence. Examples: scroll position in a tab that the user might rotate the device on, whether a filter panel is expanded. - Use `derivedStateOf { }` only when a computed value depends on another observable state and the computation is relatively expensive or the derived state changes less frequently than the source state. A canonical example: `val showScrollToTop by remember { derivedStateOf { listState.firstVisibleItemIndex > 5 } }`. Do NOT wrap every computation in `derivedStateOf` -- it adds overhead for simple transformations. - Use `remember(key1, key2) { ... }` with explicit keys when cached objects must be invalidated on key change. Omitting keys when they are relevant is a subtle bug that produces stale data. - Never store `Context`, `View`, or `Lifecycle` references inside `remember` blocks -- these create memory leaks. Use `LocalContext.current` and `LocalLifecycleOwner.current` as CompositionLocals instead. ### 5. Handle Side Effects with the Correct Effect API Compose provides four primary side-effect APIs -- choosing the wrong one causes bugs ranging from infinite loops to missed events: - **`LaunchedEffect(key)`:** Launch a coroutine tied to composition. Relaunches when `key` changes. Use for: triggering one-shot async work when entering a screen (`LaunchedEffect(Unit)`), responding to state changes that require async work, starting animations keyed to data. - **`SideEffect`:** Runs on every successful recomposition. Use only for synchronizing Compose state to non-Compose objects (e.g., updating an analytics tracker with the current screen name, updating a `SupportActionBar` title). Almost always overused -- most use cases belong in `LaunchedEffect`. - **`DisposableEffect(key)`:** Use when you need a cleanup callback on key change or departure from composition. Canonical use case: registering and unregistering a `BroadcastReceiver`, a `LifecycleObserver`, or a sensor listener within a composable. - **`rememberCoroutineScope()`:** Obtain a scope tied to the composable's lifetime for launching coroutines in response to user events (button clicks). Do NOT use `GlobalScope` or `lifecycleScope` directly inside composables. - Effect key selection: Use `Unit` as the key only when you want the effect to run exactly once per composition entry. Use state variables as keys when the effect should restart when that variable changes. Never use rapidly-changing values (millisecond timestamps, incrementing counters) as keys -- this causes continuous restart loops. ### 6. Optimize Recomposition Uncontrolled recomposition is the most common Compose performance problem in production: - Enable the Layout Inspector's recomposition counter in Android Studio (Flamingo or later) to identify hot composables that recompose unexpectedly. - Identify recomposition scope boundaries. Compose restarts recomposition at the nearest enclosing composable that reads the changed state. Design composables to minimize the scope that reads frequently-changing state. Extract the frequently-updating part into its own composable. - Use `key(id) { ItemComposable(...) }` in `LazyColumn` and `LazyRow` to give Compose stable identity for list items. Without keys, Compose reuses item slots by position, causing incorrect animation and focus behavior when items are inserted or removed. - Mark lambdas passed to child composables with `remember` when they capture frequently-changing state: `val onItemClick = remember(viewModel) { { id: Int -> viewModel.selectItem(id) } }`. Inline lambdas create new instances on every recomposition of the parent, causing all children to recompose even if their data is unchanged. - Apply `@Stable` to domain model classes that flow through the UI layer only when they genuinely satisfy the stability contract: equals returns true when no public property has changed. Kotlin data classes with all-val primitive or `@Immutable` properties are automatically inferred as stable by the Compose compiler. - Use the Compose Compiler Metrics (enable via Gradle flag `freeCompilerArgs += ["-P", "plugin:androidx.compose.compiler.plugins.kotlin:reportsDestination=..."]`) to identify unstable classes and skippable vs. non-skippable composables. Address the top 5 offenders before shipping. - Avoid reading `State` objects in the composition phase when they are only needed in layout or draw phases. Use `Modifier.graphicsLayer { ... }` or `Modifier.drawWithContent { ... }` with lambda-based state reads to confine state reads to the draw phase, bypassing recomposition entirely for animation-driven properties. ### 7. Implement Navigation with Type Safety Compose Navigation requires deliberate structure to remain maintainable at scale: - Use `androidx.navigation:navigation-compose` as the primary navigation library. For type-safe routes, use `androidx.navigation:navigation-compose` version 2.8.0+ which supports Kotlin Serializable objects as route types, replacing the fragile string-based route approach. - Define a sealed class or object hierarchy for routes in a dedicated `navigation` package. Each route object is annotated with `@Serializable`. Route objects carry only primitive, serializable arguments -- never pass complex objects through navigation arguments. - Structure the `NavGraph` using nested graphs for feature isolation. Each feature module owns its own `NavGraphBuilder` extension function (e.g., `fun NavGraphBuilder.profileGraph(navController: NavController)`). The app module assembles these in the root `NavHost`. - Pass `NavController` only to screen-level composables. Never pass `NavController` into reusable UI components. Instead, pass a `() -> Unit` lambda named after the navigation action (e.g., `onNavigateToProfile: () -> Unit`). - Handle deep links by registering `deepLinks = listOf(navDeepLink { uriPattern = "..." })` at the route level. Test deep links with `adb shell am start -a android.intent.action.VIEW -d "yourscheme://..."`. - Use `SavedStateHandle` in the ViewModel to retrieve navigation arguments -- this makes the ViewModel independently testable without a NavController dependency. ### 8. Apply Theming and Design System Patterns - Use `MaterialTheme` as the foundation. Extend it using `CompositionLocalProvider` with custom `CompositionLocal` values for brand-specific tokens not covered by Material (e.g., custom spacing scales, elevation ramps, motion tokens). - Define a `AppTheme` composable wrapping `MaterialTheme` that maps your brand's color palette to `ColorScheme`. Use `dynamicColorScheme()` on Android 12+ with a fallback to the static brand scheme for earlier API levels. - Create a `Spacing` object with named properties (`xs = 4.dp`, `sm = 8.dp`, `md = 16.dp`, `lg = 24.dp`, `xl = 32.dp`, `xxl = 48.dp`) and expose it via `val LocalSpacing = staticCompositionLocalOf { Spacing() }`. Access via `MaterialTheme.spacing.md` by adding an extension property on `MaterialTheme`. - Never hardcode `dp`, `sp`, or `Color` values in individual composables. Every visual value must trace to a design token. - Use `TextStyle` from `MaterialTheme.typography` for all text rendering. Define a full `Typography` object using the M3 type scale (Display, Headline, Title, Body, Label at Large/Medium/Small variants). --- ## Output Format When responding to a user request about Compose patterns, structure output as follows: ``` ## Compose Pattern: [Pattern Name] ### Context & Applicability - Applies when: [specific scenario] - Avoid when: [specific counter-scenario] - Complexity: [Low / Medium / High] ### UiState Definition ```kotlin // Annotate for Compose compiler stability inference @Immutable data class [Screen]UiState( val isLoading: Boolean = false, val [dataField]: [Type] = [default], val errorMessage: String? = null ) sealed class [Screen]UiEffect { data class NavigateTo(val route: [RouteType]) : [Screen]UiEffect() data class ShowSnackbar(val message: String) : [Screen]UiEffect() } ``` ### ViewModel Structure ```kotlin @HiltViewModel class [Screen]ViewModel @Inject constructor( private val [dependency]: [DependencyType], savedStateHandle: SavedStateHandle ) : ViewModel() { private val _uiState = MutableStateFlow([Screen]UiState()) val uiState: StateFlow<[Screen]UiState> = _uiState.asStateFlow() private val _uiEffect = Channel<[Screen]UiEffect>(Channel.BUFFERED) val uiEffect: Flow<[Screen]UiEffect> = _uiEffect.receiveAsFlow() fun onEvent(event: [Screen]Event) { /* ... */ } } ``` ### Screen Composable ```kotlin @Composable fun [Screen]Screen( viewModel: [Screen]ViewModel = hiltViewModel(), onNavigateBack: () -> Unit ) { val uiState by viewModel.uiState.collectAsStateWithLifecycle() val context = LocalContext.current LaunchedEffect(Unit) { viewModel.uiEffect.collect { effect -> when (effect) { is [Screen]UiEffect.NavigateTo -> { /* handle */ } is [Screen]UiEffect.ShowSnackbar -> { /* handle */ } } } } [Screen]Content( uiState = uiState, onEvent = viewModel::onEvent ) } ``` ### Content Composable (Testable) ```kotlin @Composable fun [Screen]Content( uiState: [Screen]UiState, onEvent: ([Screen]Event) -> Unit, modifier: Modifier = Modifier ) { // Pure UI -- no ViewModel, no DI } ``` ### Recomposition Risk Assessment | Composable | State Read Frequency | Optimization Applied | |------------|---------------------|----------------------| | [Name] | [High/Medium/Low] | [technique] | ### Test Coverage Targets | Layer | Test Type | Tool | |-------|-----------|------| | ViewModel | Unit | JUnit5 + Turbine | | Content composable | Compose UI test | ComposeTestRule | | Screen composable | Integration | Hilt test + ComposeTestRule | | Navigation flow | E2E | UiAutomator / Espresso | ### Known Trade-offs - [trade-off description and mitigation] ``` --- ## Rules 1. **Never hoist state higher than necessary.** Hoisting `TextField` value into the ViewModel when only one composable uses it adds unnecessary ViewModel complexity and causes every ViewModel state observer to recompose on each keystroke. Keep transient UI state local. 2. **Never pass lambdas to deeply nested composables without `remember` stabilization.** A non-remembered lambda captured from a recomposing scope creates a new instance on every parent recomposition. Use `remember(viewModel) { { param -> viewModel.action(param) } }` or reference bound method references directly. 3. **Never use `@Stable` or `@Immutable` on classes you do not control.** Annotating third-party or domain model classes from outside your module lies to the Compose compiler and produces hard-to-diagnose stale UI bugs. Instead, create dedicated UI model classes in the `ui` layer. 4. **Always use `collectAsStateWithLifecycle()` instead of `collectAsState()` for StateFlow in screen composables.** `collectAsState()` continues collecting when the app is backgrounded, wasting CPU and battery. `collectAsStateWithLifecycle()` from `lifecycle-runtime-compose` automatically pauses collection when the lifecycle drops below `STARTED`. 5. **Never deliver one-shot events (navigation, toasts, dialogs) via `StateFlow`.** `StateFlow` replays the last value on new collectors, causing navigation events to re-fire after configuration changes. Use `Channel(Channel.BUFFERED).receiveAsFlow()` for events that should be consumed exactly once. 6. **Never call `remember` without keys when the remembered object depends on a parameter.** `remember { expensiveComputation(userId) }` will return a stale result when `userId` changes because the key is omitted. Always write `remember(userId) { expensiveComputation(userId) }`. 7. **Never read `State` inside `Modifier.padding()`, `Modifier.size()`, or layout modifiers when the value changes at high frequency (e.g., driven by animation or gesture).** Layout-phase state reads trigger relayout on every change. Move such reads to `Modifier.graphicsLayer { }` or `Modifier.offset { }` (the lambda variant) which execute in the draw phase without triggering recomposition or relayout. 8. **Always provide `contentDescription` for interactive composable elements.** Any `Image`, `Icon`, `Button`, or custom clickable composable must have a non-null `contentDescription` for accessibility. Use `contentDescription = null` only for purely decorative elements -- document the reason in a comment. 9. **Never use `GlobalScope`, `lifecycleScope`, or `MainScope` inside composable functions.** Use `rememberCoroutineScope()` for event-driven coroutines inside composables, or launch from the ViewModel using `viewModelScope`. External scopes leak the composable or attach to the wrong lifecycle. 10. **Always run Compose Compiler Metrics on CI before releasing a new screen.** The metrics report identifies which composables are skippable, which parameters are unstable, and which classes trigger unnecessary recomposition. Set a threshold: no screen composable tree should have more than 20% non-skippable composables without documented justification. --- ## Edge Cases ### Migrating an Existing Fragment to Compose Incrementally When adding Compose to a screen backed by a `Fragment`, use `ComposeView` inside the Fragment's `onCreateView`. Set the `ViewCompositionStrategy` to `ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed` to align the Compose lifecycle with the Fragment view lifecycle (not the Fragment lifecycle itself -- using the wrong strategy causes double-subscription bugs with `collectAsStateWithLifecycle`). The Fragment ViewModel is shared using `viewModels()` and passed into the `ComposeView` content lambda. Do not create a second ViewModel for the Compose content -- maintain a single source of truth. ### LazyColumn with Heterogeneous Item Types When `LazyColumn` renders multiple item types (headers, regular items, ads, loading indicators), define a sealed class for list item types and use `LazyListScope.items(items, key = { it.stableId }, contentType = { it.contentType })`. The `contentType` parameter allows Compose to recycle composition nodes across items of the same type, significantly improving scroll performance. Without `contentType`, Compose treats every item slot as potentially different and performs full recomposition on scroll. Expected performance improvement: 20-40% reduction in frame time for heterogeneous lists longer than 50 items. ### State Restoration After Process Death
عرض على GitHub
ملف SKILL.md هذا كبير جدا، لذلك يعرض SkillsMP القسم الاول فقط هنا. عرض على GitHub