Skip to main content
skill CX Codex

android-jetpack-compose — Claude Code skill

Guides an agent through Android UI work with Jetpack Compose and state patterns. Use it when composables, ViewModel state, or declarative UI need attention.

Install into Claude Code · Skills CLI (Vercel) All methods and platforms ↓
npx skills add TheBushidoCollective/han --skill android-jetpack-compose -a claude-code
npx skills add TheBushidoCollective/han --skill jetpack-compose -a claude-code
Open on GitHub

What android-jetpack-compose does

This skill guides native Android UI work in Jetpack Compose. It covers state with remember, rememberSaveable, mutableStateOf, and derivedStateOf, plus state hoisting and connecting a ViewModel backed by StateFlow to screen UI with collectAsStateWithLifecycle.

It includes composable conventions such as putting Modifier first among optional parameters, using slot APIs, and assigning stable keys in lists. The material also covers LaunchedEffect, DisposableEffect, SideEffect, Navigation Compose, Material 3 themes, and derived state for filtered lists.

The skill is not user-invocable, so this is a reason to state the Android task clearly in your prompt. Its configured tools are Read, Write, Edit, Bash, Grep, and Glob.

What it helps with

  • persist field state through configuration changes
  • hoist state out of a child composable
  • connect a ViewModel StateFlow to a screen
  • add stable keys to LazyColumn items
  • choose an effect for loading or resource cleanup

Example prompts for the agent

  • Refactor UserScreen into Jetpack Compose with StateFlow from a ViewModel.
  • Make NameInput stateless and move its value into the parent composable.
  • Add navigation from home to detail/{itemId} with Navigation Compose.

How to install android-jetpack-compose

1Skills CLI (Vercel)

npx skills add TheBushidoCollective/han --skill android-jetpack-compose -a claude-code
npx skills add TheBushidoCollective/han --skill jetpack-compose -a claude-code

2Manually: put the files in place

~/.claude/skills/jetpack-compose/SKILL.md

Copy the whole skill folder; for one project use .claude/skills/jetpack-compose/

Step by step, with commands for each platform: How to install skills in Claude Code: five ways

Questions about android-jetpack-compose

How do I install android-jetpack-compose in Claude Code?

Run npx skills add TheBushidoCollective/han --skill android-jetpack-compose -a claude-code, then npx skills add TheBushidoCollective/han --skill jetpack-compose -a claude-code. The skill file is located at ~/.claude/skills/jetpack-compose/SKILL.md.

What does android-jetpack-compose do?

The skill gives an agent patterns for Android UI development with Jetpack Compose. It covers state handling, ViewModel, effects, lists, navigation, and Material 3.

Which state option does the skill cover for Jetpack Compose?

remember keeps a value through recomposition, while rememberSaveable survives configuration changes. Use derivedStateOf for computed state that updates when its dependencies change.

Source text

SKILL.md · Apache-2.0
---
name: android-jetpack-compose
user-invocable: false
description: Use when building Android UIs with Jetpack Compose, managing state with remember/mutableStateOf, or implementing declarative UI patterns.
allowed-tools:
  - Read
  - Write
  - Edit
  - Bash
  - Grep
  - Glob
---

# Android - Jetpack Compose

Modern declarative UI toolkit for building native Android interfaces.

## Key Concepts

### State Management

Compose provides several ways to manage state:

- **remember**: Survives recomposition
- **rememberSaveable**: Survives configuration changes
- **mutableStateOf**: Creates observable state
- **derivedStateOf**: Computed state that updates when dependencies change

```kotlin
@Composable
fun Counter() {
    var count by remember { mutableStateOf(0) }

    Column {
        Text("Count: $count")
        Button(onClick = { count++ }) {
            Text("Increment")
        }
    }
}

// With saveable for configuration changes
@Composable
fun SearchField() {
    var query by rememberSaveable { mutableStateOf("") }

    TextField(
        value = query,
        onValueChange = { query = it },
        placeholder = { Text("Search...") }
    )
}
```

### State Hoisting

Lift state up to make composables stateless and reusable:

