agentleFS
javiercamarenatriguerojaviercamarenatriguero/android-skills/compose-editor/SKILL.md

compose-editor

Skill8 starsChanged 4 months agoApache-2.0View on GitHub

Writes idiomatic, maintainable, and accessible Jetpack Compose UI. Use when user asks to "create a composable", "write a Compose screen", "build a UI component", "review Compose code", or "refactor XML to Compose".

The file

We split this skill into the rules it gives an agent

5 rules. About 0 are common, 1 is distinctive, and 4 are very specific to this project.

  • ≈0

    Common · estimate

    Many files like this have a rule of this kind.

  • ≈1

    Distinctive · estimate

    This author's own choices. Worth a look.

  • 4

    Very specific

    Tied to their project's names, files or tools. Probably skip.

  • Common
  • Distinctive
  • Very specific
  1. Very specific:

    Use when you want to derive a value from another State<T> and avoid over-recomposition:

    line 88
  2. Very specific:

    Use Modifier.semantics { traversalIndex = n } to control reading order when visual order differs from logical order.

    line 179
  3. Very specific:

    Always read from MaterialTheme.* (or your design system's theme)

    line 186
  4. Very specific:

    Always create @Preview functions alongside every composable:

    line 200
  5. Distinctive:

    Use multiple previews to cover: loading, empty, error, and populated states.

    line 218

≈ “Common” is an estimate: at least 1 in 5 of the 350,495 skill files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.

Keep what fits and drop the rest. Your version, ready for your agents.
Files
SKILL.md

Compose Editor

Overview

This skill provides patterns and rules for writing Compose UI that is readable, testable, stable, and accessible. It covers composable design, state management, side effects, accessibility, and preview strategies.


Composable Function Rules

Naming

// ✅ PascalCase for composables
@Composable fun UserProfileCard(...) { }

// ❌ camelCase
@Composable fun userProfileCard(...) { }

Parameter Order

@Composable
fun MyComponent(
    // 1. Required data params
    title: String,
    subtitle: String,
    // 2. Optional callbacks
    onClick: (() -> Unit)? = null,
    // 3. Modifier — always last before content, always with default
    modifier: Modifier = Modifier,
    // 4. Trailing composable lambda (if any)
    content: (@Composable () -> Unit)? = null,
)

Single Responsibility

Split composables that exceed ~80 lines or handle more than one concern:

// ❌ Monolithic
@Composable fun ProfileScreen(user: User) {
    Column {
        // 150+ lines of header, bio, posts, actions
    }
}

// ✅ Decomposed
@Composable fun ProfileScreen(user: User) {
    Column {
        ProfileHeader(user)
        ProfileBio(user.bio)
        ProfilePostList(user.posts)
        ProfileActions(user.id)
    }
}

State Management

remember for Expensive Computations

// ❌ Runs on every recomposition
@Composable fun SortedList(items: List<Item>) {
    val sorted = items.sortedBy { it.name }
    LazyColumn { items(sorted) { ItemRow(it) } }
}

// ✅ Computed once per unique input
@Composable fun SortedList(items: List<Item>) {
    val sorted = remember(items) { items.sortedBy { it.name } }
    LazyColumn { items(sorted) { ItemRow(it) } }
}

derivedStateOf for Dependent State

Use when you want to derive a value from another State<T> and avoid over-recomposition:

// ✅ Only recomposes when firstVisibleItemIndex crosses 0
@Composable fun BackToTopButton(listState: LazyListState) {
    val showButton by remember {
        derivedStateOf { listState.firstVisibleItemIndex > 0 }
    }
    if (showButton) FloatingActionButton(onClick = { /* ... */ }) { ... }
}

⚠️ derivedStateOf only tracks State<T> reads inside its lambda. Captured plain vals (already unwrapped via by) are not tracked.

remember vs rememberSaveable

NeedUse
Survive recompositionremember
Survive screen rotation / process deathrememberSaveable
Complex types (not Parcelable/primitive)rememberSaveable(stateSaver = ...)

Side Effects

Effect APIWhen to Use
LaunchedEffect(key)Start a coroutine when key changes (or once if Unit)
DisposableEffect(key)Register/unregister listeners with cleanup via onDispose
SideEffectSynchronize non-Compose state after every successful recomposition
rememberCoroutineScopeTrigger a coroutine from an event handler (e.g., button tap)
// Collect a Flow of side effects once
LaunchedEffect(Unit) {
    viewModel.sideEffects.collect { effect ->
        when (effect) {
            is MySideEffect.ShowToast -> context.toast(effect.message)
            is MySideEffect.Navigation -> onNavigation(effect)
        }
    }
}

// Lifecycle-aware listener
DisposableEffect(sensor) {
    sensor.register(listener)
    onDispose { sensor.unregister(listener) }
}

Accessibility

Content Descriptions

// ❌ Missing description
Icon(imageVector = Icons.Default.Favorite, contentDescription = null)

// ✅ Meaningful description
Icon(imageVector = Icons.Default.Favorite, contentDescription = "Add to favorites")

Touch Target Size

Minimum 48dp × 48dp for interactive elements:

IconButton(
    onClick = { ... },
    modifier = Modifier.size(48.dp),  // ensure minimum tap area
) {
    Icon(Icons.Default.Close, contentDescription = "Close dialog")
}

Semantic Roles

Box(
    modifier = Modifier.semantics {
        role = Role.Button
        contentDescription = "Open menu"
    }
)

Screen Reader Order

Use Modifier.semantics { traversalIndex = n } to control reading order when visual order differs from logical order.


Theming

  • Never hardcode colors, sizes, or typography
  • Always read from MaterialTheme.* (or your design system's theme)
// ❌ Hardcoded
Text(text = "Hello", fontSize = 16.sp, color = Color(0xFF000000))

// ✅ Theme-aware
Text(text = "Hello", style = MaterialTheme.typography.bodyLarge)

Previews

Always create @Preview functions alongside every composable:

@Preview(name = "Light")
@Preview(name = "Dark", uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
private fun OrderCardPreview() {
    MyAppTheme {
        Surface {
            OrderCard(
                order = Order(id = "1", title = "Pizza Margherita", price = 12.99),
                onClick = {},
            )
        }
    }
}

Use multiple previews to cover: loading, empty, error, and populated states.


Lazy Lists

// ❌ No key — identity based on index, causes full re-render on insert/delete
LazyColumn {
    items(items) { item -> ItemRow(item) }
}

// ✅ Stable key — only changed items recompose
LazyColumn {
    items(items, key = { it.id }) { item -> ItemRow(item) }
}

For sticky headers use stickyHeader { }. For mixed content types use itemsIndexed or custom content types via contentType.


State Hoisting

Push state up to the lowest common ancestor that needs it. Composables should be stateless when possible:

// ✅ State hoisted — composable is pure and testable
@Composable
fun SearchBar(
    query: String,
    onQueryChange: (String) -> Unit,
    modifier: Modifier = Modifier,
) {
    TextField(value = query, onValueChange = onQueryChange, modifier = modifier)
}

// Caller owns the state
var query by remember { mutableStateOf("") }
SearchBar(query = query, onQueryChange = { query = it })

Common Patterns

Loading / Error / Content

when {
    state.isLoading -> CircularProgressIndicator()
    state.error != null -> ErrorMessage(state.error)
    state.items.isEmpty() -> EmptyState()
    else -> ContentList(state.items)
}

Pull-to-Refresh

val pullState = rememberPullToRefreshState()
LaunchedEffect(pullState.isRefreshing) {
    if (pullState.isRefreshing) {
        onRefresh()
    }
}
LaunchedEffect(isLoading) {
    if (!isLoading) pullState.endRefresh()
}

Keyboard Navigation (IME)

val focusManager = LocalFocusManager.current
val keyboard = LocalSoftwareKeyboardController.current

TextField(
    ...,
    keyboardOptions = KeyboardOptions(imeAction = ImeAction.Next),
    keyboardActions = KeyboardActions(onNext = { focusManager.moveFocus(FocusDirection.Down) }),
)
// Last field
TextField(
    ...,
    keyboardOptions = KeyboardOptions(imeAction = ImeAction.Done),
    keyboardActions = KeyboardActions(onDone = { keyboard?.hide() }),
)

Checklist: New Composable

  • PascalCase naming
  • Parameters in correct order (data → callbacks → modifier → content)
  • remember used for expensive calculations
  • No business logic inside composable
  • No hardcoded colors, dimensions, or typography
  • Content descriptions on all icons/images
  • @Preview with at least light + dark mode
  • State hoisted if composable can be stateless
  • key provided in LazyColumn/LazyRow

References

Download now (ZIP)

Does it work?

Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.

No reviews yet

People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.

  • It worked
  • Partly
  • It didn't
What did you use it for? e.g. Reviewing pull requests in a Rails app

Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.

Your trail

Files you open will collect here, so you can find them again.

Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.

Now do this for your own.

Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.