jetpack-compose — Cursor rule: what it does, how to install
A Cursor rule for Jetpack Compose UI structure, modifier handling, and state flow. Use it when generating or reviewing Android composables.
.cursor/rules/jetpack-compose.mdcWhat jetpack-compose does
This rule gives Cursor concrete conventions for Jetpack Compose code. Composable functions should use PascalCase, take modifier: Modifier = Modifier first, and pass it to the root UI element. Layout choices such as size and padding stay with the caller, while each composable keeps a single clear responsibility.
The guidance also covers state hoisting, callback-based events, separate modifier instances, and breaking large screens into smaller composables. A composable should emit UI or return a value, not do both. Because its glob is **/*, the rule applies across the project, including files outside Compose UI.
What it helps with
- organize composables around single responsibilities
- keep sizing and padding under caller control
- hoist state to the lowest shared owner
- split large screens into focused UI components
Example prompts for the agent
- Review the composables in this screen using the
jetpack-composerule. - Refactor this component so state comes through parameters and changes use callbacks.
- Fix the parameter order and
Modifierusage inProfileScreen.ktaccording to the rule.
How to install jetpack-compose
Manually: put the files in place
.cursor/rules/jetpack-compose.mdcFrontmatter: description, globs, alwaysApply
Manually: put the files in place
.claude/rules/jetpack-compose.mdThe paths frontmatter field limits the files: move globs from the source into paths
AGENTS.md
Codex reads AGENTS.md: paste the rule text there
Manually: put the files in place
.github/instructions/jetpack-compose.instructions.mdThe applyTo field sets which files it applies to: move globs from the source into applyTo
Manually: put the files in place
.devin/rules/jetpack-compose.mdManually: put the files in place
.clinerules/jetpack-compose.mdStep by step, with commands for each platform: Cursor rules: .mdc files, four modes and what replaced .cursorrules
Questions about jetpack-compose
How do I install jetpack-compose in Cursor?
Place the rule file at .cursor/rules/jetpack-compose.mdc.
What does the jetpack-compose rule do?
It guides Cursor on composable structure, parameter order, modifier usage, and state organization in Jetpack Compose.
Which files does the jetpack-compose rule cover?
Its **/* glob applies the rule to every file in the project.
Source text
jetpack-compose.mdc · CC0-1.0
---
description: Definitive guidelines for writing clean, performant, and maintainable Jetpack Compose UI code, emphasizing best practices for component design, state management, and modifier usage.
globs: **/*
---
# Jetpack Compose Best Practices
Jetpack Compose is the future of Android UI. Adhering to these guidelines ensures your composables are readable, maintainable, performant, and scalable. This is your definitive guide for building high-quality Compose applications.
## 1. Code Organization & Naming
### 1.1 Name Composables in PascalCase
Composable functions that emit UI **must** start with an uppercase letter (PascalCase). This clearly distinguishes them from regular Kotlin functions.
❌ **BAD**
```kotlin
@Composable
fun myButton(text: String) { /* ... */ }
```
✅ **GOOD**
```kotlin
@Composable
fun MyButton(text: String) { /* ... */ }
```
### 1.2 Standardize Parameter Order
Always order parameters consistently. This improves readability and makes composable APIs predictable.
1. `modifier: Modifier = Modifier`
2. Required data parameters
3. Optional UI-related styling parameters (e.g., `color`, `shape`, `style`)
4. Callback lambdas (`onClick`, `onValueChange`)
5. Trailing `@Composable` content lambdas
❌ **BAD**
```kotlin
@Composable
fun UserProfile(
onClick: () -> Unit,
user: User,
modifier: Modifier = Modifier,
backgroundColor: Color = MaterialTheme.colors.surface
) { /* ... */ }
```
✅ **GOOD**
```kotlin
@Composable
fun UserProfile(
user: User,
modifier: Modifier = Modifier,
backgroundColor: Color = MaterialTheme.colors.surface,
onClick: () -> Unit
) { /* ... */ }
```
### 1.3 Single Responsibility: Emit UI *or* Return a Value
A composable function **must** either emit UI content into the composition or return a value, but never both. If a composable needs to expose control surfaces, provide them as parameters.
❌ **BAD**
```kotlin
@Composable
fun MyTextWithId(text: String): String {
Text(text)
return "text_id_123" // Emits UI AND returns a value
}
```
✅ **GOOD**
```kotlin
@Composable
fun MyText(text: String) {
Text(text)
}
fun generateTextId(text: String): String {
return "text_id_${text.hashCode()}" // Pure function, no UI emission
}
```
### 1.4 Emit Single Layout Node
A composable function **must** emit either 0 or 1 piece of layout. Avoid emitting multiple, disconnected layout nodes. This ensures composables are cohesive and behave predictably.
❌ **BAD**
```kotlin
@Composable
fun TwoTexts(text1: String, text2: String) {
Text(text1)
Text(text2) // Emits two root layout nodes
}
```
✅ **GOOD**
```kotlin
@Composable
fun TwoTexts(text1: String, text2: String) {
Column { // Single root layout node
Text(text1)
Text(text2)
}
}
```
## 2. Modifiers: The Contract
### 2.1 Accept and Respect a Single Modifier
Every UI-emitting composable **must** accept a `modifier: Modifier = Modifier` as its first parameter. This parameter **must** be passed to the composable's root UI-emitting child.
❌ **BAD**
```kotlin
@Composable
fun MyCard(content: @Composable () -> Unit) {
Card { // No modifier parameter accepted
content()
}
}
```
✅ **GOOD**
```kotlin
@Composable
fun MyCard(modifier: Modifier = Modifier, content: @Composable () -> Unit) {
Card(modifier = modifier) { // Modifier passed to the root
content()
}
}
```
### 2.2 Avoid Internal Layout Modifiers
Composables **must not** apply layout-controlling modifiers (e.g., `padding()`, `fillMaxWidth()`, `width()`, `height()`, `size()`) to their root component internally. These **must** be provided by the caller via the `modifier` parameter to ensure reusability and flexibility.
❌ **BAD**
```kotlin
@Composable
fun ProfileImage(imageUrl: String) {
Image(
painter = rememberImagePainter(imageUrl),
contentDescription = "Profile picture",
modifier = Modifier.size(48.dp).padding(8.dp) // Layout modifiers applied internally
)
}
```
✅ **GOOD**
```kotlin
@Composable
fun ProfileImage(imageUrl: String, modifier: Modifier = Modifier) {
Image(
painter = rememberImagePainter(imageUrl),
contentDescription = "Profile picture",
modifier = modifier // Caller controls size and padding
)
}
// Usage:
ProfileImage(imageUrl = "...", modifier = Modifier.size(48.dp).padding(8.dp))
```
### 2.3 Do Not Reuse Modifier Instances
Each composable **must** receive its own `Modifier` instance. Reusing the same `Modifier` object across multiple composables or within a composable's internal hierarchy can lead to unpredictable behavior and unintended side effects.
❌ **BAD**
```kotlin
@Composable
fun ReusedModifierExample(modifier: Modifier = Modifier) {
Column(modifier = modifier) {
Text("Hello", modifier = modifier.padding(8.dp)) // Modifier reused
Text("World", modifier = modifier.padding(8.dp)) // Modifier reused
}
}
```
✅ **GOOD**
```kotlin
@Composable
fun ReusedModifierExample(modifier: Modifier = Modifier) {
Column(modifier = modifier) {
Text("Hello", modifier = Modifier.padding(8.dp)) // New modifier instance
Text("World", modifier = Modifier.padding(8.dp)) // New modifier instance
}
}
```
## 3. State Management & Architecture
### 3.1 Prefer Stateless & Controlled Composables
Design composables to be stateless and controlled. They **must** accept external state as parameters and expose events (callbacks) for state changes. This enables unidirectional data flow and simplifies testing.
❌ **BAD**
```kotlin
@Composable
fun MyToggle() {
var isChecked by remember { mutableStateOf(false) } // State owned internally
Switch(checked = isChecked, onCheckedChange = { isChecked = it })
}
```
✅ **GOOD**
```kotlin
@Composable
fun MyToggle(
isChecked: Boolean, // State hoisted
onCheckedChange: (Boolean) -> Unit,
modifier: Modifier = Modifier
) {
Switch(checked = isChecked, onCheckedChange = onCheckedChange, modifier = modifier)
}
```
### 3.2 Hoist State Upwards
State **must** be hoisted to the lowest common ancestor that needs to read or modify it. This keeps most composables stateless, improving reusability and testability.
❌ **BAD**
```kotlin
@Composable
fun MyScreen(viewModel: MyViewModel) { // Passing ViewModel down
val uiState = viewModel.uiState.collectAsState()
MyContent(uiState.value.data, viewModel::onEvent) // Passing ViewModel functions
}
```
✅ **GOOD**
```kotlin
@Composable
fun MyScreen(
uiState: MyUiState, // Pass immutable data
onAction: (MyAction) -> Unit // Pass specific event callbacks
) {
MyContent(
data = uiState.data,
onItemClick = { item -> onAction(MyAction.ItemClicked(item)) }
)
}
```
### 3.3 Build Small, Single-Purpose Composables
Decompose your UI into small, focused composables. Start with low-level building blocks and compose them into higher-level sections. This promotes reusability and maintainability.
❌ **BAD**
```kotlin
@Composable
fun BigComplexScreen() {
Column {
// All UI logic for header, list, footer in one composable
Header()
LazyColumn { /* ... */ }
Footer()
}
}
```
✅ **GOOD**
```kotlin
// Low-level building blocks
@Composable fun AppIcon(painter: Painter, contentDescription: String?, modifier: Modifier = Modifier) { /* ... */ }
@Composable fun BodyText(text: String, modifier: Modifier = Modifier) { /* ... */ }
// Higher-level component
@Composable
fun IconTextButton(
title: String,
icon: Painter,
modifier: Modifier = Modifier,
onClick: () -> Unit
) {
Surface(modifier = modifier.clickable(onClick = onClick)) {
Row(verticalAlignment = Alignment.CenterVertically) {
AppIcon(painter = icon, contentDescription = null)
Spacer(Modifier.width(8.dp))
BodyText(text = title)
}
}
}
// Screen-level composition
@Composable
fun NavigationScreen(modifier: Modifier = Modifier) {
Column(modifier = modifier) {
IconTextButton(title = "Home", icon = painterResource(R.drawable.ic_home), onClick = { /* ... */ })
IconTextButton(title = "Settings", icon = painterResource(R.drawable.ic_settings), onClick = { /* ... */ })
}
}
```
### 3.4 Leverage Immutability for Stability
Define UI state using immutable data classes (`val` properties). This allows the Compose runtime to correctly identify changes and skip unnecessary recompositions, significantly improving performance.
❌ **BAD**
```kotlin
data class UserProfileState(var name: String, var email: String) // `var` makes it unstable
```
✅ **GOOD**
```kotlin
data class UserProfileState(val name: String, val email: String) // `val` makes it stable
```
## 4. Performance Considerations
### 4.1 Optimize Recomposition with `remember` and `derivedStateOf`
Use `remember` to cache values that don't change frequently across recompositions. Use `derivedStateOf` when a state is derived from other states and you want to avoid recomposing consumers unless the *derived* state actually changes.
❌ **BAD**
```kotlin
@Composable
fun MyListScreen(items: List<String>) {
val filteredItems = items.filter { it.contains("A") } // Re-calculated on every recomposition
LazyColumn {
items(filteredItems) { Text(it) }
}
}
```
✅ **GOOD**
```kotlin
@Composable
fun MyListScreen(items: List<String>) {
val filteredItems by remember(items) { // Only re-calculate if `items` changes
derivedStateOf { items.filter { it.contains("A") } }
}
LazyColumn {
items(filteredItems) { Text(it) }
}
}
```
### 4.2 Use Lazy Containers for Large Lists
For lists with potentially many items, always use `LazyColumn`, `LazyRow`, or `LazyVerticalGrid`. These components only compose and lay out visible items, preventing performance bottlenecks.
❌ **BAD**
```kotlin
@Composable
fun MyScrollableList(items: List<String>) {
Column(modifier = Modifier.verticalScroll(rememberScrollState())) {
items.forEach { Text(it) } // All items composed, even if not visible
}
}
```
✅ **GOOD**
```kotlin
@Composable
fun MyScrollableList(items: List<String>) {
LazyColumn {
items(items) { Text(it) } // Only visible items composed
}
}
```
## 5. Theming & Consistency
### 5.1 Use MaterialTheme for Consistent Styling
Always wrap your application's UI in a `MaterialTheme` (or `MaterialTheme` 3) to ensure consistent typography, colors, and shapes across all components. Avoid creating custom styling objects outside of the theme system.
❌ **BAD**
```kotlin
@Composable
fun CustomButton(text: String, style: CustomButtonStyle) { /* ... */ }
```
✅ **GOOD**
```kotlin
@Composable
fun PrimaryButton(text: String, onClick: () -> Unit, modifier: Modifier = Modifier) {
Button(onClick = onClick, modifier = modifier, colors = ButtonDefaults.buttonColors(backgroundColor = MaterialTheme.colors.primary)) {
Text(text = text, style = MaterialTheme.typography.button)
}
}
```
### 5.2 Respect Scaffold Padding
When using `Scaffold`, always apply the provided `PaddingValues` to your main content. This ensures proper layout and avoids overlapping with top bars, bottom bars, or floating action buttons.
❌ **BAD**
```kotlin
@Composable
fun MyScreenContent() {
Scaffold(topBar = { TopAppBar(title = { Text("Title") }) }) {
Column { Text("Content") } // Ignores Scaffold padding
}
}
```
✅ **GOOD**
```kotlin
@Composable
fun MyScreenContent() {
Scaffold(topBar = { TopAppBar(title = { Text("Title") }) }) { paddingValues ->
Column(modifier = Modifier.padding(paddingValues)) { // Applies Scaffold padding
Text("Content")
}
}
}
```
## 6. Testing Approaches
### 6.1 Embrace Test-Driven Development (TDD)
Write UI tests for your composables *before* writing the implementation. This ensures components are well-defined, testable, and robust. Decompose your UI into small, testable units.
```kotlin
class FavoriteButtonTest {
@get:Rule
val composeTestRule = createComposeRule()
@Test
fun whenFavoriteIsTrueThenFilledHeartIconIsDisplayed() {
composeTestRule.setContent {
MyTheme {
FavoriteButton(isFavorite = true, onClick = {})
}
}
composeTestRule.onNodeWithContentDescription("Filled heart icon").assertIsDisplayed()
}
@Test
fun whenFavoriteIsFalseThenEmptyHeartIconIsDisplayed() {
composeTestRule.setContent {
MyTheme {
FavoriteButton(isFavorite = false, onClick = {})
}
}
composeTestRule.onNodeWithContentDescription("Empty heart icon").assertIsDisplayed()
}
}
```
© sanjeed5 · CC0-1.0 · sanjeed5/awesome-cursor-rules-mdc