jetpack-compose-ui

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.

Safety Notice

This listing is imported from skills.sh public index metadata. Review upstream SKILL.md and repository scripts before running.

Copy this and send it to your AI assistant to learn

Install skill "jetpack-compose-ui" with this command: npx skills add peterbamuhigire/skills-web-dev/peterbamuhigire-skills-web-dev-jetpack-compose-ui

Required Plugins

Superpowers plugin: MUST be active for all work using this skill. Use throughout the entire build pipeline — design decisions, code generation, debugging, quality checks, and any task where it offers enhanced capabilities. If superpowers provides a better way to accomplish something, prefer it over the default approach.

Jetpack Compose UI Standards

Design Philosophy

Goal: Every screen should feel beautiful, sleek, fast, and effortless to use.

Core Design Principles

  1. Minimalism over decoration - Remove anything that doesn't serve the user
  2. Consistency over novelty - Same patterns across every app screen
  3. Whitespace is a feature - Generous spacing creates visual breathing room
  4. Speed is UX - If it feels slow, it's broken regardless of how it looks
  5. Content-first hierarchy - Important information is immediately visible
  6. Touch-friendly targets - Minimum 48dp for all interactive elements
  7. Adaptive by default - Every screen MUST work on phones AND tablets

Enterprise Mobile UX Principles

Mobile is NOT a scaled-down desktop app. Design from the ground up for mobile users, not as a replica:

  • Task-oriented design - Mobile users have specific goals and limited time. Minimize steps/taps to task completion. Focus on one primary action per screen.
  • Value over features - Include only functionality that delivers genuine user value. Eliminate features that require users to rework on desktop later or provide insufficient context for decisions.
  • UX before UI aesthetics - Prioritize (1) backend connectivity, (2) offline support, (3) performance, (4) reliability, then UI polish. Users tolerate degraded visuals if the app works and is responsive.
  • Offline-first mentality - Design flows that work without connectivity. Sync back when online. Users won't use an app that breaks without internet.

Task Completion Efficiency (Enterprise)

For enterprise mobile apps, measure success by business impact, not UI novelty:

  • Minimize interaction steps - Every tap/swipe is friction. Test with actual users and eliminate unnecessary screens.
  • Show decision-enabling data - Always provide enough context (but not overload). E.g., for field agents: appointment count + status, not monthly analysis.
  • Reduce cognitive load - Make correct actions obvious. Use clear labels, consistent patterns, and logical groupings.
  • Measure KPIs, not vanity metrics - Define what success looks like (reduced wait times, faster task completion, fewer support requests). Avoid metrics like "time in app" or "login count."

Visual Standards

ElementStandard
Corner radius12-16dp for cards, 8dp for inputs, 24dp for FABs
Card elevation0-2dp (subtle shadows, never heavy)
Content padding16dp horizontal, 8-16dp vertical between items
Screen padding16dp compact, 24dp medium, 32dp expanded
Touch targetsMinimum 48dp height/width
Icon size24dp standard, 20dp in buttons, 48dp for empty states
Typography scaleUse Material 3 type scale exclusively

Icon Policy (Required): Use custom PNG icons with painterResource(R.drawable.<name>). Maintain PROJECT_ICONS.md per android-custom-icons.

Report Table Policy (Required): Any report that can exceed 25 rows must render as a table (see android-report-tables).

Compact Number Formatting (Required): KPI cards, summary tiles, and stat chips MUST use CurrencyFormatter.formatStat() for monetary values. Values >= 1,000,000 display as compact (e.g. "32.45M"). Values < 1,000,000 display as full format ("999,999.00"). Table rows and list items MUST use CurrencyFormatter.format() (always full format). Chart axis labels use CurrencyFormatter.formatCompact() (e.g. "1.2M", "12.3K").

Quick Reference

TopicReference FileWhen to Use
Design Philosophyreferences/design-philosophy.mdVisual standards, spacing, color, typography
Responsive & Adaptivereferences/responsive-adaptive.mdWindowSizeClass, phone/tablet layouts, adaptive nav
Composable Patternsreferences/composable-patterns.mdState hoisting, MVVM, screen templates
Layouts & Componentsreferences/layout-and-components.mdLayouts, modifiers, Material components
Data Tablesreferences/data-tables.mdTables, pagination, responsive table/card layouts, badges
Animation & Polishreferences/animation-and-polish.mdTransitions, micro-interactions, loading
Navigation & Perfreferences/navigation-and-performance.mdNav setup, deep links, optimization

Core Compose Principles

1. Declarative UI

Describe what the UI looks like, not how to build it:

// 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)
    }
}

2. Unidirectional Data Flow

State flows DOWN  (ViewModel -> Screen -> Components)
Events flow UP    (Components -> Screen -> ViewModel)

3. State Hoisting (CRITICAL)

Every reusable composable must be stateless:

// 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 Function Signature

Always follow this parameter order:

@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 = {}
)

Screen Architecture Pattern

Every screen follows this structure:

@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) })
        }
    }
}

Responsive & Adaptive Design (MANDATORY)

All apps MUST be responsive for phones and tablets. Use WindowSizeClass from the Material 3 adaptive library — never hardcode device checks.

4-Step Playbook: Know your space → Pass it down → Adapt layout → Polish transitions

// 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 */ }
    }
}

Key rules:

  • Compact (<600dp): single column, bottom nav
  • Medium (600-840dp): optional two-pane, navigation rail
  • Expanded (>840dp): two-pane, permanent nav drawer
  • Use AnimatedContent for smooth layout transitions between size classes
  • Use rememberSaveable for state that must survive configuration changes

See references/responsive-adaptive.md for complete patterns, adaptive navigation, and list-detail templates.

Theming (Consistent Across Apps)

