Skip to main content
android-compose 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.
インストールへ移動 Skills Marketplace コミュニティが作成したAIスキルを発見・探索
Codex または Claude でインストール この Prompt をコピーして Codex、Claude、または他のアシスタントに貼り付けると、Skill ページを確認してインストールできます。
直接コマンドでは確認用 Prompt が省略されます。実行前にソースを確認してください。
npx skills add https://github.com/ar4mirez/samuel --skill android-composeコマンドは1行のまま表示されます。コピー前に横へスクロールして全体を確認してください。
ローカルで確認しますか?SkillsMP が現在取得できるファイルをダウンロードできます。
Zipをダウンロード ダウンロード中... name android-compose description 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.
license MIT metadata {"author":"samuel","version":"1.0","category":"framework","language":"kotlin","extensions":".kt"}
Android Jetpack Compose Guide
Applies to: Android SDK 24+, Jetpack Compose 1.5+, Kotlin 1.9+, Material Design 3
Core Principles
Declarative UI : Describe what the UI should look like, not how to build it step-by-step
Unidirectional Data Flow : State flows down, events flow up -- always
Composable Functions Are Cheap : The framework handles when to recompose; keep composables pure
State Hoisting : Lift state to the nearest common ancestor that needs it
Lifecycle Awareness : Collect flows with collectAsStateWithLifecycle(), never raw collectAsState()
Guardrails
Composable Conventions
Every composable accepts modifier: Modifier = Modifier as its first optional parameter
Keep composables small and focused (one responsibility per function)
Use @Preview on every reusable component with representative data
Never perform side effects (network, DB, analytics) directly inside a composable body
Use LaunchedEffect, DisposableEffect, or SideEffect for side-effect work
Prefer stateless composables; hoist state to the caller or ViewModel
@Composable
fun UserList (users: List <User >) {
analytics.trackScreenView("user_list" )
LazyColumn { ... }
}
@Composable
fun UserList (users: List <User >) {
LaunchedEffect(Unit ) {
analytics.trackScreenView("user_list" )
}
LazyColumn { ... }
}
State Management
Use StateFlow in ViewModel, collect with collectAsStateWithLifecycle()
Model UI state as a sealed interface (Loading, Success, Error)
Never expose MutableStateFlow publicly from a ViewModel
Use remember for local composable state; rememberSaveable when it must survive config changes
Use derivedStateOf for expensive computations that depend on other state
sealed interface HomeUiState {
data object Loading : HomeUiState
data class Success (val users: List<User>) : HomeUiState
data class Error (val message: String) : HomeUiState
}
@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) }
}
}
}
@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)
}
}
Navigation
Define routes as a sealed class or sealed interface for type safety
Never pass complex objects as navigation arguments; pass IDs and load from ViewModel
Clear back stack correctly with popUpTo and inclusive when navigating to auth screens
Use hiltViewModel() inside composable {} blocks for scoped ViewModels
sealed 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() })
}
}
}
Material 3 Theming
Always wrap the app root in your custom MaterialTheme composable
Support dynamic color on Android 12+ with dynamicLightColorScheme / dynamicDarkColorScheme
Define color tokens in a dedicated Color.kt; typography in Type.kt; theme in Theme.kt
Use MaterialTheme.colorScheme.* and MaterialTheme.typography.* instead of hardcoded values
Support both light and dark themes from day one
@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)
}
Dependency Injection (Hilt)
Annotate Application with @HiltAndroidApp, Activity with @AndroidEntryPoint
Annotate ViewModels with @HiltViewModel and use @Inject constructor
Use hiltViewModel() in composables, never construct ViewModels manually
Organize DI modules by layer: AppModule, NetworkModule, DatabaseModule
Lifecycle & Effects 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 SideEffectAfter every successful recomposition Syncing compose state to non-compose rememberCoroutineScope()Manual launch from callbacks Button clicks launching coroutines
LaunchedEffect(Unit ) { viewModel.loadData() }
DisposableEffect(lifecycleOwner) {
val observer = LifecycleEventObserver { _, event -> ... }
lifecycleOwner.lifecycle.addObserver(observer)
onDispose { lifecycleOwner.lifecycle.removeObserver(observer) }
}
Project Structure 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
ui/screens/ : One sub-package per feature, each with Screen.kt + ViewModel.kt
ui/components/ : Reusable composables shared across screens
data/ : Models, repositories, API services; no Compose dependencies here
di/ : Hilt modules providing singletons and scoped dependencies
Key Dependencies
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" )
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" )
Testing
ViewModel Unit Tests
Use StandardTestDispatcher + Dispatchers.setMain() for coroutine control
Use Turbine for StateFlow assertions
Use MockK with coEvery / coVerify for suspend function mocking
Reset dispatcher in @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 = "a@b.com" , 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)
}
}
}
Compose UI Tests
Use createComposeRule() for isolated composable tests
Query nodes with onNodeWithText, onNodeWithTag, onNodeWithContentDescription
Assert with assertExists(), assertIsDisplayed(), assertIsEnabled()
Perform actions with performClick(), performTextInput()
class HomeScreenTest {
@get:Rule val composeTestRule = createComposeRule()
@Test
fun displays_user_name_when_loaded () {
composeTestRule.setContent {
MyAppTheme {
UserCard(user = User("1" , "a@b.com" , "Alice" ), onClick = {}, onDeleteClick = {})
}
}
composeTestRule.onNodeWithText("Alice" ).assertIsDisplayed()
composeTestRule.onNodeWithText("a@b.com" ).assertIsDisplayed()
}
}
Commands
./gradlew assembleDebug
./gradlew assembleRelease
./gradlew bundleRelease
./gradlew test
./gradlew connectedAndroidTest
./gradlew lint
./gradlew ktlintCheck
./gradlew ktlintFormat
./gradlew detekt
./gradlew installDebug
./gradlew clean
Best Practices Summary
Do
Use collectAsStateWithLifecycle() for all Flow collection in composables
Use remember {} for expensive local computations
Use LaunchedEffect for one-shot side effects, DisposableEffect for cleanup
Keep composables stateless; hoist state to ViewModel or caller
Use sealed interfaces for UI state with exhaustive when expressions
Provide contentDescription on all interactive and decorative icons
Use Modifier chaining; accept modifier parameter in every public composable
Preview every reusable component with @Preview
Avoid
Performing I/O, network calls, or analytics tracking directly in composable bodies
Exposing MutableStateFlow or MutableState from ViewModels
Using !! operator in production code
Hardcoding colors, dimensions, or strings (use theme tokens and resources)
Creating ViewModels manually (use hiltViewModel())
Passing complex objects through navigation arguments (pass IDs instead)
Ignoring configuration changes (use rememberSaveable when needed)
References For detailed UI patterns, animation, testing strategies, performance, and accessibility guidance, see:
External References