```kotlin
// Stateless composable
@Composable
fun NameInput(
    name: String,
    onNameChange: (String) -> Unit,
    modifier: Modifier = Modifier
) {
    TextField(
        value = name,
        onValueChange = onNameChange,
        label = { Text("Name") },
        modifier = modifier
    )
}

// Stateful parent
@Composable
fun UserForm() {
    var name by remember { mutableStateOf("") }

    NameInput(
        name = name,
        onNameChange = { name = it }
    )
}
```

### ViewModel Integration

```kotlin
class UserViewModel : ViewModel() {
    private val _uiState = MutableStateFlow(UserUiState())
    val uiState: StateFlow<UserUiState> = _uiState.asStateFlow()

    fun updateName(name: String) {
        _uiState.update { it.copy(name = name) }
    }

    fun saveUser() {
        viewModelScope.launch {
            _uiState.update { it.copy(isLoading = true) }
            try {
                userRepository.save(_uiState.value.toUser())
                _uiState.update { it.copy(isLoading = false, isSaved = true) }
            } catch (e: Exception) {
                _uiState.update { it.copy(isLoading = false, error = e.message) }
            }
        }
    }
}

@Composable
fun UserScreen(viewModel: UserViewModel = viewModel()) {
    val uiState by viewModel.uiState.collectAsStateWithLifecycle()

    UserContent(
        uiState = uiState,
        onNameChange = viewModel::updateName,
        onSave = viewModel::saveUser
    )
}
```

## Best Practices

### Composable Function Guidelines

```kotlin
// Use Modifier as first optional parameter
@Composable
fun CustomCard(
    title: String,
    modifier: Modifier = Modifier,
    onClick: () -> Unit = {}
) {
    Card(
        modifier = modifier.clickable(onClick = onClick)
    ) {
        Text(
            text = title,
            modifier = Modifier.padding(16.dp)
        )
    }
}

// Use slot APIs for flexible content
@Composable
fun CustomScaffold(
    topBar: @Composable () -> Unit = {},
    bottomBar: @Composable () -> Unit = {},
    content: @Composable (PaddingValues) -> Unit
) {
    Scaffold(
        topBar = topBar,
        bottomBar = bottomBar,
        content = content
    )
}
```

### Efficient Recomposition

```kotlin
// Use keys for list items
@Composable
fun UserList(users: List<User>) {
    LazyColumn {
        items(
            items = users,
            key = { it.id }  // Stable key for efficient updates
        ) { user ->
            UserItem(user)
        }
    }
}

// Use derivedStateOf for expensive computations
@Composable
fun FilteredList(items: List<Item>, query: String) {
    val filteredItems by remember(items, query) {
        derivedStateOf {
            items.filter { it.name.contains(query, ignoreCase = true) }
        }
    }

    LazyColumn {
        items(filteredItems) { item ->
            ItemRow(item)
        }
    }
}
```

### Side Effects

```kotlin
// LaunchedEffect for coroutine-based side effects
@Composable
fun UserProfile(userId: String, viewModel: UserViewModel) {
    LaunchedEffect(userId) {
        viewModel.loadUser(userId)
    }

    // UI content
}

// DisposableEffect for cleanup
@Composable
fun LifecycleAwareComponent(lifecycle: Lifecycle) {
    DisposableEffect(lifecycle) {
        val observer = LifecycleEventObserver { _, event ->
            // Handle lifecycle events
        }
        lifecycle.addObserver(observer)

        onDispose {
            lifecycle.removeObserver(observer)
        }
    }
}

// SideEffect for non-suspend side effects
@Composable
fun AnalyticsScreen(screenName: String) {
    SideEffect {
        analytics.logScreenView(screenName)
    }
}
```

## Common Patterns

### Navigation with Navigation Compose

```kotlin
@Composable
fun AppNavigation() {
    val navController = rememberNavController()

    NavHost(navController = navController, startDestination = "home") {
        composable("home") {
            HomeScreen(
                onNavigateToDetail = { id ->
                    navController.navigate("detail/$id")
                }
            )
        }
        composable(
            route = "detail/{itemId}",
            arguments = listOf(navArgument("itemId") { type = NavType.StringType })
        ) { backStackEntry ->
            val itemId = backStackEntry.arguments?.getString("itemId")
            DetailScreen(itemId = itemId)
        }
    }
}
```

