Loading...
Loading...
Jetpack Compose UI standards for beautiful, sleek, minimalistic Android apps. Enforces Material 3 design, unidirectional data flow, state hoisting, consistent theming, smooth animations, and performance patterns. Use when building or reviewing Compose UI code to ensure modern, user-friendly, fast-loading interfaces that are standard across all apps.
npx skill4agent add peterbamuhigire/skills-web-dev jetpack-compose-ui| Element | Standard |
|---|---|
| Corner radius | 12-16dp for cards, 8dp for inputs, 24dp for FABs |
| Card elevation | 0-2dp (subtle shadows, never heavy) |
| Content padding | 16dp horizontal, 8-16dp vertical between items |
| Screen padding | 16dp compact, 24dp medium, 32dp expanded |
| Touch targets | Minimum 48dp height/width |
| Icon size | 24dp standard, 20dp in buttons, 48dp for empty states |
| Typography scale | Use Material 3 type scale exclusively |
painterResource(R.drawable.<name>)PROJECT_ICONS.mdandroid-custom-iconsandroid-report-tables| Topic | Reference File | When to Use |
|---|---|---|
| Design Philosophy | | Visual standards, spacing, color, typography |
| Responsive & Adaptive | | WindowSizeClass, phone/tablet layouts, adaptive nav |
| Composable Patterns | | State hoisting, MVVM, screen templates |
| Layouts & Components | | Layouts, modifiers, Material components |
| Data Tables | | Tables, pagination, responsive table/card layouts, badges |
| Animation & Polish | | Transitions, micro-interactions, loading |
| Navigation & Perf | | Nav setup, deep links, optimization |
// The UI is a function of state - nothing more
@Composable
fun UserCard(user: User, modifier: Modifier = Modifier) {
Card(modifier = modifier) {
Text(user.name, style = MaterialTheme.typography.titleMedium)
}
}State flows DOWN (ViewModel -> Screen -> Components)
Events flow UP (Components -> Screen -> ViewModel)// ALWAYS: Stateless composable (testable, reusable, previewable)
@Composable
fun SearchBar(
query: String,
onQueryChange: (String) -> Unit,
modifier: Modifier = Modifier
) {
OutlinedTextField(
value = query,
onValueChange = onQueryChange,
modifier = modifier.fillMaxWidth(),
placeholder = { Text("Search...") },
leadingIcon = { Icon(painterResource(R.drawable.search), null) },
singleLine = true,
shape = RoundedCornerShape(12.dp)
)
}@Composable
fun MyComponent(
// 1. Required data
title: String,
items: List<Item>,
// 2. Optional data with defaults
subtitle: String = "",
isLoading: Boolean = false,
// 3. Modifier (always with default)
modifier: Modifier = Modifier,
// 4. Event callbacks (last)
onClick: () -> Unit = {},
onItemClick: (Item) -> Unit = {}
)@Composable
fun FeatureScreen(
onNavigateBack: () -> Unit,
viewModel: FeatureViewModel = hiltViewModel()
) {
val uiState by viewModel.uiState.collectAsStateWithLifecycle()
Scaffold(
topBar = { /* TopAppBar */ }
) { padding ->
when (val state = uiState) {
is UiState.Loading -> LoadingScreen()
is UiState.Empty -> EmptyScreen(onAction = { /* ... */ })
is UiState.Error -> ErrorScreen(
message = state.message,
onRetry = viewModel::retry
)
is UiState.Success -> FeatureContent(
data = state.data,
onItemClick = viewModel::onItemClick,
modifier = Modifier.padding(padding)
)
}
}
}
// Content is ALWAYS a separate private composable
@Composable
private fun FeatureContent(
data: List<Item>,
onItemClick: (Item) -> Unit,
modifier: Modifier = Modifier
) {
LazyColumn(
modifier = modifier,
contentPadding = PaddingValues(16.dp),
verticalArrangement = Arrangement.spacedBy(12.dp)
) {
items(items = data, key = { it.id }) { item ->
ItemCard(item = item, onClick = { onItemClick(item) })
}
}
}WindowSizeClass// Step 1: Calculate in MainActivity
val windowSizeClass = currentWindowAdaptiveInfo().windowSizeClass
// Step 2: Pass to composables that need to adapt
@Composable
fun MyScreen(windowSizeClass: WindowSizeClass, ...) {
// Step 3: Switch layout based on breakpoint
when {
windowSizeClass.isWidthAtLeastBreakpoint(
WindowSizeClass.WIDTH_DP_MEDIUM_LOWER_BOUND
) -> { /* Two-pane / Row layout for tablets */ }
else -> { /* Single-pane / Column layout for phones */ }
}
}AnimatedContentrememberSaveablereferences/responsive-adaptive.mdenableEdgeToEdge()MainActivity.onCreate()window.statusBarColor// In Theme composable — CORRECT approach
SideEffect {
val window = (view.context as Activity).window
WindowCompat.getInsetsController(window, view).isAppearanceLightStatusBars = !darkTheme
}
// Do NOT use: window.statusBarColor = color.toArgb() ← DEPRECATED, causes issues@Composable
fun AppTheme(
darkTheme: Boolean = isSystemInDarkTheme(),
dynamicColor: Boolean = true,
content: @Composable () -> Unit
) {
val colorScheme = when {
dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> {
if (darkTheme) dynamicDarkColorScheme(LocalContext.current)
else dynamicLightColorScheme(LocalContext.current)
}
darkTheme -> DarkColorScheme
else -> LightColorScheme
}
MaterialTheme(
colorScheme = colorScheme,
typography = AppTypography,
content = content
)
}// Use consistently across ALL screens:
MaterialTheme.typography.headlineLarge // Screen titles
MaterialTheme.typography.titleLarge // Section headers
MaterialTheme.typography.titleMedium // Card titles
MaterialTheme.typography.bodyLarge // Primary body text
MaterialTheme.typography.bodyMedium // Secondary body text
MaterialTheme.typography.labelLarge // Button text
MaterialTheme.typography.labelMedium // Chips, tags, metadataobject Spacing {
val xs = 4.dp
val sm = 8.dp
val md = 16.dp
val lg = 24.dp
val xl = 32.dp
val xxl = 48.dp
}@Composable
fun StandardCard(
modifier: Modifier = Modifier,
onClick: (() -> Unit)? = null,
content: @Composable ColumnScope.() -> Unit
) {
Card(
modifier = modifier.fillMaxWidth().then(
if (onClick != null) Modifier.clickable(onClick = onClick)
else Modifier
),
shape = RoundedCornerShape(16.dp),
colors = CardDefaults.cardColors(
containerColor = MaterialTheme.colorScheme.surface
),
elevation = CardDefaults.cardElevation(defaultElevation = 1.dp),
content = content
)
}// Loading: centered progress indicator
@Composable
fun LoadingScreen(modifier: Modifier = Modifier) {
Box(modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
CircularProgressIndicator()
}
}
// Empty: icon + title + subtitle + optional action
@Composable
fun EmptyScreen(
iconRes: Int = R.drawable.inbox,
title: String,
subtitle: String,
modifier: Modifier = Modifier,
actionLabel: String? = null,
onAction: (() -> Unit)? = null
)
// Error: icon + message + retry button
@Composable
fun ErrorScreen(
message: String,
modifier: Modifier = Modifier,
onRetry: (() -> Unit)? = null
)items(items = list, key = { it.id }) { item -> ItemRow(item) }val filtered = remember(items, query) {
items.filter { it.name.contains(query, ignoreCase = true) }
}val showScrollToTop by remember {
derivedStateOf { listState.firstVisibleItemIndex > 0 }
}// BAD: creates new lambda on every recomposition
Button(onClick = { viewModel.onClick(item) })
// GOOD: stable reference
val callback = remember(item) { { viewModel.onClick(item) } }
Button(onClick = callback)// Content visibility transitions
AnimatedVisibility(
visible = isVisible,
enter = fadeIn() + expandVertically(),
exit = fadeOut() + shrinkVertically()
)
// Smooth value changes
val elevation by animateDpAsState(
targetValue = if (isPressed) 0.dp else 2.dp,
animationSpec = tween(150)
)
// Crossfade between states
Crossfade(targetState = currentTab, label = "tab") { tab ->
when (tab) {
Tab.Home -> HomeContent()
Tab.Profile -> ProfileContent()
}
}tweenModifier@PreviewkeyrememberWindowSizeClassmutableStateOfrememberColumnRowLazyColumnLazyRowisTablet()WindowSizeClassfeature-planning → Define screens, user stories, acceptance criteria
|
android-development → Architecture (MVVM, Clean, Hilt)
|
jetpack-compose-ui → Beautiful, consistent UI implementation (THIS SKILL)
|
android-tdd → Test composables and ViewModelscreateComposeRule()onNodeWithTag()