Jetpack Compose-first Kotlin patterns for Google/Android platforms: phone, tablet/foldable, Wear OS, Android TV/Google TV, Android Auto, Android Automotive, and AOSP devices (Meta Quest, Fire TV, Fire Tablet). Use whenever the user asks to "build an Android app", "create a Composable" or "add a screen", "set up an Android project", "review/refactor Android code", "build for Wear OS / TV / Auto / Automotive / Quest / Fire", "set up Room, DataStore, or local storage" (including migrations and encryption), "add in-app purchases / subscriptions / Play Billing", "write Android tests" (ViewModel, Composable, Room), "configure Android Lint", or when generating any Kotlin code that targets Android — even if they only mention Jetpack, Compose, or ViewModel. Covers UI, lifecycle, navigation, concurrency, storage, networking, media, billing, testing, Lint, and per-platform guidance. Pairs with `kotlin-development` for language fundamentals. Always generates Kotlin unless the project explicitly requires Java.
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.
references/java-interop.md for bridging patterns.references/view-interop.md for interop patterns.minSdk, Compose BOM version, and existing architecture. Adapt recommendations accordingly.<latest> (inside quoted version strings) and <latest-stable-api> (bare, e.g. compileSdk = <latest-stable-api>) are fill-in markers, not literal values. Replace them with the current stable version / API level before emitting code; the bare form is not valid Kotlin.kotlin-development skill is also installed and defers Kotlin language topics (null safety, coroutines fundamentals, Detekt/ktlint, generic Gradle layout) to it. If that skill is not available, fall back to standard idiomatic Kotlin and the official Kotlin docs instead of following the dangling pointers.references/compose-patterns.md — Composables, state hoisting, recomposition, remember, LazyColumn, navigation, theming, Material 3, side effectsreferences/concurrency.md — Coroutines in Android context: viewModelScope, lifecycleScope, repeatOnLifecycle, WorkManager, testingreferences/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 changesreferences/view-interop.md — AndroidView, ComposeView, embedding Compose in Views and Views in Compose, migration strategiesreferences/project-structure.md — Gradle setup, multi-module architecture, build variants, version catalogs, convention pluginsreferences/tablet-patterns.md — Adaptive layouts, WindowSizeClass (isWidthAtLeastBreakpoint), multi-window, foldable devices, large screen guidelinesreferences/wear-os-patterns.md — Compose for Wear OS, Tiles, complications, Health Services, watch facereferences/tv-patterns.md — Compose for TV, focus management, Leanback, D-pad navigation, Google TV / Android TVreferences/car-app-library.md — Car App Library shared API: CarAppService, Session, Screen, ScreenManager, templates, constraints, lifecycle, CarContext, testingreferences/android-auto-patterns.md — Android Auto phone projection: Media3 MediaLibraryService, messaging notifications, navigation/DHU, POI, distributionreferences/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 audioreferences/java-interop.md — Calling Java from Kotlin, nullability annotations, SAM conversions, incremental migrationreferences/meta-quest-patterns.md — Adapting Android APK for Meta Quest, spatial UI, entitlement check, VR input, passthroughreferences/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 guidereferences/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, testingreferences/fire-tv-patterns.md — Amazon Fire TV, Appstore, Amazon IAP, Alexa integration, missing Google Play Servicesreferences/media-playback.md — Media3 / ExoPlayer, MediaSession, audio focus, Picture-in-Picture, offline downloads, DRM, streaming formats, cachingreferences/billing-payments.md — Google Play Billing Library, 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, testingreferences/fire-tablet-patterns.md — Amazon Fire Tablets, device capabilities, Show Mode, Kids Edition, Special Offersreferences/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 integrationreferences/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.mdFor Kotlin language style (naming, null safety, type annotations), follow the kotlin-development skill. Below are Android/Compose-specific conventions.
PascalCase for UI-emitting composables, camelCase for composables that return values.Modifier.Preview, annotate with @Preview.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 production UI code. When a numeric value appears in app UI, prefer a Material theme token or an app-defined design constant so spacing and sizing stay consistent and changeable in one place. The snippets in this skill's reference files use literal dp/sp values for brevity — treat those as illustrations of the API, not as a licence to hardcode in generated code. For theming details see references/compose-patterns.md.
| 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 |
// 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()
init {
load() // initial load belongs to the ViewModel, not to a LaunchedEffect in the UI
}
private 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, onItemClick = onItemClick)
}
}Rules:
StateFlow + collectAsStateWithLifecycle() for state in ViewModels.remember for composable-local state, rememberSaveable for state surviving config changes.@Serializable route classes, composable<Route>, navigate(Route(...))) — not string routes. Every navigation snippet in this skill follows that style.For navigation, theming, side effects, lists, animations see references/compose-patterns.md.
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).For WorkManager and advanced coroutine patterns see references/concurrency.md; for foreground services, background limits (Doze, App Standby) and AlarmManager see references/android-background-work.md.
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.
// 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 Refresh : 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()
init {
loadItems() // initial load happens once per ViewModel, surviving recomposition and rotation
}
fun onIntent(intent: HomeIntent) {
when (intent) {
is HomeIntent.Refresh -> 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()
when {
uiState.isLoading -> LoadingIndicator()
uiState.error != null -> ErrorScreen(
message = uiState.error!!,
onRetry = { viewModel.onIntent(HomeIntent.RetryLoad) },
)
else -> ItemList(
items = uiState.items,
onDelete = { id -> viewModel.onIntent(HomeIntent.DeleteItem(id)) },
)
}
}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/UiState data class, one StateFlow.For multi-module architecture, Gradle setup, and build variants see references/project-structure.md.
// 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:
@Singleton sparingly — scope to the narrowest lifecycle.createComposeRule(), semantic matchers, onNodeWithText.class HomeViewModelTest {
private val fakeRepository = FakeItemRepository()
@Test
fun `initial load populates items`() = runTest {
fakeRepository.emit(listOf(Item("1", "Test")))
val viewModel = HomeViewModel(fakeRepository) // loads in init
assertEquals(listOf(Item("1", "Test")), viewModel.uiState.value.items)
}
}Test naming: fun 'description of behavior'() with backtick syntax, or test_method_condition_expected().
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 |
| 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 |
provectus/awos-recruitment
ce6a3ea
If you maintain this skill, you can claim it as your own. Once claimed, you can manage eval scenarios, bundle related skills, attach documentation or rules, and ensure cross-agent compatibility.