Edge-to-Edge & Status Bar (MANDATORY)

Apps targeting SDK 35+ MUST call enableEdgeToEdge() in MainActivity.onCreate(). Without it, the app crashes on Android 15. Do NOT set window.statusBarColor directly — it's deprecated and conflicts with edge-to-edge. Only control light/dark status bar icons:

// 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

Color Strategy

Use Material 3 dynamic color with brand fallbacks:

@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
    )
}

Typography Hierarchy

// 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, metadata

Spacing System (Design Tokens)

object 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
}

Use these exclusively. No arbitrary values like 13.dp or 19.dp.

Essential UI Patterns

Card Pattern (Standard across apps)

@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
    )
}

Chart Pattern (Compose)

Use Vico for all charts. Do not introduce alternate chart libraries.

Loading / Error / Empty States

Every screen must handle all three. Use consistent components:

// 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
)

Pull-to-Refresh (MANDATORY)

Every screen that loads data from network or database MUST have pull-to-refresh. This is a universal mobile UX pattern that users expect.

Rules

  1. Use the shared PullRefreshBox wrapper from core/ui/components/PullRefreshBox.kt
  2. ViewModel must expose isRefreshing: Boolean in its state data class
  3. ViewModel must have a refresh() function that sets isRefreshing = true, reloads data, and clears the flag on success/error
  4. Static/one-time screens are exempt: login, menus, payment results, coming soon

Implementation Pattern

// In ViewModel state:
data class FeatureState(
    val listState: PaginatedListState<Item> = PaginatedListState(),
    val isRefreshing: Boolean = false
)

// In ViewModel:
fun refresh() {
    _state.update { it.copy(isRefreshing = true, listState = PaginatedListState()) }
    paginator.reset()
    loadFirstPage()
}

// In paginator onSuccess/onError: always set isRefreshing = false

// In Screen:
PullRefreshBox(
    isRefreshing = state.isRefreshing,
    onRefresh = { viewModel.refresh() },
    modifier = Modifier.fillMaxSize().padding(paddingValues)
) {
    Column(modifier = Modifier.fillMaxSize()) {
        // Screen content (filters, lists, etc.)
    }
}

Placement

  • Wrap the outermost scrollable content inside Scaffold's content lambda
  • Place PullRefreshBox outside the when block so it covers loading/error/content states
  • For tabbed screens (e.g., Inventory), wrap the pager content, passing the current tab index to refresh

Performance Essentials

1. Always use keys in lazy lists

items(items = list, key = { it.id }) { item -> ItemRow(item) }

2. Remember expensive computations

val filtered = remember(items, query) {
    items.filter { it.name.contains(query, ignoreCase = true) }
}

3. Use derivedStateOf for computed booleans

val showScrollToTop by remember {
    derivedStateOf { listState.firstVisibleItemIndex > 0 }
}

4. Never allocate in composition

// 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)

Animation Standards

Use subtle, purposeful animations:

// 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()
    }
}

Rules: Keep animations under 300ms. Use tween for most cases. Never animate on first composition unless it's a staggered list.

Patterns & Anti-Patterns

DO

  • Hoist all state out of reusable composables
  • Use Modifier parameter with default on every composable
  • Use MaterialTheme tokens for all colors, typography, shapes
  • Provide @Preview for every public composable (light + dark + tablet)
  • Use key parameter in all lazy lists
  • Handle Loading, Error, Empty states on every screen
  • Keep composables small and focused (one responsibility)
  • Use remember for expensive computations
  • Use WindowSizeClass for adaptive layouts (phone/tablet/foldable)
  • Test on both phone and tablet emulators before shipping

DON'T

  • Hardcode colors, dimensions, or font sizes
  • Create ViewModels inside composables
  • Put business logic in composables
  • Use mutableStateOf without remember
  • Use Column/Row for long scrollable lists (use LazyColumn/LazyRow)
  • Skip the empty/error states ("I'll add them later")
  • Use heavy animations that block the UI thread
  • Nest scrollable containers (LazyColumn inside Column with scroll)
  • Hardcode isTablet() booleans — use WindowSizeClass breakpoints
  • Ship without verifying the UI on a tablet-sized screen

Enterprise Mobile Anti-Patterns

  • Port desktop features as-is - Mobile users don't need 100% feature parity. Identify their top tasks and optimize for those.
  • Ignore offline capability - Don't assume always-online. Design flows that work without connectivity and sync when available.
  • Overload screens with data - Show only decision-enabling information. Too much context is as bad as too little.
  • Nest UI too deeply - More than 2-3 screens to complete a task is friction. Redesign.
  • Rely on network performance - Assume slow/spotty networks. Cache aggressively, validate on device, provide offline fallbacks.
  • Ship without real user testing - Test with actual users doing their actual work, not with designers tapping screens.

Integration with Other Skills

feature-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 ViewModels

Key integrations:

  • android-development: Architecture, DI, design tokens (this skill builds on that foundation)
  • android-tdd: Compose testing with createComposeRule(), onNodeWithTag()
  • feature-planning: Screen specs become composable implementations

References

  • Compose Samples: github.com/android/compose-samples
  • Material 3 Design: m3.material.io
  • Compose Documentation: developer.android.com/jetpack/compose
  • Architecture Samples: github.com/android/architecture-samples

Source Transparency

This detail page is rendered from real SKILL.md content. Trust labels are metadata-based hints, not a safety guarantee.

Related Skills

Related by shared tags or category signals.

Coding

google-play-store-review

No summary provided by upstream source.

Repository SourceNeeds Review
Coding

api-error-handling

No summary provided by upstream source.

Repository SourceNeeds Review
Coding

android-development

No summary provided by upstream source.

Repository SourceNeeds Review