internal/skills/content/android-compose/SKILL.md
Jetpack Compose framework guardrails, patterns, and best practices for AI-assisted development. Use when working with Android Compose projects, or when the user mentions Jetpack Compose. Provides composable patterns, state management, Material 3, navigation, and lifecycle guidelines.
npx skillsauth add ar4mirez/samuel android-composeInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Applies to: Android SDK 24+, Jetpack Compose 1.5+, Kotlin 1.9+, Material Design 3
collectAsStateWithLifecycle(), never raw collectAsState()modifier: Modifier = Modifier as its first optional parameter@Preview on every reusable component with representative dataLaunchedEffect, DisposableEffect, or SideEffect for side-effect work// BAD: side effects in composition
@Composable
fun UserList(users: List<User>) {
analytics.trackScreenView("user_list") // fires on every recomposition
LazyColumn { ... }
}
// GOOD: side effect in LaunchedEffect
@Composable
fun UserList(users: List<User>) {
LaunchedEffect(Unit) {
analytics.trackScreenView("user_list")
}
LazyColumn { ... }
}
StateFlow in ViewModel, collect with collectAsStateWithLifecycle()Loading, Success, Error)MutableStateFlow publicly from a ViewModelremember for local composable state; rememberSaveable when it must survive config changesderivedStateOf for expensive computations that depend on other state// Sealed UI state pattern
sealed interface HomeUiState {
data object Loading : HomeUiState
data class Success(val users: List<User>) : HomeUiState
data class Error(val message: String) : HomeUiState
}
// ViewModel exposes immutable StateFlow
@HiltViewModel
class HomeViewModel @Inject constructor(
private val userRepository: UserRepository,
) : ViewModel() {
private val _uiState = MutableStateFlow<HomeUiState>(HomeUiState.Loading)
val uiState: StateFlow<HomeUiState> = _uiState.asStateFlow()
fun loadUsers() {
viewModelScope.launch {
_uiState.value = HomeUiState.Loading
userRepository.getUsers()
.catch { e -> _uiState.value = HomeUiState.Error(e.message ?: "Unknown error") }
.collect { users -> _uiState.value = HomeUiState.Success(users) }
}
}
}
// Screen collects lifecycle-aware
@Composable
fun HomeScreen(viewModel: HomeViewModel = hiltViewModel()) {
val uiState by viewModel.uiState.collectAsStateWithLifecycle()
when (val state = uiState) {
is HomeUiState.Loading -> LoadingIndicator()
is HomeUiState.Success -> UserList(state.users)
is HomeUiState.Error -> ErrorMessage(state.message, onRetry = viewModel::loadUsers)
}
}
popUpTo and inclusive when navigating to auth screenshiltViewModel() inside composable {} blocks for scoped ViewModelssealed class Screen(val route: String) {
data object Login : Screen("login")
data object Home : Screen("home")
data object Detail : Screen("detail/{userId}") {
fun createRoute(userId: String) = "detail/$userId"
}
}
@Composable
fun AppNavigation() {
val navController = rememberNavController()
NavHost(navController = navController, startDestination = Screen.Home.route) {
composable(Screen.Home.route) {
HomeScreen(onUserClick = { userId ->
navController.navigate(Screen.Detail.createRoute(userId))
})
}
composable(
route = Screen.Detail.route,
arguments = listOf(navArgument("userId") { type = NavType.StringType }),
) { backStackEntry ->
val userId = backStackEntry.arguments?.getString("userId") ?: ""
DetailScreen(userId = userId, onBack = { navController.popBackStack() })
}
}
}
MaterialTheme composabledynamicLightColorScheme / dynamicDarkColorSchemeColor.kt; typography in Type.kt; theme in Theme.ktMaterialTheme.colorScheme.* and MaterialTheme.typography.* instead of hardcoded values@Composable
fun MyAppTheme(
darkTheme: Boolean = isSystemInDarkTheme(),
dynamicColor: Boolean = true,
content: @Composable () -> Unit,
) {
val colorScheme = when {
dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> {
val context = LocalContext.current
if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context)
}
darkTheme -> DarkColorScheme
else -> LightColorScheme
}
MaterialTheme(colorScheme = colorScheme, typography = Typography, content = content)
}
Application with @HiltAndroidApp, Activity with @AndroidEntryPoint@HiltViewModel and use @Inject constructorhiltViewModel() in composables, never construct ViewModels manuallyAppModule, NetworkModule, DatabaseModule| Effect | Trigger | Use for |
|--------|---------|---------|
| LaunchedEffect(key) | On enter + when key changes | One-shot loads, navigation events |
| DisposableEffect(key) | On enter + when key changes (with cleanup) | Listeners, observers, callbacks |
| SideEffect | After every successful recomposition | Syncing compose state to non-compose |
| rememberCoroutineScope() | Manual launch from callbacks | Button clicks launching coroutines |
// One-shot load on screen enter
LaunchedEffect(Unit) { viewModel.loadData() }
// Clean up a listener when leaving composition
DisposableEffect(lifecycleOwner) {
val observer = LifecycleEventObserver { _, event -> ... }
lifecycleOwner.lifecycle.addObserver(observer)
onDispose { lifecycleOwner.lifecycle.removeObserver(observer) }
}
app/src/main/java/com/example/myapp/
├── MyApplication.kt # @HiltAndroidApp
├── MainActivity.kt # @AndroidEntryPoint, setContent {}
├── navigation/
│ └── AppNavigation.kt # NavHost, route definitions
├── ui/
│ ├── theme/ # Color.kt, Type.kt, Theme.kt
│ ├── screens/ # Feature screens
│ │ ├── home/
│ │ │ ├── HomeScreen.kt # Composable
│ │ │ └── HomeViewModel.kt # ViewModel + UiState
│ │ └── detail/
│ │ ├── DetailScreen.kt
│ │ └── DetailViewModel.kt
│ └── components/ # Shared composables
│ ├── LoadingIndicator.kt
│ └── ErrorMessage.kt
├── data/
│ ├── model/ # Domain models (@Serializable)
│ ├── remote/ # Retrofit service, DTOs
│ ├── local/ # Room database, DAOs
│ └── repository/ # Repository implementations
└── di/ # Hilt modules
├── AppModule.kt
└── NetworkModule.kt
Screen.kt + ViewModel.kt// build.gradle.kts (app)
val composeBom = platform("androidx.compose:compose-bom:2024.01.00")
implementation(composeBom)
implementation("androidx.compose.ui:ui")
implementation("androidx.compose.material3:material3")
implementation("androidx.compose.ui:ui-tooling-preview")
implementation("androidx.activity:activity-compose:1.8.2")
implementation("androidx.lifecycle:lifecycle-runtime-compose:2.7.0")
implementation("androidx.lifecycle:lifecycle-viewmodel-compose:2.7.0")
implementation("androidx.navigation:navigation-compose:2.7.6")
implementation("androidx.hilt:hilt-navigation-compose:1.1.0")
implementation("com.google.dagger:hilt-android:2.50")
ksp("com.google.dagger:hilt-compiler:2.50")
implementation("io.coil-kt:coil-compose:2.5.0")
// Testing
testImplementation("junit:junit:4.13.2")
testImplementation("io.mockk:mockk:1.13.9")
testImplementation("app.cash.turbine:turbine:1.0.0")
testImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:1.7.3")
androidTestImplementation(composeBom)
androidTestImplementation("androidx.compose.ui:ui-test-junit4")
debugImplementation("androidx.compose.ui:ui-tooling")
debugImplementation("androidx.compose.ui:ui-test-manifest")
StandardTestDispatcher + Dispatchers.setMain() for coroutine controlStateFlow assertionscoEvery / coVerify for suspend function mocking@After with Dispatchers.resetMain()@OptIn(ExperimentalCoroutinesApi::class)
class HomeViewModelTest {
private val testDispatcher = StandardTestDispatcher()
private val userRepository = mockk<UserRepository>()
@Before fun setup() { Dispatchers.setMain(testDispatcher) }
@After fun tearDown() { Dispatchers.resetMain() }
@Test
fun `loadUsers emits Success state on valid data`() = runTest {
val users = listOf(User(id = "1", email = "[email protected]", name = "Alice"))
coEvery { userRepository.getUsers() } returns flowOf(users)
val viewModel = HomeViewModel(userRepository)
advanceUntilIdle()
viewModel.uiState.test {
val state = awaitItem()
assertTrue(state is HomeUiState.Success)
assertEquals(users, (state as HomeUiState.Success).users)
}
}
}
createComposeRule() for isolated composable testsonNodeWithText, onNodeWithTag, onNodeWithContentDescriptionassertExists(), assertIsDisplayed(), assertIsEnabled()performClick(), performTextInput()class HomeScreenTest {
@get:Rule val composeTestRule = createComposeRule()
@Test
fun displays_user_name_when_loaded() {
composeTestRule.setContent {
MyAppTheme {
UserCard(user = User("1", "[email protected]", "Alice"), onClick = {}, onDeleteClick = {})
}
}
composeTestRule.onNodeWithText("Alice").assertIsDisplayed()
composeTestRule.onNodeWithText("[email protected]").assertIsDisplayed()
}
}
# Build
./gradlew assembleDebug # Debug APK
./gradlew assembleRelease # Release APK
./gradlew bundleRelease # AAB for Play Store
# Test
./gradlew test # Unit tests
./gradlew connectedAndroidTest # Instrumented tests on device/emulator
# Quality
./gradlew lint # Android Lint
./gradlew ktlintCheck # Code style check
./gradlew ktlintFormat # Auto-fix formatting
./gradlew detekt # Static analysis
# Install & Run
./gradlew installDebug # Install on connected device
./gradlew clean # Clean build artifacts
collectAsStateWithLifecycle() for all Flow collection in composablesremember {} for expensive local computationsLaunchedEffect for one-shot side effects, DisposableEffect for cleanupwhen expressionscontentDescription on all interactive and decorative iconsModifier chaining; accept modifier parameter in every public composable@PreviewMutableStateFlow or MutableState from ViewModels!! operator in production codehiltViewModel())rememberSaveable when needed)For detailed UI patterns, animation, testing strategies, performance, and accessibility guidance, see:
development
Zig language guardrails, patterns, and best practices for AI-assisted development. Use when working with Zig files (.zig), build.zig, or when the user mentions Zig. Provides comptime patterns, allocator conventions, C interop guidelines, and testing standards specific to this project's coding standards.
tools
WordPress framework guardrails, patterns, and best practices for AI-assisted development. Use when working with WordPress projects, or when the user mentions WordPress. Provides theme development, plugin architecture, REST API, blocks, and security guidelines.
tools
Toolkit for interacting with and testing local web applications using Playwright. Supports verifying frontend functionality, debugging UI behavior, capturing browser screenshots, and viewing browser logs. Use when testing web apps, automating browser interactions, or debugging frontend issues.
tools
Suite of tools for creating elaborate, multi-component web applications using modern frontend technologies (React, Tailwind CSS, shadcn/ui). Use for complex projects requiring state management, routing, or shadcn/ui components - not for simple single-file HTML/JSX pages.