Google App Development (Jetpack Compose / Kotlin)
Modern best practices for building apps across Google platforms. Targets Jetpack Compose as the primary UI framework with Kotlin. Covers Android (phone), tablet, foldables, Wear OS, Google TV / Android TV, Android Auto, Android Automotive OS, and AOSP-based platforms (Meta Quest, Amazon Fire TV, Amazon Fire Tablets).
For Kotlin language fundamentals (null safety, coroutines, data modeling, error handling, idiomatic patterns), see the kotlin-development skill. This skill focuses on platform and framework patterns.
Important Rules
- Always generate Kotlin. Only write Java when the project explicitly requires it (legacy codebase, Java-only API). See
references/java-interop.md for bridging patterns.
- Compose-first. Use View-based UI only when Compose lacks the capability or the project has an existing View-based codebase. See
references/view-interop.md for interop patterns.
- Check the project context. Before applying patterns, check the
minSdk, Compose BOM version, and existing architecture. Adapt recommendations accordingly.
Reference Files
references/compose-patterns.md — Composables, state hoisting, recomposition, remember, LazyColumn, navigation, theming, Material 3, side effects
references/concurrency.md — Coroutines in Android context: viewModelScope, lifecycleScope, repeatOnLifecycle, WorkManager, testing
references/android-background-work.md — Foreground Services (types, permissions, restrictions), Background Limits (Doze, App Standby Buckets), AlarmManager (exact/inexact, permissions)
references/android-lifecycle.md — Activity/Fragment lifecycle, ViewModel, saved state, process death, configuration changes
references/view-interop.md — AndroidView, ComposeView, embedding Compose in Views and Views in Compose, migration strategies
references/project-structure.md — Gradle setup, multi-module architecture, build variants, version catalogs, convention plugins
references/tablet-patterns.md — Adaptive layouts, WindowSizeClass (isWidthAtLeastBreakpoint), multi-window, foldable devices, large screen guidelines
references/wear-os-patterns.md — Compose for Wear OS, Tiles, complications, Health Services, watch face
references/tv-patterns.md — Compose for TV, focus management, Leanback, D-pad navigation, Google TV / Android TV
references/car-app-library.md — Car App Library shared API: CarAppService, Session, Screen, ScreenManager, templates, constraints, lifecycle, CarContext, testing
references/android-auto-patterns.md — Android Auto phone projection: Media3 MediaLibraryService, messaging notifications, navigation/DHU, POI, distribution
references/android-automotive-patterns.md — Android Automotive OS (AAOS): AAOS-specific Car App Library diffs, car hardware APIs, HVAC, system UI, multi-user, OEM customization, multi-zone audio
references/java-interop.md — Calling Java from Kotlin, nullability annotations, SAM conversions, incremental migration
references/meta-quest-patterns.md — Adapting Android APK for Meta Quest, spatial UI, entitlement check, VR input, passthrough
references/local-storage.md — Room database (entities, DAOs, relations, migrations, testing), DataStore (Preferences and Proto), encrypted storage (Android Keystore, DataStore + Tink, SQLCipher), file storage (internal, scoped storage, FileProvider), storage selection guide
references/networking-api.md — Retrofit, OkHttp, Ktor Client, JSON serialization, repository pattern, error handling, interceptors, certificate pinning, caching, connectivity, pagination (custom + Paging 3), file upload/download, testing
references/fire-tv-patterns.md — Amazon Fire TV, Appstore, Amazon IAP, Alexa integration, missing Google Play Services
references/media-playback.md — Media3 / ExoPlayer, MediaSession, audio focus, Picture-in-Picture, offline downloads, DRM, streaming formats, caching
references/billing-payments.md — Google Play Billing Library (PBL 8), BillingClient, one-time purchases (consumable / non-consumable), subscriptions (base plans, offers, replacement modes), subscription offers (eligibility types, pricing phases, offer tags, developer-determined offers, winback offers, promo codes), purchase verification, RTDN, subscription lifecycle (grace period, account hold, pause), alternative billing, testing
references/fire-tablet-patterns.md — Amazon Fire Tablets, device capabilities, Show Mode, Kids Edition, Special Offers
references/testing.md — JUnit 5, MockK, Turbine, ViewModel testing, Compose UI testing, Robolectric, Room in-memory testing, Hilt testing, Espresso, coroutine testing (runTest, TestDispatcher), test architecture (pyramid, MVI/MVVM strategies), fakes vs mocks, CI integration
references/code-quality.md — Android Lint configuration (lint {} block, lint.xml, severity levels), baseline management, suppression (@SuppressLint, tools:ignore), built-in check categories (correctness, security, performance, accessibility), Compose lint checks, custom lint rules (Detector, Issue, IssueRegistry), CI integration (SARIF, GitHub Code Scanning), multi-module convention plugin. For Detekt/ktlint, see kotlin-development skill's references/static-analysis.md
Code Style
For Kotlin language style (naming, null safety, type annotations), follow the kotlin-development skill. Below are Android/Compose-specific conventions.
- Composable naming —
PascalCase for UI-emitting composables, camelCase for composables that return values.
- Modifier parameter — always the first optional parameter, default to
Modifier.
- Preview functions — prefix with
Preview, annotate with @Preview.
- Define a custom app theme and apply it at the root. Every app must have a custom
AppTheme composable that wraps MaterialTheme with app-specific colors, typography, and shapes. Apply it once at the top level (setContent { AppTheme { ... } }). All UI code must reference theme tokens — never raw values.
// 1. Define custom theme (once, in ui/theme/)
@Composable
fun AppTheme(
darkTheme: Boolean = isSystemInDarkTheme(),
content: @Composable () -> Unit,
) {
val colorScheme = if (darkTheme) AppDarkColorScheme else AppLightColorScheme
MaterialTheme(
colorScheme = colorScheme,
typography = AppTypography,
shapes = AppShapes,
content = content,
)
}
// 2. Apply at root (once)
setContent { AppTheme { AppNavGraph() } }
// 3. Use theme tokens everywhere — never raw values
Text(
text = "Hello",
style = MaterialTheme.typography.bodyMedium, // not fontSize = 14.sp
color = MaterialTheme.colorScheme.onSurface, // not Color(0xFF1C1B1F)
)
Card(shape = MaterialTheme.shapes.medium) { ... } // not RoundedCornerShape(8.dp)
For values not covered by Material tokens (spacing, elevation, icon sizes), define app-level design tokens:
object AppSpacing {
val small = 8.dp
val medium = 16.dp
val large = 24.dp
}
No magic numbers in UI code. If a numeric value appears in UI, it must be either a Material theme token or an app-defined design constant. For theming details see references/compose-patterns.md.
Naming Conventions (Android-Specific)
| Element |
Convention |
Example |
| Composable (UI) |
PascalCase |
UserProfileScreen, SettingsCard |
| Composable (value) |
camelCase |
rememberScrollState() |
| ViewModel |
Suffix ViewModel |
SettingsViewModel |
| Screen composable |
Suffix Screen |
HomeScreen, ProfileScreen |
| UI State |
Suffix UiState |
HomeUiState, ProfileUiState |
| Activity |
Suffix Activity |
MainActivity |
| Fragment (legacy) |
Suffix Fragment |
HomeFragment |
| Repository |
Suffix Repository |
UserRepository |
| Use case |
Verb phrase |
GetUserUseCase, SyncDataUseCase |
| Module |
Feature name, kebab-case |
:feature:auth, :core:network |
Jetpack Compose Essentials
// UI State
data class HomeUiState(
val items: List<Item> = emptyList(),
val isLoading: Boolean = false,
val error: String? = null,
)
// ViewModel
class HomeViewModel(
private val repository: ItemRepository,
) : ViewModel() {
private val _uiState = MutableStateFlow(HomeUiState())
val uiState: StateFlow<HomeUiState> = _uiState.asStateFlow()
fun load() {
viewModelScope.launch {
_uiState.update { it.copy(isLoading = true) }
repository.getItems()
.onSuccess { items -> _uiState.update { it.copy(items = items, isLoading = false) } }
.onFailure { e -> _uiState.update { it.copy(error = e.message, isLoading = false) } }
}
}
}
// Composable
@Composable
fun HomeScreen(
viewModel: HomeViewModel = viewModel(),
onItemClick: (Item) -> Unit,
) {
val uiState by viewModel.uiState.collectAsStateWithLifecycle()
when {
uiState.isLoading -> LoadingIndicator()
uiState.error != null -> ErrorMessage(uiState.error!!)
else -> ItemList(items = uiState.items,
}
}
Rules:
- Use
StateFlow + collectAsStateWithLifecycle() for state in ViewModels.
- State hoisting — composables receive state and emit events, they don't own state.
- Use
remember for composable-local state, rememberSaveable for state surviving config changes.
- Keep composables small. Extract when a composable exceeds ~40 lines.
For navigation, theming, side effects, lists, animations see references/compose-patterns.md.
Android Concurrency
For coroutines language fundamentals (Flow, structured concurrency, cancellation), see the kotlin-development skill. Below are Android-specific patterns.
// ViewModel scope — cancelled when ViewModel is cleared
class UserViewModel(private val repo: UserRepository) : ViewModel() {
val users = repo.observeUsers()
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), emptyList())
}
// Lifecycle-aware collection in Compose
@Composable
fun UserScreen(viewModel: UserViewModel = viewModel()) {
val users by viewModel.users.collectAsStateWithLifecycle()
UserList(users)
}
// Lifecycle-aware collection in Activity/Fragment (legacy)
lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.STARTED) {
viewModel.users.collect { updateUI(it) }
}
}
Rules:
viewModelScope for ViewModel operations — auto-cancelled on clear.
collectAsStateWithLifecycle() in Compose — lifecycle-aware, stops collection when not visible.
repeatOnLifecycle in Activities/Fragments — restarts collection on lifecycle transitions.
WhileSubscribed(5000) for stateIn — keeps upstream active 5s after last subscriber (survives rotation).
- WorkManager for deferrable background work. Foreground services for user-visible ongoing tasks.
For WorkManager, foreground services, and advanced patterns see references/concurrency.md.
Architecture
Recommended: MVI (Model-View-Intent) for new projects. MVI enforces unidirectional data flow with a single immutable state and explicit user intents, which maps naturally to Compose. If the project already uses MVVM, MVP, or MVC — adapt to the existing architecture instead of forcing a rewrite.
MVI Pattern
// 1. State — single immutable data class per screen
data class HomeUiState(
val items: List<Item> = emptyList(),
val isLoading: Boolean = false,
val error: String? = null,
)
// 2. Intent — sealed interface of all user actions
sealed interface HomeIntent {
data object LoadItems : HomeIntent
data class DeleteItem(val id: String) : HomeIntent
data object RetryLoad : HomeIntent
}
// 3. ViewModel — reduces intents into state
@HiltViewModel
class HomeViewModel @Inject constructor(
private val repository: ItemRepository,
) : ViewModel() {
private val _uiState = MutableStateFlow(HomeUiState())
val uiState: StateFlow<HomeUiState> = _uiState.asStateFlow()
fun onIntent(intent: HomeIntent) {
when (intent) {
is HomeIntent.LoadItems -> loadItems()
is HomeIntent.DeleteItem -> deleteItem(intent.id)
is HomeIntent.RetryLoad -> loadItems()
}
}
private fun loadItems() {
viewModelScope.launch {
_uiState.update { it.copy(isLoading = true, error = null) }
repository.getItems()
.onSuccess { items -> _uiState.update { it.copy(items = items, isLoading = false) } }
.onFailure { e -> _uiState.update { it.copy(error = e.message, isLoading = false) } }
}
}
private fun deleteItem(id: String) {
viewModelScope.launch {
repository.delete(id)
_uiState.update { it.copy(items = it.items.filter { item -> item.id != id }) }
}
}
}
// 4. View — renders state, emits intents
@Composable
fun HomeScreen(viewModel: HomeViewModel = hiltViewModel()) {
val uiState by viewModel.uiState.collectAsStateWithLifecycle()
LaunchedEffect(Unit) { viewModel.onIntent(HomeIntent.LoadItems) }
when {
uiState.isLoading -> LoadingIndicator()
uiState.error != null -> ErrorScreen(
message = uiState.error!!,
viewModel.onIntent(HomeIntent.RetryLoad) },
)
else -> ItemList(
items = uiState.items,
id -> viewModel.onIntent(HomeIntent.DeleteItem(id)) },
)
}
}
Project Structure
app/
├── MainActivity.kt
├── navigation/
│ └── AppNavGraph.kt
├── feature/
│ ├── home/
│ │ ├── HomeScreen.kt
│ │ ├── HomeViewModel.kt
│ │ ├── HomeUiState.kt
│ │ └── HomeIntent.kt
│ ├── profile/
│ └── settings/
├── core/
│ ├── data/
│ │ ├── repository/
│ │ └── model/
│ ├── network/
│ └── database/
└── ui/
├── theme/
└── components/
Rules
- Organize by feature, not by technical layer.
- Unidirectional Data Flow (UDF) — intents flow up, state flows down.
- Single state per screen — one
UiState data class, one StateFlow.
- Explicit intents — all user actions are modeled as sealed interface members. No ad-hoc methods on ViewModel.
- Repository pattern — single source of truth for data. Repositories expose Flows.
- Use case classes (optional) — encapsulate complex business logic. Skip for simple CRUD.
- One screen composable per file. ViewModel per screen.
- Adapt to existing architecture. If the project uses MVVM/MVP/MVC, follow the established pattern. Propose MVI for new screens or new projects.
For multi-module architecture, Gradle setup, and build variants see references/project-structure.md.
Dependency Injection
// Hilt (recommended)
@HiltViewModel
class HomeViewModel @Inject constructor(
private val repository: ItemRepository,
) : ViewModel() { ... }
@Module
@InstallIn(SingletonComponent::class)
abstract class RepositoryModule {
@Binds
abstract fun bindItemRepository(impl: DefaultItemRepository): ItemRepository
}
// Manual DI (for small projects or libraries)
class AppContainer {
private val api: ApiService by lazy { RetrofitApiService() }
val repository: ItemRepository by lazy { DefaultItemRepository(api) }
}
Rules:
- Hilt for apps — standard Android DI, integrates with ViewModel, WorkManager, Navigation.
- Manual DI or Koin for libraries or KMP shared modules.
- Inject interfaces, not implementations.
- Use
@Singleton sparingly — scope to the narrowest lifecycle.
Testable Design
- Inject dependencies via constructor. ViewModels receive repositories, not context.
- Repository interfaces — swap real implementations with fakes in tests.
- UI state as data class — easy to assert in unit tests.
- Compose testing —
createComposeRule(), semantic matchers, onNodeWithText.
class HomeViewModelTest {
private val fakeRepository = FakeItemRepository()
private val viewModel = HomeViewModel(fakeRepository)
@Test
fun `load items updates state`() = runTest {
fakeRepository.emit(listOf(Item("1", "Test")))
viewModel.load()
assertEquals(listOf(Item("1", "Test")), viewModel.uiState.value.items)
}
}
Test naming: fun 'description of behavior'() with backtick syntax, or test_method_condition_expected().
Platform-Specific Guidance
The core skill covers Android phone by default. For other platforms, consult the corresponding reference:
| Platform |
Reference |
Key Topics |
| Tablet / Foldable |
references/tablet-patterns.md |
WindowSizeClass, adaptive layouts, multi-window, foldable postures |
| Wear OS |
references/wear-os-patterns.md |
Compose for Wear, Tiles, complications, Health Services |
| Google TV / Android TV |
references/tv-patterns.md |
Compose for TV, focus/D-pad navigation, Leanback |
| Car App Library |
references/car-app-library.md |
Shared API for Auto + AAOS: templates, lifecycle, testing |
| Android Auto |
references/android-auto-patterns.md |
Phone projection, Media3, messaging, DHU |
| Android Automotive |
references/android-automotive-patterns.md |
AAOS, car hardware, HVAC, multi-user, OEM |
| Meta Quest |
references/meta-quest-patterns.md |
Adapting APK for VR, spatial UI, entitlement, passthrough |
| Amazon Fire TV |
references/fire-tv-patterns.md |
Appstore, Amazon IAP, Alexa, missing GMS |
| Amazon Fire Tablets |
references/fire-tablet-patterns.md |
Device lineup, Show Mode, Kids Edition |
Quick Reference: Common Mistakes
| Mistake |
Fix |
| Collecting Flow without lifecycle awareness |
Use collectAsStateWithLifecycle() in Compose |
| Business logic in composables |
Move to ViewModel, expose as StateFlow |
mutableStateOf in ViewModel |
Use MutableStateFlow + asStateFlow() |
Passing Context to ViewModel |
Use AndroidViewModel only if truly needed, prefer abstractions |
LaunchedEffect(Unit) for one-time loads |
Consider loading in ViewModel init block |
| Hardcoded strings in composables |
Use stringResource(R.string.xxx) |
| Not handling process death |
Use SavedStateHandle in ViewModel, rememberSaveable in Compose |
| God ViewModel (500+ lines) |
Split by screen, extract use cases |
Not using Modifier parameter |
Always accept modifier: Modifier = Modifier as first optional param |
remember for complex objects |
Use remember with proper keys, or move to ViewModel |
| Ignoring configuration changes |
Test with rotation, dark mode, font scale |
Using GlobalScope |
Use viewModelScope or structured concurrency |
| View-based navigation in Compose app |
Use Compose Navigation (NavHost) |
LiveData in new code |
Use StateFlow + collectAsStateWithLifecycle() |
Magic numbers in UI (padding(16.dp), fontSize = 14.sp) |
Define design tokens (AppSpacing.medium) or use MaterialTheme tokens |
Overriding onBackPressed() |
onBackPressed() is no longer called on recent platform versions. Use BackHandler (Compose) or OnBackPressedDispatcher (Views). For custom back animations, use PredictiveBackHandler |
| Not supporting edge-to-edge |
Call enableEdgeToEdge() in onCreate. Edge-to-edge is mandatory on recent platform versions |
1---2name: google-app-development3description: This skill should be used when the user asks to "build an Android app", "create a Composable", "set up an Android project", "review Android code", "refactor Android", "add a screen", "create a Wear OS app", "build for Android TV", "build for Android Auto", "build for Android Automotive", "adapt for Meta Quest", "build for Fire TV", "build for Fire Tablet", "set up Room database", "add local storage", "use DataStore", "persist data", "add database migration", "encrypt storage", "add in-app purchases", "integrate Google Play Billing", "add subscriptions", "implement billing", "monetize app", "write Android tests", "test ViewModel", "test Composable", "set up Android testing", "add unit tests", "configure Android Lint", "add custom lint rule", "set up lint baseline", or when generating any Kotlin code targeting Google/Android platforms (including AOSP-based devices). Provides modern Jetpack Compose-first best practices covering UI patterns, app lifecycle, navigation, local storage, billing/payments, testing, And4---56# Google App Development (Jetpack Compose / Kotlin)78Modern best practices for building apps across Google platforms. Targets Jetpack Compose as the primary UI framework with Kotlin. Covers Android (phone), tablet, foldables, Wear OS, Google TV / Android TV, Android Auto, Android Automotive OS, and AOSP-based platforms (Meta Quest, Amazon Fire TV, Amazon Fire Tablets).910For Kotlin language fundamentals (null safety, coroutines, data modeling, error handling, idiomatic patterns), see the `kotlin-development` skill. This skill focuses on platform and framework patterns.1112## Important Rules1314- **Always generate Kotlin.** Only write Java when the project explicitly requires it (legacy codebase, Java-only API). See `references/java-interop.md` for bridging patterns.15- **Compose-first.** Use View-based UI only when Compose lacks the capability or the project has an existing View-based codebase. See `references/view-interop.md` for interop patterns.16- **Check the project context.** Before applying patterns, check the `minSdk`, Compose BOM version, and existing architecture. Adapt recommendations accordingly.1718## Reference Files1920- **`references/compose-patterns.md`** — Composables, state hoisting, recomposition, `remember`, `LazyColumn`, navigation, theming, Material 3, side effects21- **`references/concurrency.md`** — Coroutines in Android context: `viewModelScope`, `lifecycleScope`, `repeatOnLifecycle`, WorkManager, testing22- **`references/android-background-work.md`** — Foreground Services (types, permissions, restrictions), Background Limits (Doze, App Standby Buckets), AlarmManager (exact/inexact, permissions)23- **`references/android-lifecycle.md`** — Activity/Fragment lifecycle, `ViewModel`, saved state, process death, configuration changes24- **`references/view-interop.md`** — `AndroidView`, `ComposeView`, embedding Compose in Views and Views in Compose, migration strategies25- **`references/project-structure.md`** — Gradle setup, multi-module architecture, build variants, version catalogs, convention plugins26- **`references/tablet-patterns.md`** — Adaptive layouts, `WindowSizeClass` (`isWidthAtLeastBreakpoint`), multi-window, foldable devices, large screen guidelines27- **`references/wear-os-patterns.md`** — Compose for Wear OS, Tiles, complications, Health Services, watch face28- **`references/tv-patterns.md`** — Compose for TV, focus management, Leanback, D-pad navigation, Google TV / Android TV29- **`references/car-app-library.md`** — Car App Library shared API: CarAppService, Session, Screen, ScreenManager, templates, constraints, lifecycle, CarContext, testing30- **`references/android-auto-patterns.md`** — Android Auto phone projection: Media3 MediaLibraryService, messaging notifications, navigation/DHU, POI, distribution31- **`references/android-automotive-patterns.md`** — Android Automotive OS (AAOS): AAOS-specific Car App Library diffs, car hardware APIs, HVAC, system UI, multi-user, OEM customization, multi-zone audio32- **`references/java-interop.md`** — Calling Java from Kotlin, nullability annotations, SAM conversions, incremental migration33- **`references/meta-quest-patterns.md`** — Adapting Android APK for Meta Quest, spatial UI, entitlement check, VR input, passthrough34- **`references/local-storage.md`** — Room database (entities, DAOs, relations, migrations, testing), DataStore (Preferences and Proto), encrypted storage (Android Keystore, DataStore + Tink, SQLCipher), file storage (internal, scoped storage, FileProvider), storage selection guide35- **`references/networking-api.md`** — Retrofit, OkHttp, Ktor Client, JSON serialization, repository pattern, error handling, interceptors, certificate pinning, caching, connectivity, pagination (custom + Paging 3), file upload/download, testing36- **`references/fire-tv-patterns.md`** — Amazon Fire TV, Appstore, Amazon IAP, Alexa integration, missing Google Play Services37- **`references/media-playback.md`** — Media3 / ExoPlayer, MediaSession, audio focus, Picture-in-Picture, offline downloads, DRM, streaming formats, caching38- **`references/billing-payments.md`** — Google Play Billing Library (PBL 8), BillingClient, one-time purchases (consumable / non-consumable), subscriptions (base plans, offers, replacement modes), subscription offers (eligibility types, pricing phases, offer tags, developer-determined offers, winback offers, promo codes), purchase verification, RTDN, subscription lifecycle (grace period, account hold, pause), alternative billing, testing39- **`references/fire-tablet-patterns.md`** — Amazon Fire Tablets, device capabilities, Show Mode, Kids Edition, Special Offers40- **`references/testing.md`** — JUnit 5, MockK, Turbine, ViewModel testing, Compose UI testing, Robolectric, Room in-memory testing, Hilt testing, Espresso, coroutine testing (`runTest`, `TestDispatcher`), test architecture (pyramid, MVI/MVVM strategies), fakes vs mocks, CI integration41- **`references/code-quality.md`** — Android Lint configuration (`lint {}` block, `lint.xml`, severity levels), baseline management, suppression (`@SuppressLint`, `tools:ignore`), built-in check categories (correctness, security, performance, accessibility), Compose lint checks, custom lint rules (`Detector`, `Issue`, `IssueRegistry`), CI integration (SARIF, GitHub Code Scanning), multi-module convention plugin. For Detekt/ktlint, see `kotlin-development` skill's `references/static-analysis.md`4243## Code Style4445For Kotlin language style (naming, null safety, type annotations), follow the `kotlin-development` skill. Below are Android/Compose-specific conventions.4647- **Composable naming** — `PascalCase` for UI-emitting composables, `camelCase` for composables that return values.48- **Modifier parameter** — always the first optional parameter, default to `Modifier`.49- **Preview functions** — prefix with `Preview`, annotate with `@Preview`.50- **Define a custom app theme and apply it at the root.** Every app must have a custom `AppTheme` composable that wraps `MaterialTheme` with app-specific colors, typography, and shapes. Apply it once at the top level (`setContent { AppTheme { ... } }`). All UI code must reference theme tokens — never raw values.5152```kotlin53// 1. Define custom theme (once, in ui/theme/)54@Composable55fun AppTheme(56 darkTheme: Boolean = isSystemInDarkTheme(),57 content: @Composable () -> Unit,58) {59 val colorScheme = if (darkTheme) AppDarkColorScheme else AppLightColorScheme60 MaterialTheme(61 colorScheme = colorScheme,62 typography = AppTypography,63 shapes = AppShapes,64 content = content,65 )66}6768// 2. Apply at root (once)69setContent { AppTheme { AppNavGraph() } }7071// 3. Use theme tokens everywhere — never raw values72Text(73 text = "Hello",74 style = MaterialTheme.typography.bodyMedium, // not fontSize = 14.sp75 color = MaterialTheme.colorScheme.onSurface, // not Color(0xFF1C1B1F)76)77Card(shape = MaterialTheme.shapes.medium) { ... } // not RoundedCornerShape(8.dp)78```7980For values not covered by Material tokens (spacing, elevation, icon sizes), define app-level design tokens:8182```kotlin83object AppSpacing {84 val small = 8.dp85 val medium = 16.dp86 val large = 24.dp87}88```8990**No magic numbers in UI code.** If a numeric value appears in UI, it must be either a Material theme token or an app-defined design constant. For theming details see `references/compose-patterns.md`.9192## Naming Conventions (Android-Specific)9394| Element | Convention | Example |95|---|---|---|96| Composable (UI) | `PascalCase` | `UserProfileScreen`, `SettingsCard` |97| Composable (value) | `camelCase` | `rememberScrollState()` |98| ViewModel | Suffix `ViewModel` | `SettingsViewModel` |99| Screen composable | Suffix `Screen` | `HomeScreen`, `ProfileScreen` |100| UI State | Suffix `UiState` | `HomeUiState`, `ProfileUiState` |101| Activity | Suffix `Activity` | `MainActivity` |102| Fragment (legacy) | Suffix `Fragment` | `HomeFragment` |103| Repository | Suffix `Repository` | `UserRepository` |104| Use case | Verb phrase | `GetUserUseCase`, `SyncDataUseCase` |105| Module | Feature name, kebab-case | `:feature:auth`, `:core:network` |106107## Jetpack Compose Essentials108109```kotlin110// UI State111data class HomeUiState(112 val items: List<Item> = emptyList(),113 val isLoading: Boolean = false,114 val error: String? = null,115)116117// ViewModel118class HomeViewModel(119 private val repository: ItemRepository,120) : ViewModel() {121 private val _uiState = MutableStateFlow(HomeUiState())122 val uiState: StateFlow<HomeUiState> = _uiState.asStateFlow()123124 fun load() {125 viewModelScope.launch {126 _uiState.update { it.copy(isLoading = true) }127 repository.getItems()128 .onSuccess { items -> _uiState.update { it.copy(items = items, isLoading = false) } }129 .onFailure { e -> _uiState.update { it.copy(error = e.message, isLoading = false) } }130 }131 }132}133134// Composable135@Composable136fun HomeScreen(137 viewModel: HomeViewModel = viewModel(),138 onItemClick: (Item) -> Unit,139) {140 val uiState by viewModel.uiState.collectAsStateWithLifecycle()141142 when {143 uiState.isLoading -> LoadingIndicator()144 uiState.error != null -> ErrorMessage(uiState.error!!)145 else -> ItemList(items = uiState.items, onItemClick = onItemClick)146 }147}148```149150Rules:151- Use `StateFlow` + `collectAsStateWithLifecycle()` for state in ViewModels.152- State hoisting — composables receive state and emit events, they don't own state.153- Use `remember` for composable-local state, `rememberSaveable` for state surviving config changes.154- Keep composables small. Extract when a composable exceeds ~40 lines.155156For navigation, theming, side effects, lists, animations see `references/compose-patterns.md`.157158## Android Concurrency159160For coroutines language fundamentals (Flow, structured concurrency, cancellation), see the `kotlin-development` skill. Below are Android-specific patterns.161162```kotlin163// ViewModel scope — cancelled when ViewModel is cleared164class UserViewModel(private val repo: UserRepository) : ViewModel() {165 val users = repo.observeUsers()166 .stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), emptyList())167}168169// Lifecycle-aware collection in Compose170@Composable171fun UserScreen(viewModel: UserViewModel = viewModel()) {172 val users by viewModel.users.collectAsStateWithLifecycle()173 UserList(users)174}175176// Lifecycle-aware collection in Activity/Fragment (legacy)177lifecycleScope.launch {178 repeatOnLifecycle(Lifecycle.State.STARTED) {179 viewModel.users.collect { updateUI(it) }180 }181}182```183184Rules:185- **`viewModelScope`** for ViewModel operations — auto-cancelled on clear.186- **`collectAsStateWithLifecycle()`** in Compose — lifecycle-aware, stops collection when not visible.187- **`repeatOnLifecycle`** in Activities/Fragments — restarts collection on lifecycle transitions.188- **`WhileSubscribed(5000)`** for `stateIn` — keeps upstream active 5s after last subscriber (survives rotation).189- **WorkManager** for deferrable background work. **Foreground services** for user-visible ongoing tasks.190191For WorkManager, foreground services, and advanced patterns see `references/concurrency.md`.192193## Architecture194195**Recommended: MVI (Model-View-Intent)** for new projects. MVI enforces unidirectional data flow with a single immutable state and explicit user intents, which maps naturally to Compose. If the project already uses MVVM, MVP, or MVC — adapt to the existing architecture instead of forcing a rewrite.196197### MVI Pattern198199```kotlin200// 1. State — single immutable data class per screen201data class HomeUiState(202 val items: List<Item> = emptyList(),203 val isLoading: Boolean = false,204 val error: String? = null,205)206207// 2. Intent — sealed interface of all user actions208sealed interface HomeIntent {209 data object LoadItems : HomeIntent210 data class DeleteItem(val id: String) : HomeIntent211 data object RetryLoad : HomeIntent212}213214// 3. ViewModel — reduces intents into state215@HiltViewModel216class HomeViewModel @Inject constructor(217 private val repository: ItemRepository,218) : ViewModel() {219 private val _uiState = MutableStateFlow(HomeUiState())220 val uiState: StateFlow<HomeUiState> = _uiState.asStateFlow()221222 fun onIntent(intent: HomeIntent) {223 when (intent) {224 is HomeIntent.LoadItems -> loadItems()225 is HomeIntent.DeleteItem -> deleteItem(intent.id)226 is HomeIntent.RetryLoad -> loadItems()227 }228 }229230 private fun loadItems() {231 viewModelScope.launch {232 _uiState.update { it.copy(isLoading = true, error = null) }233 repository.getItems()234 .onSuccess { items -> _uiState.update { it.copy(items = items, isLoading = false) } }235 .onFailure { e -> _uiState.update { it.copy(error = e.message, isLoading = false) } }236 }237 }238239 private fun deleteItem(id: String) {240 viewModelScope.launch {241 repository.delete(id)242 _uiState.update { it.copy(items = it.items.filter { item -> item.id != id }) }243 }244 }245}246247// 4. View — renders state, emits intents248@Composable249fun HomeScreen(viewModel: HomeViewModel = hiltViewModel()) {250 val uiState by viewModel.uiState.collectAsStateWithLifecycle()251252 LaunchedEffect(Unit) { viewModel.onIntent(HomeIntent.LoadItems) }253254 when {255 uiState.isLoading -> LoadingIndicator()256 uiState.error != null -> ErrorScreen(257 message = uiState.error!!,258 onRetry = { viewModel.onIntent(HomeIntent.RetryLoad) },259 )260 else -> ItemList(261 items = uiState.items,262 onDelete = { id -> viewModel.onIntent(HomeIntent.DeleteItem(id)) },263 )264 }265}266```267268### Project Structure269270```271app/272├── MainActivity.kt273├── navigation/274│ └── AppNavGraph.kt275├── feature/276│ ├── home/277│ │ ├── HomeScreen.kt278│ │ ├── HomeViewModel.kt279│ │ ├── HomeUiState.kt280│ │ └── HomeIntent.kt281│ ├── profile/282│ └── settings/283├── core/284│ ├── data/285│ │ ├── repository/286│ │ └── model/287│ ├── network/288│ └── database/289└── ui/290 ├── theme/291 └── components/292```293294### Rules295296- Organize by feature, not by technical layer.297- **Unidirectional Data Flow (UDF)** — intents flow up, state flows down.298- **Single state per screen** — one `UiState` data class, one `StateFlow`.299- **Explicit intents** — all user actions are modeled as sealed interface members. No ad-hoc methods on ViewModel.300- **Repository pattern** — single source of truth for data. Repositories expose Flows.301- **Use case classes** (optional) — encapsulate complex business logic. Skip for simple CRUD.302- One screen composable per file. ViewModel per screen.303- **Adapt to existing architecture.** If the project uses MVVM/MVP/MVC, follow the established pattern. Propose MVI for new screens or new projects.304305For multi-module architecture, Gradle setup, and build variants see `references/project-structure.md`.306307## Dependency Injection308309```kotlin310// Hilt (recommended)311@HiltViewModel312class HomeViewModel @Inject constructor(313 private val repository: ItemRepository,314) : ViewModel() { ... }315316@Module317@InstallIn(SingletonComponent::class)318abstract class RepositoryModule {319 @Binds320 abstract fun bindItemRepository(impl: DefaultItemRepository): ItemRepository321}322323// Manual DI (for small projects or libraries)324class AppContainer {325 private val api: ApiService by lazy { RetrofitApiService() }326 val repository: ItemRepository by lazy { DefaultItemRepository(api) }327}328```329330Rules:331- **Hilt** for apps — standard Android DI, integrates with ViewModel, WorkManager, Navigation.332- **Manual DI or Koin** for libraries or KMP shared modules.333- Inject interfaces, not implementations.334- Use `@Singleton` sparingly — scope to the narrowest lifecycle.335336## Testable Design337338- **Inject dependencies** via constructor. ViewModels receive repositories, not context.339- **Repository interfaces** — swap real implementations with fakes in tests.340- **UI state as data class** — easy to assert in unit tests.341- **Compose testing** — `createComposeRule()`, semantic matchers, `onNodeWithText`.342343```kotlin344class HomeViewModelTest {345 private val fakeRepository = FakeItemRepository()346 private val viewModel = HomeViewModel(fakeRepository)347348 @Test349 fun `load items updates state`() = runTest {350 fakeRepository.emit(listOf(Item("1", "Test")))351 viewModel.load()352 assertEquals(listOf(Item("1", "Test")), viewModel.uiState.value.items)353 }354}355```356357Test naming: `fun 'description of behavior'()` with backtick syntax, or `test_method_condition_expected()`.358359## Platform-Specific Guidance360361The core skill covers Android phone by default. For other platforms, consult the corresponding reference:362363| Platform | Reference | Key Topics |364|---|---|---|365| Tablet / Foldable | `references/tablet-patterns.md` | `WindowSizeClass`, adaptive layouts, multi-window, foldable postures |366| Wear OS | `references/wear-os-patterns.md` | Compose for Wear, Tiles, complications, Health Services |367| Google TV / Android TV | `references/tv-patterns.md` | Compose for TV, focus/D-pad navigation, Leanback |368| Car App Library | `references/car-app-library.md` | Shared API for Auto + AAOS: templates, lifecycle, testing |369| Android Auto | `references/android-auto-patterns.md` | Phone projection, Media3, messaging, DHU |370| Android Automotive | `references/android-automotive-patterns.md` | AAOS, car hardware, HVAC, multi-user, OEM |371| Meta Quest | `references/meta-quest-patterns.md` | Adapting APK for VR, spatial UI, entitlement, passthrough |372| Amazon Fire TV | `references/fire-tv-patterns.md` | Appstore, Amazon IAP, Alexa, missing GMS |373| Amazon Fire Tablets | `references/fire-tablet-patterns.md` | Device lineup, Show Mode, Kids Edition |374375## Quick Reference: Common Mistakes376377| Mistake | Fix |378|---|---|379| Collecting Flow without lifecycle awareness | Use `collectAsStateWithLifecycle()` in Compose |380| Business logic in composables | Move to ViewModel, expose as StateFlow |381| `mutableStateOf` in ViewModel | Use `MutableStateFlow` + `asStateFlow()` |382| Passing `Context` to ViewModel | Use `AndroidViewModel` only if truly needed, prefer abstractions |383| `LaunchedEffect(Unit)` for one-time loads | Consider loading in ViewModel `init` block |384| Hardcoded strings in composables | Use `stringResource(R.string.xxx)` |385| Not handling process death | Use `SavedStateHandle` in ViewModel, `rememberSaveable` in Compose |386| God ViewModel (500+ lines) | Split by screen, extract use cases |387| Not using `Modifier` parameter | Always accept `modifier: Modifier = Modifier` as first optional param |388| `remember` for complex objects | Use `remember` with proper keys, or move to ViewModel |389| Ignoring configuration changes | Test with rotation, dark mode, font scale |390| Using `GlobalScope` | Use `viewModelScope` or structured concurrency |391| View-based navigation in Compose app | Use Compose Navigation (`NavHost`) |392| `LiveData` in new code | Use `StateFlow` + `collectAsStateWithLifecycle()` |393| Magic numbers in UI (`padding(16.dp)`, `fontSize = 14.sp`) | Define design tokens (`AppSpacing.medium`) or use `MaterialTheme` tokens |394| Overriding `onBackPressed()` | `onBackPressed()` is no longer called on recent platform versions. Use `BackHandler` (Compose) or `OnBackPressedDispatcher` (Views). For custom back animations, use `PredictiveBackHandler` |395| Not supporting edge-to-edge | Call `enableEdgeToEdge()` in `onCreate`. Edge-to-edge is mandatory on recent platform versions |