### Material 3 Theming

```kotlin
@Composable
fun AppTheme(
    darkTheme: Boolean = isSystemInDarkTheme(),
    content: @Composable () -> Unit
) {
    val colorScheme = when {
        darkTheme -> darkColorScheme(
            primary = Purple80,
            secondary = PurpleGrey80,
            tertiary = Pink80
        )
        else -> lightColorScheme(
            primary = Purple40,
            secondary = PurpleGrey40,
            tertiary = Pink40
        )
    }

    MaterialTheme(
        colorScheme = colorScheme,
        typography = Typography,
        content = content
    )
}

// Using theme values
@Composable
fun ThemedCard() {
    Card(
        colors = CardDefaults.cardColors(
            containerColor = MaterialTheme.colorScheme.surfaceVariant
        )
    ) {
        Text(
            text = "Themed content",
            style = MaterialTheme.typography.bodyLarge,
            color = MaterialTheme.colorScheme.onSurfaceVariant
        )
    }
}
```

### Lists and Grids

```kotlin
@Composable
fun ProductGrid(products: List<Product>) {
    LazyVerticalGrid(
        columns = GridCells.Adaptive(minSize = 160.dp),
        contentPadding = PaddingValues(16.dp),
        horizontalArrangement = Arrangement.spacedBy(16.dp),
        verticalArrangement = Arrangement.spacedBy(16.dp)
    ) {
        items(products, key = { it.id }) { product ->
            ProductCard(product)
        }
    }
}

// Sticky headers
@Composable
fun ContactList(contacts: Map<Char, List<Contact>>) {
    LazyColumn {
        contacts.forEach { (initial, contactsForInitial) ->
            stickyHeader {
                Text(
                    text = initial.toString(),
                    modifier = Modifier
                        .fillMaxWidth()
                        .background(MaterialTheme.colorScheme.surface)
                        .padding(16.dp),
                    style = MaterialTheme.typography.titleMedium
                )
            }
            items(contactsForInitial) { contact ->
                ContactItem(contact)
            }
        }
    }
}
```

## Anti-Patterns

### Avoid Side Effects in Composition

Bad:

```kotlin
@Composable
fun BadExample(viewModel: ViewModel) {
    viewModel.loadData()  // Called on every recomposition!

    Text("Data loaded")
}
```

Good:

```kotlin
@Composable
fun GoodExample(viewModel: ViewModel) {
    LaunchedEffect(Unit) {
        viewModel.loadData()
    }

    Text("Data loaded")
}
```

### Don't Read State in Remember Block

Bad:

```kotlin
@Composable
fun BadCounter(initial: Int) {
    // Won't update when initial changes
    var count by remember { mutableStateOf(initial) }
}
```

Good:

```kotlin
@Composable
fun GoodCounter(initial: Int) {
    var count by remember(initial) { mutableStateOf(initial) }
}
```

### Avoid Heavy Computation During Composition

Bad:

```kotlin
@Composable
fun BadList(items: List<Item>) {
    // Runs on every recomposition
    val sorted = items.sortedBy { it.name }
    LazyColumn { /* ... */ }
}
```

Good:

```kotlin
@Composable
fun GoodList(items: List<Item>) {
    val sorted by remember(items) {
        derivedStateOf { items.sortedBy { it.name } }
    }
    LazyColumn { /* ... */ }
}
```

## Related Skills

- **android-architecture**: MVVM and clean architecture patterns
- **android-kotlin-coroutines**: Async operations in Compose

© TheBushidoCollective · Apache-2.0 · TheBushidoCollective/han

Similar skills

Everything for Mobile →

From the same set7

Project sponsors

Companies that keep this analytics open
Gold sponsor

Impressions, clicks and code copies — open statistics. We keep the numbers public for the same reason NeBlask itself is public. Become a sponsor