Compose Agent Skill

repository·master·Indexed 19 days ago

https://github.com/aldefy/compose-skill

A specialized plugin for AI coding assistants providing source-accurate knowledge of Jetpack Compose and Compose Multiplatform. It reduces AI hallucinations using 24 reference guides and direct access to AndroidX and Compose Multiplatform source code. Compatible with Claude Code, Copilot CLI, Codex CLI, Gemini CLI, Google Antigravity, Cursor, GitHub Copilot, Windsurf, and Amazon Q Developer.

Tokens
199.3K
Snippets
520
Records
673
Agent score
68%

What's inside compose-skill

  1. How the compose-expert skill works

    master

    The compose-expert skill provides practical, non-opinionated guidance for Jetpack Compose and Compose Multiplatform (CMP) development across Android, Desktop, iOS, and Web.

    It is designed to automatically detect Compose projects at session_start and activates based on specific triggers including:

    • Compose API mentions: Such as @Composable, remember, LaunchedEffect, NavHost, Modifier, etc.
    • Multiplatform keywords: commonMain, expect/actual, ComposeUIViewController, etc.
    • Platform-specific contexts: Android TV (tv-material), Paging 3, or Design Systems.
    • Review Mode: Triggered by GitHub PR URLs or phrases like "review this PR".

    The skill is backed by source code analysis from androidx/androidx and JetBrains/compose-multiplatform-core to provide authoritative implementation details.

  2. What is the Compose Styles API?

    master

    The Compose Styles API (Experimental) is a declarative, state-driven styling system. Instead of manually managing animations and state transitions for every interaction (like animateColorAsState), you declare visual states within a single Style { } block. The framework automatically handles state detection, property interpolation, and animations between those states.

    // Declarative approach
    val style = Style {
        background(Color.Blue)
        shape(RoundedCornerShape(16.dp))
        contentPadding(16.dp)
        pressed(Style {
            animate(Style {
                background(Color.DarkBlue)
                scale(0.95f)
            })
        })
    }
    Box(Modifier.styleable(styleState = styleState, style = style))
  3. How Material 3 motion APIs work together

    master

    Material 3 provides two distinct ways to apply motion, depending on whether your component needs to be theme-aware:

    1. MotionScheme (Preferred): Use this inside components that should adapt to the application's global motion settings. The theme controls whether the motion uses spring-based or tween-based specs. This allows motion to change across the entire app without modifying individual component code.
    2. MotionTokens + tween(): Use this when you need explicit, hard-coded control over tween() or keyframes {} and the component is not intended to be theme-motion-aware.

    Decision Rule: Use MotionScheme for new components. Use MotionTokens when the caller explicitly provides AnimationSpec parameters or when working with AnimatedVisibility, Crossfade, or shared elements.

  4. Use SubcomposeLayout for dynamic content composition

    master

    What is SubcomposeLayout?

    SubcomposeLayout is an analogue of Layout that allows you to subcompose content during the measurement stage. This enables you to use values calculated during measurement (like child sizes or parent constraints) as parameters for the composition of children.

    Common Use Cases

    • Constraint-dependent composition: When you need to know the constraints passed by a parent during composition and cannot solve the problem with a standard Layout or LayoutModifier (e.g., BoxWithConstraints).
    • Inter-child dependency: When the size of one child must be known to compose a second child.
    • Lazy composition: Composing items lazily based on available size (e.g., a list where only visible items are composed).

    Usage

    You can use the simple overload which manages its own state, or provide a SubcomposeLayoutState for more control.

    SubcomposeLayout(
        modifier = Modifier,
        measurePolicy = { constraints ->
            // Perform subcomposition and measurement here
            layout(constraints.maxWidth, constraints.maxHeight) { /* ... */ }
        }
    )
    @Composable
    fun SubcomposeLayout(
        modifier: Modifier = Modifier,
        measurePolicy: SubcomposeMeasureScope.(Constraints) -> MeasureResult,
    ) {
        // ...
    }
  5. How SubcomposeLayout handles measurement and lookahead

    master

    The SubcomposeLayout uses a MeasurePolicy that manages two distinct passes when lookahead is involved:

    1. Approach Pass: If isLookingAhead is true and a lookaheadRoot exists, the approachMeasureScope block is executed first. This pass allows the layout to prepare for upcoming changes.
    2. Lookahead/Main Pass: The main measurement block is executed.

    After the main pass, the layout performs cleanup by disposing of unused slots or reusing them. If the layout is in a lookahead scope, disposal happens after the approach placement to ensure the approach pass can transfer ownership of subcompositions before they are destroyed.

  6. Production State Rules for Jetpack Compose

    master

    Follow these rules to prevent common state-related crashes and architectural issues in production:

    1. ViewModel State: Use StateFlow (via MutableStateFlow) in ViewModels. Never use mutableStateOf in a ViewModel, as it couples the business logic to the Compose runtime.
    2. UI Events: Use SharedFlow with extraBufferCapacity = 1 and onBufferOverflow = BufferOverflow.DROP_OLDEST for UI events. Avoid Channel for events, as they can drop events during lifecycle transitions.
    3. Saved State: Use rememberSaveable only at the screen/NavGraph level. Avoid using it inside individual list items to prevent TransactionTooLargeException due to excessive Bundle size.
    4. Reactive Scrolling: Use snapshotFlow combined with distinctUntilChanged() to monitor scroll positions or other snapshot states. Never poll state in a recomposition loop.
    5. Derived Flows: Use .map() on repository flows and convert them to state using .stateIn() with SharingStarted.WhileSubscribed(5_000) to handle configuration changes gracefully.
    // Rule 1: ViewModel State
    class ProfileViewModel : ViewModel() {
        private val _name = MutableStateFlow("")
        val name: StateFlow<String> = _name.asStateFlow()
    }
    
    // Rule 2: SharedFlow for Events
    class OrderViewModel : ViewModel() {
        private val _events = MutableSharedFlow<UiEvent>(
            extraBufferCapacity = 1,
            onBufferOverflow = BufferOverflow.DROP_OLDEST
        )
        val events = _events.asSharedFlow()
    }
    
    // Rule 4: Reactive Scroll
    LaunchedEffect(listState) {
        snapshotFlow { listState.firstVisibleItemIndex }
            .distinctUntilChanged()
            .collect { index -> /* side effect */ }
    }
    
    // Rule 5: Derived Flows
    val uiState: StateFlow<DashboardUiState> = repository.dashboardData
        .map { data -> DashboardUiState(...) }
        .stateIn(
            scope = viewModelScope,
            started = SharingStarted.WhileSubscribed(5_000),
            initialValue = DashboardUiState()
        )
  7. Manage TextField state with TextFieldValue

    master

    When using the TextField overload that accepts TextFieldValue, you gain control over more than just the text string. TextFieldValue allows you to manage:

    • The actual text content.
    • The current cursor position (selection).
    • The selection range (highlighted text).
    • IME composition state.

    This is essential for advanced text manipulation, such as custom text formatting or programmatic cursor movement.

  8. Configure Paging 3 for Compose Multiplatform (SQLDelight)

    master

    While Room is Android-only, the RemoteMediator mental model applies to Compose Multiplatform (CMP) using SQLDelight.

    In commonMain, use app.cash.sqldelight:androidx-paging3-extensions to provide a PagingSource factory. The logic remains identical: the local DB is the source of truth, RemoteMediator writes to it, and the UI observes the DB-backed PagingSource. Only the DAO and transaction syntax changes:

    • Room: db.itemDao().insertAll(...) / db.withTransaction { ... }
    • SQLDelight: database.itemQueries.insertAll(...) / database.transaction { ... }
  9. Understand Press Interaction Lifecycle in Clickable components

    master

    In Jetpack Compose Foundation, clickable components manage press interactions through a lifecycle of PressInteraction.Press, PressInteraction.Release, and PressInteraction.Cancel.

    • Press: Triggered when a pointer touches the component. Depending on the context (e.g., if inside a scrollable container), the press might be delayed to avoid accidental clicks during scrolling.
    • Release: Triggered when the pointer is lifted successfully. This completes the interaction cycle.
    • Cancel: Triggered when the press is interrupted (e.g., by a scroll gesture or the component being detached). This prevents the interaction from being treated as a successful click.

    These interactions are emitted to an InteractionSource, allowing UI elements to react to press states (like showing ripples or changing colors).

  10. Understand SemanticsPropertyKey and custom properties

    master

    In Jetpack Compose, SemanticsPropertyKey is the infrastructure used to set type-safe key/value pairs within semantics blocks. Each key is associated with a specific type T.

    When using mergeDescendants = true on a semantics node, the merge function is called for each descendant in depth-first-search order. The parent value accumulates the results based on a mergePolicy. By default, the policy returns the parent value if it exists, otherwise it uses the child element (effectively picking the first value found in the subtree).

  11. How nested scrolling and overscroll work in Compose Foundation

    master

    Compose Foundation uses a nested scrolling system to allow multiple scrollable components to coordinate movement. When a scroll or fling occurs, the system uses a NestedScrollScope to manage how much of a delta is consumed by the current node versus how much is passed to parent nodes.

    Key mechanisms include:

    • scroll function: Opens a scrolling session that supports nested scrolling and overscroll. It provides a NestedScrollScope to the block.
    • onPostScroll: A hook in NestedScrollConnection used to handle remaining scroll deltas after a child has consumed what it can.
    • onPostFling: A hook used to handle leftover velocity after a child's fling animation completes. If the child is still flinging, it returns Velocity.Zero; otherwise, it executes the remaining fling animation via doFlingAnimation.
    • FlingCancellationException: An internal exception thrown when a fling hits the bounds of a node or the node leaves composition, allowing the leftover velocity to be passed to a nested scroll node above.
    /** Opens a scrolling session with nested scrolling and overscroll support. */
    suspend fun scroll(
        scrollPriority: MutatePriority = MutatePriority.Default,
        block: suspend NestedScrollScope.() -> Unit,
    ) {
        scrollableState.scroll(scrollPriority) {
            outerStateScope = this
            block.invoke(nestedScrollScope)
        }
    }
  12. Structure Stateless and Stateful Composables

    master

    Use the Stateless + Wrapper pattern to create highly reusable and testable UI components.

    1. Stateless Composable: Accepts raw values and callbacks (e.g., onToggle: (Boolean) -> Unit). It does not manage its own state. This makes it easy to test and reuse in different contexts.
    2. Stateful Wrapper: Manages the state internally using remember and calls the stateless child. This provides a convenient API for simple, isolated use cases.

    Advantage: The caller can choose to manage the state themselves (using the stateless version) or use the convenience wrapper (the stateful version).

    // Stateless: Reusable and testable
    @Composable
    fun ToggleButton(
        isEnabled: Boolean,
        onToggle: (Boolean) -> Unit,
        text: String
    ) { ... }
    
    // Stateful: Manages its own state
    @Composable
    fun StatefulToggleButton(text: String = "Toggle") {
        var isEnabled by remember { mutableStateOf(false) }
        ToggleButton(isEnabled, { isEnabled = it }, text)
    }