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".
In this repository14 other files it gives its agents.
- android-modularizationSkill
- android-permissions-editorSkill
- android-unit-test-editorSkill
- compose-navigation3Skill
- compose-performance-auditorSkill
- github-action-editorSkill
- gradle-configurationSkill
- gradle-convention-pluginSkill
- koin-editorSkill
- kotlin-conventionSkill
- kotlin-coroutinesSkill
- mvi-editorSkill
- paparazzi-editorSkill
- review-owasp-securitySkill
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
- Very specific: line 88
Use when you want to derive a value from another State<T> and avoid over-recomposition:
- Very specific: line 179
Use Modifier.semantics { traversalIndex = n } to control reading order when visual order differs from logical order.
- Very specific: line 186
Always read from MaterialTheme.* (or your design system's theme)
- Very specific: line 200
Always create @Preview functions alongside every composable:
- Distinctive: line 218
Use multiple previews to cover: loading, empty, error, and populated states.
≈ “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.
Files
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 = { /* ... */ }) { ... }
}⚠️
derivedStateOfonly tracksState<T>reads inside its lambda. Captured plainvals (already unwrapped viaby) are not tracked.
remember vs rememberSaveable
| Need | Use |
| Survive recomposition | remember |
| Survive screen rotation / process death | rememberSaveable |
Complex types (not Parcelable/primitive) | rememberSaveable(stateSaver = ...) |
Side Effects
| Effect API | When to Use |
LaunchedEffect(key) | Start a coroutine when key changes (or once if Unit) |
DisposableEffect(key) | Register/unregister listeners with cleanup via onDispose |
SideEffect | Synchronize non-Compose state after every successful recomposition |
rememberCoroutineScope | Trigger 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)
rememberused for expensive calculations- No business logic inside composable
- No hardcoded colors, dimensions, or typography
- Content descriptions on all icons/images
@Previewwith at least light + dark mode- State hoisted if composable can be stateless
keyprovided inLazyColumn/LazyRow
References
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
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.

