Jetpack Compose Cursor Rules: Modern Android UI
Cursor rules for Jetpack Compose covering composable functions, state management, Material 3, Navigation, ViewModel, recomposition, and preview testing.

Overview
Jetpack Compose is Android's modern declarative UI toolkit, replacing XML-based layouts with Kotlin composable functions. These cursor rules enforce unidirectional data flow, state hoisting, ViewModel integration, Material 3 theming, Navigation Compose patterns, and preview-driven development to help AI assistants generate idiomatic, recomposition-aware Android UI code.
Note:
Enforces declarative composable patterns, state/StateFlow management, ViewModel + SavedStateHandle, Material 3 Design tokens, Navigation Compose with type-safe routes, modifier ordering, and @Preview testing conventions.
Rules Configuration
---
description: Enforces Jetpack Compose best practices including unidirectional data flow, state hoisting, ViewModel integration, Material 3 theming, Navigation Compose, modifier conventions, and preview testing. Provides guidelines for modern declarative Android UI development.
globs: **/*.kt
---
# Jetpack Compose Best Practices
You are an expert in Jetpack Compose, Android UI development, and Kotlin.
You understand declarative UI, unidirectional data flow, recomposition optimization, and Material Design 3.
### Project Structure
- /ui/screens — top-level composable screens, one per destination
- /ui/components — reusable composable UI components (buttons, cards, inputs)
- /ui/theme — Material 3 theme, colors, typography, shapes
- /ui/navigation — NavHost setup, route definitions, navigation actions
- /data — repositories, data sources, models
- /viewmodel — ViewModel classes holding UI state as StateFlow
- /util — extension functions, composable helpers, preview data
### Composable Functions & State
- Hoist state to the highest common ancestor that needs it
- Pass state down via parameters, events up via lambdas
- Use remember { mutableStateOf() } for local ephemeral state only
- Derive UI state from a single source of truth in the ViewModel
- Expose state as StateFlow, collect with collectAsStateWithLifecycle()
- Never mutate state during composition — use LaunchedEffect or SideEffect
### ViewModel & Lifecycle
- Extend ViewModel(); inject repositories via constructor
- Expose UI state as a single data class: data class PostsUiState(val posts: List<Post>, val isLoading: Boolean)
- Use viewModel() or hiltViewModel() to obtain ViewModel in composables
- Use SavedStateHandle for surviving process death
- Scope coroutines to viewModelScope; cancel automatically on clear
### Material 3 Theming
- Define MaterialTheme with custom colorScheme, typography, shapes
- Use MaterialTheme.colorScheme.primary (never hardcode colors)
- Support dynamic colors with dynamicColor = Build.VERSION.SDK_INT >= Build.VERSION_CODES.S
- Use Surface, Scaffold, TopAppBar, NavigationBar for layout structure
- Apply Modifier.semantics for accessibility
### Navigation
- Use NavHost with composable() route builder and type-safe navigation
- Pass arguments via route placeholders: composable("post/{postId}")
- Never pass complex objects as nav arguments — pass IDs, load data in ViewModel
- Avoid nested NavHosts; use a single NavHost with nested navigation graphs
- Navigate from ViewModel via savedStateHandle or exposed navigation lambdas
### Modifiers & Layout
- Order modifiers correctly: size → padding → background → clickable
- Use Modifier.fillMaxWidth(), .weight(), .padding() for responsive layouts
- Use LazyColumn/LazyRow for scrollable lists; provide stable keys
- Use AnimatedVisibility and animateContentSize for transitions
- Extract complex modifier chains into extension functions
### Performance & Recomposition
- Mark composable functions as restartable and skippable by providing stable params
- Use derivedStateOf for expensive derived calculations
- Avoid unstable parameters — use data classes or @Stable annotation
- Keep composable functions small and focused; split large screens into components
- Use @ReadOnlyComposable for composables that don't emit into composition
### Testing & Previews
- Annotate composables with @Preview for rapid iteration
- Provide PreviewParameterProvider for stateful previews
- Write Compose UI tests with createComposeRule() and onNodeWithText()
- Test ViewModels with Turbine for StateFlow assertions
- Use fake repositories for ViewModel unit tests
Installation
Create jetpack-compose.mdc in your project's .cursor/rules/ directory and paste the configuration above. Cursor and Windsurf both read .cursor/rules/ — Copilot users place it in .github/copilot-instructions.md instead.
Examples
// ui/screens/PostsScreen.kt
@Composable
fun PostsScreen(
viewModel: PostsViewModel = hiltViewModel(),
onPostClick: (Long) -> Unit
) {
val uiState by viewModel.uiState.collectAsStateWithLifecycle()
PostsScreenContent(
uiState = uiState,
onPostClick = onPostClick,
onRefresh = viewModel::refresh
)
}
@Composable
private fun PostsScreenContent(
uiState: PostsUiState,
onPostClick: (Long) -> Unit,
onRefresh: () -> Unit
) {
Scaffold(
topBar = {
TopAppBar(title = { Text("Posts") })
}
) { padding ->
when {
uiState.isLoading -> Box(
modifier = Modifier.fillMaxSize().padding(padding),
contentAlignment = Alignment.Center
) {
CircularProgressIndicator()
}
uiState.error != null -> ErrorState(
message = uiState.error,
onRetry = onRefresh
)
else -> LazyColumn(
modifier = Modifier.fillMaxSize().padding(padding)
) {
items(
items = uiState.posts,
key = { it.id }
) { post ->
PostCard(
post = post,
onClick = { onPostClick(post.id) }
)
}
}
}
}
}
@Composable
private fun PostCard(post: Post, onClick: () -> Unit) {
Card(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp, vertical = 8.dp)
.clickable(onClick = onClick),
colors = CardDefaults.cardColors(
containerColor = MaterialTheme.colorScheme.surfaceVariant
)
) {
Column(modifier = Modifier.padding(16.dp)) {
Text(
text = post.title,
style = MaterialTheme.typography.titleMedium
)
Spacer(modifier = Modifier.height(4.dp))
Text(
text = post.body,
maxLines = 3,
overflow = TextOverflow.Ellipsis,
style = MaterialTheme.typography.bodyMedium
)
}
}
}
// viewmodel/PostsViewModel.kt
@HiltViewModel
class PostsViewModel @Inject constructor(
private val postRepository: PostRepository
) : ViewModel() {
private val _uiState = MutableStateFlow(PostsUiState())
val uiState: StateFlow<PostsUiState> = _uiState.asStateFlow()
init {
loadPosts()
}
fun refresh() {
_uiState.update { it.copy(isLoading = true, error = null) }
loadPosts()
}
private fun loadPosts() {
viewModelScope.launch {
postRepository.getPosts()
.onSuccess { posts ->
_uiState.update { it.copy(posts = posts, isLoading = false) }
}
.onFailure { e ->
_uiState.update {
it.copy(error = e.message ?: "Unknown error", isLoading = false)
}
}
}
}
}
data class PostsUiState(
val posts: List<Post> = emptyList(),
val isLoading: Boolean = false,
val error: String? = null
)
// ui/navigation/NavGraph.kt
@Composable
fun AppNavGraph(navController: NavHostController = rememberNavController()) {
NavHost(
navController = navController,
startDestination = "posts"
) {
composable("posts") {
PostsScreen(onPostClick = { postId ->
navController.navigate("post/$postId")
})
}
composable(
route = "post/{postId}",
arguments = listOf(navArgument("postId") { type = NavType.LongType })
) { backStackEntry ->
val postId = backStackEntry.arguments?.getLong("postId") ?: return@composable
PostDetailScreen(postId = postId)
}
}
}
@Composable
private fun ErrorState(message: String?, onRetry: () -> Unit) {
Column(
modifier = Modifier.fillMaxSize(),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center
) {
Text(
text = message ?: "Something went wrong",
color = MaterialTheme.colorScheme.error
)
Spacer(modifier = Modifier.height(16.dp))
OutlinedButton(onClick = onRetry) {
Text("Retry")
}
}
}
@Composable
fun PostDetailScreen(postId: Long) {
Text("Post detail: $postId")
// Display full post content, comments, etc.
}
Related Resources
Related Articles
Angular Cursor Rules: AI-Assisted Development Best Practices
Angular cursor rules for TypeScript and RxJS. Enforce clean architecture, component design, testing, and CLI workflows. Generate secure, maintainable code with complete project context.
Go Cursor Rules: AI-Powered Development Best Practices
Cursor rules for Go development enforcing idiomatic patterns, modern Go 1.21+ features, and clean code principles with AI assistance for production-ready code.
Rust Cursor Rules: AI-Powered Development Best Practices
Cursor rules for Rust development enforcing ownership patterns, type safety, async/await practices, and clean code principles with AI assistance for production-ready code.