mobile-best-practices 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (68) hide show
  1. package/README.md +64 -0
  2. package/assets/data/anti-patterns.csv +114 -0
  3. package/assets/data/architectures.csv +50 -0
  4. package/assets/data/code-snippets.csv +80 -0
  5. package/assets/data/gradle-deps.csv +79 -0
  6. package/assets/data/libraries.csv +102 -0
  7. package/assets/data/performance.csv +229 -0
  8. package/assets/data/platforms/android.csv +247 -0
  9. package/assets/data/platforms/flutter.csv +55 -0
  10. package/assets/data/platforms/ios.csv +61 -0
  11. package/assets/data/platforms/react-native.csv +56 -0
  12. package/assets/data/project-templates.csv +19 -0
  13. package/assets/data/reasoning-rules.csv +57 -0
  14. package/assets/data/security.csv +438 -0
  15. package/assets/data/testing.csv +74 -0
  16. package/assets/data/ui-patterns.csv +92 -0
  17. package/assets/references/CHECKLIST.md +49 -0
  18. package/assets/references/CODE-RULES.md +123 -0
  19. package/assets/scripts/__pycache__/core.cpython-314.pyc +0 -0
  20. package/assets/scripts/core.py +432 -0
  21. package/assets/scripts/search.py +104 -0
  22. package/assets/skills/all.md +245 -0
  23. package/assets/skills/android.md +168 -0
  24. package/assets/skills/flutter.md +153 -0
  25. package/assets/skills/ios.md +149 -0
  26. package/assets/skills/react-native.md +154 -0
  27. package/assets/templates/base/quick-reference.md +41 -0
  28. package/assets/templates/base/skill-content.md +60 -0
  29. package/assets/templates/platforms/agent.json +11 -0
  30. package/assets/templates/platforms/antigravity.json +13 -0
  31. package/assets/templates/platforms/claude.json +27 -0
  32. package/assets/templates/platforms/codebuddy.json +11 -0
  33. package/assets/templates/platforms/codex.json +11 -0
  34. package/assets/templates/platforms/continue.json +11 -0
  35. package/assets/templates/platforms/copilot.json +11 -0
  36. package/assets/templates/platforms/cursor.json +11 -0
  37. package/assets/templates/platforms/gemini.json +11 -0
  38. package/assets/templates/platforms/kiro.json +11 -0
  39. package/assets/templates/platforms/opencode.json +11 -0
  40. package/assets/templates/platforms/qoder.json +11 -0
  41. package/assets/templates/platforms/roocode.json +11 -0
  42. package/assets/templates/platforms/trae.json +11 -0
  43. package/assets/templates/platforms/windsurf.json +11 -0
  44. package/dist/commands/init.d.ts +6 -0
  45. package/dist/commands/init.d.ts.map +1 -0
  46. package/dist/commands/init.js +94 -0
  47. package/dist/commands/init.js.map +1 -0
  48. package/dist/commands/update.d.ts +2 -0
  49. package/dist/commands/update.d.ts.map +1 -0
  50. package/dist/commands/update.js +28 -0
  51. package/dist/commands/update.js.map +1 -0
  52. package/dist/commands/versions.d.ts +2 -0
  53. package/dist/commands/versions.d.ts.map +1 -0
  54. package/dist/commands/versions.js +30 -0
  55. package/dist/commands/versions.js.map +1 -0
  56. package/dist/index.d.ts +3 -0
  57. package/dist/index.d.ts.map +1 -0
  58. package/dist/index.js +27 -0
  59. package/dist/index.js.map +1 -0
  60. package/dist/types/index.d.ts +23 -0
  61. package/dist/types/index.d.ts.map +1 -0
  62. package/dist/types/index.js +24 -0
  63. package/dist/types/index.js.map +1 -0
  64. package/dist/utils/index.d.ts +9 -0
  65. package/dist/utils/index.d.ts.map +1 -0
  66. package/dist/utils/index.js +103 -0
  67. package/dist/utils/index.js.map +1 -0
  68. package/package.json +57 -0
@@ -0,0 +1,247 @@
1
+ Category,Guideline,Description,Do,Dont,Code Good,Code Bad,Severity,Docs URL
2
+ Compose,State Hoisting,Hoist state to caller for reusable composables,Hoist state to caller pass via parameters,Put state inside low-level composables,fun Counter(count: Int onIncrement: () -> Unit),fun Counter() { var count by remember { mutableStateOf(0) } },High,https://developer.android.com/develop/ui/compose/state#state-hoisting
3
+ Compose,rememberSaveable,Use rememberSaveable to survive config changes,rememberSaveable for user input and visible state,remember for state that must survive rotation,"val text by rememberSaveable { mutableStateOf("""") }","val text by remember { mutableStateOf("""") }",High,https://developer.android.com/develop/ui/compose/state#restore-ui-state
4
+ Compose,LazyColumn Keys,Always provide stable keys for LazyColumn items,Use unique ID from data model as key,Use index or no key,LazyColumn { items(list key = { it.id }) { } },LazyColumn { items(list) { } },High,https://developer.android.com/develop/ui/compose/lists#item-keys
5
+ Compose,Stable Annotations,Mark classes @Stable or @Immutable for skip optimization,Use @Immutable for truly immutable data classes,Pass mutable classes to composables,@Immutable data class UiState(val items: ImmutableList<Item>),data class UiState(val items: List<Item>) // unstable,Medium,https://developer.android.com/develop/ui/compose/performance/stability
6
+ Compose,derivedStateOf,Use derivedStateOf for computed values from other states,Wrap expensive computations depending on state,Compute in composition on every recomposition,val filtered by remember { derivedStateOf { list.filter { it.active } } },val filtered = list.filter { it.active } // recomputes every recomposition,Medium,https://developer.android.com/develop/ui/compose/side-effects#derivedstateof
7
+ Compose,Modifier Order,Modifier order matters - applied outside-in,Apply clickable before padding for larger touch target,Random modifier ordering,Modifier.clickable { }.padding(16.dp),Modifier.padding(16.dp).clickable { } // small touch target,Medium,https://developer.android.com/develop/ui/compose/modifiers#order-matters
8
+ Compose,Side Effects,Use LaunchedEffect/DisposableEffect for side effects,LaunchedEffect for coroutines DisposableEffect for cleanup,Launch coroutines in composition directly,LaunchedEffect(key) { viewModel.load() },rememberCoroutineScope().launch { } // in composition,High,https://developer.android.com/develop/ui/compose/side-effects
9
+ Compose,CompositionLocal,Use CompositionLocal sparingly for implicit data,Use for theme-level or widely-used dependencies,Use for passing regular business data,val LocalAnalytics = staticCompositionLocalOf<Analytics> { error('') },CompositionLocal for every dependency,Low,https://developer.android.com/develop/ui/compose/compositionlocal
10
+ Compose,Slot APIs,Use slot APIs (content lambdas) for flexible composables,Accept @Composable content parameters,Hardcode composable content,fun Card(content: @Composable () -> Unit) { Surface { content() } },fun Card(title: String body: String) // inflexible,Medium,https://developer.android.com/develop/ui/compose/layouts/basics
11
+ Compose,Preview,Write Compose previews for rapid iteration,Create previews with different states and themes,Skip previews for composables,@Preview @Composable fun MyScreenPreview() { MyScreen(UiState.Success(mock)) },No previews only test on device,Low,https://developer.android.com/develop/ui/compose/tooling/previews
12
+ Architecture,ViewModel Scope,Use viewModelScope for coroutines in ViewModel,Launch in viewModelScope auto-cancelled on clear,Create custom CoroutineScope in ViewModel,viewModelScope.launch { repo.fetchData() },CoroutineScope(Dispatchers.Main) // not cancelled,High,https://developer.android.com/topic/libraries/architecture/viewmodel
13
+ Architecture,StateFlow over LiveData,Prefer StateFlow over LiveData in new code,Use StateFlow with collectAsStateWithLifecycle,Use LiveData in Compose projects,val state: StateFlow<UiState> = _state.asStateFlow(),val state: LiveData<UiState> = _state,Medium,https://developer.android.com/kotlin/flow/stateflow-and-sharedflow
14
+ Architecture,Single Activity,Use single Activity with Navigation Compose,One Activity multiple composable destinations,Multiple Activities for each screen,NavHost(navController startDest = 'home') { composable('home') {} },startActivity(Intent(this DetailActivity::class.java)),Medium,https://developer.android.com/guide/navigation/get-started
15
+ Architecture,Hilt ViewModel Injection,Inject dependencies into ViewModel via Hilt,Use @HiltViewModel with @Inject constructor,Create ViewModel with manual factory,@HiltViewModel class MyVM @Inject constructor(val repo: Repo) : ViewModel(),class MyVM : ViewModel() { val repo = Repo(ApiService()) },High,https://developer.android.com/training/dependency-injection/hilt-android
16
+ Architecture,Repository Pattern,Abstract data sources behind Repository interface,Repository mediates between data sources and domain,ViewModel directly calls API or database,class UserRepo(val api: Api val db: Db) { suspend fun getUser(id) = db.get(id) ?: api.fetch(id) },class MyVM { val data = Retrofit.create(Api::class).getData() },High,https://developer.android.com/topic/architecture/data-layer
17
+ Architecture,Sealed Interface for State,Use sealed interface for UI state modeling,Represent all possible states explicitly,Use nullable booleans for state,sealed interface UiState { data object Loading; data class Success(val data: List<Item>); data class Error(val msg: String) },data class UiState(val data: List<Item>? val loading: Boolean val error: String?),High,
18
+ Architecture,SavedStateHandle,Use SavedStateHandle for process death survival,Save minimal UI state in SavedStateHandle,Rely only on ViewModel surviving,class MyVM(saved: SavedStateHandle) { val query = saved.getStateFlow('query' '') },class MyVM { var query = '' } // lost on process death,Medium,https://developer.android.com/topic/libraries/architecture/viewmodel/viewmodel-savedstate
19
+ Architecture,Multi-Module Structure,Organize large projects into feature modules,feature/ core/ app/ modules with clear boundaries,Single monolithic app module,feature-home/ feature-auth/ core-network/ core-database/ app/,app/src/main/java/com/myapp/everything/,Medium,https://developer.android.com/topic/modularization
20
+ Coroutines,Dispatchers Usage,Use correct dispatcher for the work type,IO for network/disk Main for UI Default for CPU,Use Dispatchers.Main for everything,withContext(Dispatchers.IO) { repo.fetchData() },GlobalScope.launch(Dispatchers.Main) { api.fetch() },High,https://developer.android.com/kotlin/coroutines/coroutines-best-practices
21
+ Coroutines,Structured Concurrency,Use structured concurrency never GlobalScope,Use viewModelScope lifecycleScope or coroutineScope,Use GlobalScope for fire-and-forget,viewModelScope.launch { loadData() },GlobalScope.launch { loadData() } // leaks,Critical,https://developer.android.com/kotlin/coroutines/coroutines-best-practices
22
+ Coroutines,Exception Handling,Handle coroutine exceptions properly,Use try-catch or CoroutineExceptionHandler,Let exceptions crash silently,try { api.fetch() } catch (e: Exception) { emit(Error(e.message)) },launch { api.fetch() } // exception swallowed,High,
23
+ Coroutines,Flow Collection,Collect Flow lifecycle-aware in Compose,Use collectAsStateWithLifecycle,Use collectAsState without lifecycle,val state by viewModel.state.collectAsStateWithLifecycle(),val state by viewModel.state.collectAsState() // collects invisible,High,https://developer.android.com/kotlin/flow/stateflow-and-sharedflow
24
+ Coroutines,Cancellation,Respect coroutine cancellation properly,Check isActive use ensureActive() in loops,Ignore cancellation in long work,while (isActive) { processNextItem() },while (true) { processNextItem() } // not cancellable,Medium,
25
+ Lifecycle,Lifecycle-aware Collection,Collect flows respecting lifecycle,Use repeatOnLifecycle or flowWithLifecycle,Collect in onCreate without lifecycle,lifecycleScope.launch { repeatOnLifecycle(STARTED) { flow.collect {} } },lifecycleScope.launch { flow.collect {} } // background,Critical,https://developer.android.com/topic/libraries/architecture/coroutines
26
+ Lifecycle,Configuration Change,Handle configuration changes properly,Use ViewModel + rememberSaveable,Manually handle configChanges in manifest,ViewModel survives; rememberSaveable for UI state,android:configChanges='orientation' // blocks handling,High,https://developer.android.com/guide/topics/resources/runtime-changes
27
+ Lifecycle,Process Death,Handle process death restoration,Save state in SavedStateHandle and restore,Assume ViewModel always survives,SavedStateHandle for nav args and transient state,Only ViewModel without SavedStateHandle,Medium,https://developer.android.com/topic/libraries/architecture/saving-states
28
+ Lifecycle,onCleared Cleanup,Clean up resources in ViewModel onCleared,Cancel jobs close connections in onCleared,Let resources leak after ViewModel destroyed,override fun onCleared() { connection.close() },No cleanup in onCleared,High,
29
+ Lifecycle,BackHandler,Handle predictive back gesture properly,Use BackHandler composable for custom back,Override onBackPressed in Activity,BackHandler(enabled = showDialog) { showDialog = false },override fun onBackPressed() {} // deprecated,Medium,https://developer.android.com/develop/ui/views/touch-and-input/gestures/predictive-back
30
+ Performance,Baseline Profile,Generate Baseline Profiles for startup,Use Macrobenchmark to generate include in app,Skip baseline profiles for release,profileinstaller + macrobenchmark BaselineProfileRule,No baseline profile module,High,https://developer.android.com/topic/performance/baselineprofiles
31
+ Performance,R8 Full Mode,Enable R8 full mode for max optimization,Set android.enableR8.fullMode=true,Use default R8 mode only,android.enableR8.fullMode=true in gradle.properties,Default R8 without full mode,Medium,https://developer.android.com/build/shrink-code
32
+ Performance,Strict Mode,Use StrictMode in debug to detect issues,Enable disk/network on main thread detection,Run StrictMode in release,StrictMode.setThreadPolicy(detectAll().penaltyLog().build()),No StrictMode configured,Medium,https://developer.android.com/reference/android/os/StrictMode
33
+ Performance,Compose Stability,Check Compose compiler stability report,Generate stability report fix unstable params,Ignore stability warnings,composeCompiler { reportsDestination = buildDir('compose_reports') },No stability analysis,Medium,https://developer.android.com/develop/ui/compose/performance/stability
34
+ Performance,Image Loading Size,Load images at display size not original,Use Coil/Glide size transformations,Load full-res images into small views,AsyncImage(model = ImageRequest.Builder(ctx).data(url).size(200).build()),AsyncImage(model = url) // loads full size,High,
35
+ Performance,ProGuard Rules,Write correct ProGuard/R8 rules,Keep serialization models add rules for reflection,Keep everything or nothing,-keep class com.myapp.model.** { *; },-keep class ** { *; } // keeps everything,Medium,https://developer.android.com/build/shrink-code
36
+ Performance,Lazy Performance,Optimize LazyColumn/LazyRow performance,Use key contentType avoid heavy items,No keys no contentType,LazyColumn { items(list key = { it.id } contentType = { it.type }) {} },LazyColumn { items(list) { HeavyComposable() } },High,https://developer.android.com/develop/ui/compose/lists
37
+ Testing,Compose Test Rule,Use ComposeTestRule for UI tests,Set up test rule with createComposeRule,Test compose with Espresso only,@get:Rule val rule = createComposeRule(); rule.setContent { MyScreen() },ActivityScenarioRule for Compose tests,Medium,https://developer.android.com/develop/ui/compose/testing
38
+ Testing,Coroutine Test,Use runTest for testing coroutines,Use Turbine for Flow TestDispatcher for time,Test with Thread.sleep or delay,runTest { viewModel.state.test { assertEquals(Loading awaitItem()) } },runBlocking { delay(1000); assertEquals(expected) },High,https://developer.android.com/kotlin/coroutines/test
39
+ Testing,Hilt Test,Use HiltTestApplication for integration tests,Annotate @HiltAndroidTest use @BindValue for fakes,Create Hilt modules manually in tests,@HiltAndroidTest class MyTest { @BindValue val repo: Repo = FakeRepo() },Manual DI setup in test classes,Medium,https://developer.android.com/training/dependency-injection/hilt-testing
40
+ Testing,Screenshot Test,Use Paparazzi for Compose screenshot tests,Test visual regressions without emulator,Only visual testing on device,@Test fun snapshot() { paparazzi.snapshot { MyScreen(state) } },Manual visual checking only,Low,https://github.com/cashapp/paparazzi
41
+ Material3,Dynamic Color,Use dynamic color theming with Material3,dynamicDarkColorScheme/dynamicLightColorScheme on Android 12+,Hardcoded colors ignoring system theme,val colorScheme = if (Build.VERSION.SDK_INT >= 31) dynamicDarkColorScheme(context) else DarkColorScheme,val colorScheme = darkColorScheme() // ignores wallpaper,Medium,https://developer.android.com/develop/ui/compose/designsystems/material3
42
+ Material3,TopAppBar Scroll,Use TopAppBar with scroll behavior,TopAppBar with scrollBehavior for collapsing,Static TopAppBar that doesn't respond to scroll,TopAppBar(title = { Text(title) } scrollBehavior = scrollBehavior),TopAppBar(title = { Text(title) }) // no scroll behavior,Medium,https://developer.android.com/reference/kotlin/androidx/compose/material3/package-summary#TopAppBar
43
+ Material3,Scaffold Usage,Use Scaffold for standard screen layout,Scaffold with topBar bottomBar snackbarHost floatingActionButton,Manual Column layout mimicking scaffold,Scaffold(topBar = { TopAppBar() } bottomBar = { NavBar() } snackbarHost = { SnackbarHost(hostState) }) { paddingValues -> Content(Modifier.padding(paddingValues)) },Column { TopBar(); Content(); BottomBar() } // no insets,High,https://developer.android.com/develop/ui/compose/components/scaffold
44
+ Material3,SnackbarHost,Use SnackbarHostState with Scaffold for snackbars,SnackbarHostState + LaunchedEffect to show snackbar,Toast.makeText for user feedback,val snackbarHostState = remember { SnackbarHostState() }; LaunchedEffect(message) { snackbarHostState.showSnackbar(message) },Toast.makeText(context message Toast.LENGTH_SHORT).show(),Medium,https://developer.android.com/develop/ui/compose/components/snackbar
45
+ Material3,BottomSheet,Use ModalBottomSheet from Material3,ModalBottomSheet with rememberModalBottomSheetState,Custom Dialog trying to look like bottom sheet,ModalBottomSheet(onDismissRequest = { onDismiss() } sheetState = sheetState) { Content() },Dialog { Surface(shape = BottomSheetShape) { Content() } } // wrong,Medium,https://developer.android.com/reference/kotlin/androidx/compose/material3/package-summary#ModalBottomSheet
46
+ Material3,TextField Variants,Use OutlinedTextField or TextField from M3,OutlinedTextField with proper label supportingText isError,BasicTextField for standard input,OutlinedTextField(value = text onValueChange = { text = it } label = { Text('Email') } isError = hasError supportingText = { if (hasError) Text('Invalid email') }),BasicTextField(value = text onValueChange = { text = it }) // no styling,Medium,https://developer.android.com/develop/ui/compose/text/user-input
47
+ Material3,NavigationBar,Use NavigationBar for bottom navigation,NavigationBar with NavigationBarItem and selected state,Custom Row with icon buttons for navigation,NavigationBar { items.forEach { NavigationBarItem(selected = it == current onClick = { nav(it) } icon = { Icon(it.icon) } label = { Text(it.label) }) } },Row { icons.forEach { IconButton(onClick = {}) { Icon(it) } } },High,https://developer.android.com/develop/ui/compose/components/bottom-navigation
48
+ Material3,Adaptive Layout,Use adaptive layouts for different screen sizes,WindowSizeClass for responsive layout decisions,Fixed layout ignoring screen size,val windowSizeClass = calculateWindowSizeClass(this); when (windowSizeClass.widthSizeClass) { Compact -> PhoneLayout(); Medium -> TabletLayout() },Fixed single-column layout on all screens,Medium,https://developer.android.com/develop/ui/compose/layouts/adaptive
49
+ Gradle,Version Catalog,Use Gradle Version Catalog (libs.versions.toml),Centralize versions in gradle/libs.versions.toml,Hardcode versions in each build.gradle,implementation(libs.compose.material3) // from version catalog,implementation('androidx.compose.material3:material3:1.2.0') // hardcoded,High,https://developer.android.com/build/migrate-to-catalogs
50
+ Gradle,Convention Plugins,Use convention plugins for shared build logic,buildSrc or build-logic module with convention plugins,Copy-paste build config across modules,plugins { id('myapp.android.library') } // convention plugin,Duplicate android {} block in every module,Medium,https://developer.android.com/build/migrate-to-catalogs
51
+ Gradle,Compose BOM,Use Compose BOM to manage Compose versions,Single BOM controls all Compose library versions,Manually version each Compose dependency,implementation(platform(libs.compose.bom)); implementation(libs.compose.material3) // no version,implementation('androidx.compose.ui:ui:1.6.0') // manual version,High,https://developer.android.com/develop/ui/compose/bom
52
+ Gradle,KSP over KAPT,Use KSP instead of KAPT for annotation processing,KSP for Hilt Room Moshi - 2x faster builds,KAPT slows build and is in maintenance mode,ksp(libs.hilt.compiler); ksp(libs.room.compiler),kapt(libs.hilt.compiler) // slower deprecated,High,https://developer.android.com/build/migrate-to-ksp
53
+ Gradle,Build Config,Use BuildConfig for environment-specific values,buildConfigField for API URLs and feature flags,Hardcode API URLs in source code,"buildConfigField('String' 'BASE_URL' '\https://api.example.com\""')""",const val BASE_URL = 'https://api.example.com' // hardcoded,Medium,
54
+ Gradle,Dependency Configurations,Use correct dependency configurations,implementation for internal api for exposed testImplementation for tests,Use api for everything leaking transitive deps,implementation(libs.retrofit) // internal to module,api(libs.retrofit) // exposes to all consumers,Medium,https://developer.android.com/build/dependencies
55
+ Navigation,Type-Safe Routes,Use type-safe Navigation Compose routes,@Serializable data classes for route definitions,String-based route patterns,@Serializable data class DetailRoute(val id: Int); composable<DetailRoute> { entry -> DetailScreen(entry.toRoute<DetailRoute>().id) },composable('detail/{id}') { backStackEntry -> val id = backStackEntry.arguments?.getString('id') },High,https://developer.android.com/guide/navigation/design/type-safety
56
+ Navigation,Nested NavGraphs,Use nested navigation graphs for feature grouping,Separate NavGraph per feature module,Single flat navigation graph,navigation<FeatureRoute>(startDestination = ListRoute) { composable<ListRoute> {}; composable<DetailRoute> {} },All composable() in single NavHost // no grouping,Medium,https://developer.android.com/guide/navigation/design/nested-graphs
57
+ Navigation,Result Passing,Pass results back via SavedStateHandle,previousBackStackEntry.savedStateHandle for results,SharedViewModel or global state for screen results,navController.previousBackStackEntry?.savedStateHandle?.set('result' value),sharedViewModel.result = value // wrong scope,Medium,
58
+ Compose,Modifier.semantics,Add semantics for accessibility,Proper contentDescription and semantic properties,Skip accessibility annotations,Image(painter icon contentDescription = 'Profile photo'),Image(painter icon contentDescription = null) // inaccessible,High,https://developer.android.com/develop/ui/compose/accessibility
59
+ Compose,SubcomposeLayout,Use SubcomposeLayout for dependent measurements,SubcomposeLayout when child size depends on sibling,BoxWithConstraints for all measurement needs,SubcomposeLayout { constraints -> val mainPlaceable = subcompose('main') { MainContent() }.first().measure(constraints) },BoxWithConstraints everywhere // less efficient,Low,
60
+ Compose,Shared Element Transitions,Use shared element transitions in Navigation,SharedTransitionLayout with AnimatedContent,No transition between screens,SharedTransitionLayout { AnimatedContent(targetState) { state -> when(state) { ... } } },No transitions between screens,Low,https://developer.android.com/develop/ui/compose/animation/shared-elements
61
+ Architecture,Data Layer Organization,Define a clear data layer containing repositories and data sources in a data package or module,"Create repositories even for single data sources, organize in data/ package",Let UI components interact directly with databases or APIs directly,,ViewModel directly accessing Room DAO or Retrofit service,High,https://developer.android.com/topic/architecture/recommendations
62
+ Architecture,UI Layer Organization,Define a clear UI layer for displaying data and handling user interaction in a ui package or module,Organize UI types in ui/ package or module,Mix UI and data layer types in same package,,,High,https://developer.android.com/topic/architecture/recommendations
63
+ Architecture,Domain Layer,Use domain layer with use cases when reusing business logic across multiple ViewModels or simplifying complex ViewModel logic,Add use cases for shared cross-ViewModel business logic,Add domain layer to every project regardless of complexity,class GetLatestNewsUseCase(val repo: NewsRepo) { operator fun invoke(): Flow<List<News>> = repo.getNews() },Duplicate business logic across ViewModels,Medium,https://developer.android.com/topic/architecture/recommendations
64
+ Architecture,Unidirectional Data Flow,Follow UDF where ViewModels expose UI state via observer pattern and receive actions from UI through method calls,ViewModels expose StateFlow and receive user actions via methods,Bidirectional data binding or direct state mutation from UI,class MyVM : ViewModel() { val uiState: StateFlow<UiState>; fun onAction(action: Action) {} },Two-way data binding modifying ViewModel state directly,Critical,https://developer.android.com/topic/architecture/recommendations
65
+ Architecture,No ViewModel Events to UI,Process events immediately in ViewModel and cause a state update with the result rather than sending one-off events to UI,Process events in ViewModel and update state,Send SingleLiveEvent or Channel events to UI,fun onBookmark() { _uiState.update { it.copy(isBookmarked = true) } },val events: Channel<Event> // one-off events to UI,High,https://developer.android.com/topic/architecture/recommendations
66
+ Architecture,Use Jetpack Compose,"Use Jetpack Compose for building new apps on phones, tablets, foldables, and Wear OS",Adopt Compose for new projects and new screens,Start new projects with XML Views only,,New project using only XML layouts and fragments,Medium,https://developer.android.com/topic/architecture/recommendations
67
+ ViewModel,Lifecycle Agnostic ViewModel,"ViewModels should not hold references to Lifecycle-related types like Activity, Fragment, Context, or Resources",Keep ViewModel free of Android framework references,"Pass Activity, Fragment, Context, or Resources to ViewModel",class MyVM @Inject constructor(val repo: Repo) : ViewModel(),class MyVM(val context: Context) : ViewModel() // holds Context,Critical,https://developer.android.com/topic/architecture/recommendations
68
+ ViewModel,Screen-Level ViewModel,Use ViewModels only at screen level not in reusable UI components,Use ViewModels in screen composables or Activity/Fragment/nav destinations,Use ViewModels in reusable composable components,@Composable fun ProfileScreen(vm: ProfileVM = hiltViewModel()) {},@Composable fun UserAvatar(vm: AvatarVM = viewModel()) // reusable component with VM,High,https://developer.android.com/topic/architecture/recommendations
69
+ ViewModel,Plain State Holders for Reusable UI,"Use plain state holder classes for managing complexity in reusable UI components, allowing state to be hoisted and controlled externally",Create state holder classes for complex reusable UI,Use ViewModel for reusable UI component state,class DatePickerState(initial: LocalDate) { var selectedDate by mutableStateOf(initial) },@HiltViewModel class DatePickerVM // ViewModel for reusable component,High,https://developer.android.com/topic/architecture/recommendations
70
+ ViewModel,Avoid AndroidViewModel,Use ViewModel class instead of AndroidViewModel. Move Application dependencies to UI or data layer.,Use ViewModel with proper DI instead of AndroidViewModel,Use AndroidViewModel to access Application context,@HiltViewModel class MyVM @Inject constructor(repo: Repo) : ViewModel(),class MyVM(app: Application) : AndroidViewModel(app) { val ctx = app },Medium,https://developer.android.com/topic/architecture/recommendations
71
+ ViewModel,Expose Single UiState,Expose UI state through a single uiState property as StateFlow. Use stateIn with WhileSubscribed(5000) for stream data or MutableStateFlow for simpler cases.,Single uiState StateFlow with stateIn WhileSubscribed(5000),Multiple scattered LiveData or state properties,"val uiState: StateFlow<UiState> = repo.dataStream().map { UiState.Success(it) }.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5_000), UiState.Loading)",val loading = MutableLiveData<Boolean>(); val data = MutableLiveData<List<Item>>(); val error = MutableLiveData<String>() // scattered,High,https://developer.android.com/topic/architecture/recommendations
72
+ Lifecycle,No Lifecycle Method Override,Do not override lifecycle methods like onResume in Activities or Fragments. Use LifecycleObserver and repeatOnLifecycle API instead.,Use LifecycleObserver and repeatOnLifecycle,"Override onResume, onPause, onStart in Activity/Fragment",lifecycle.addObserver(object : DefaultLifecycleObserver { override fun onResume(owner: LifecycleOwner) {} }),override fun onResume() { super.onResume(); loadData() },High,https://developer.android.com/topic/architecture/recommendations
73
+ Architecture,DI Scoping,Scope dependencies to a container when the type contains mutable shared data or is expensive to initialize and widely used,Scope expensive or shared-state dependencies to appropriate container,Create new instances of expensive objects every time,@Singleton class UserSessionManager @Inject constructor(),fun provideUserSession() = UserSessionManager() // new instance each time,High,https://developer.android.com/topic/architecture/recommendations
74
+ Testing,Know What to Test,"At minimum: unit test ViewModels including Flows, unit test data layer entities, and UI navigation tests for regression in CI","Test ViewModels, repositories, data sources, and navigation",Skip testing or only test manually,"@Test fun uiState_initially_loading() = runTest { assertEquals(Loading, viewModel.uiState.value) }",No automated tests,High,https://developer.android.com/topic/architecture/recommendations
75
+ Testing,Prefer Fakes Over Mocks,Use fake implementations instead of mocking frameworks for test doubles,Create Fake implementations of interfaces,Use Mockito/Mockk for everything,class FakeUserRepo : UserRepo { override suspend fun getUser(id: String) = testUser },val repo = mockk<UserRepo>(); every { repo.getUser(any()) } returns testUser,High,https://developer.android.com/topic/architecture/recommendations
76
+ Testing,Test StateFlows,"When testing StateFlow: assert on value property when possible, create collectJob when using WhileSubscribed",Assert StateFlow.value directly or use Turbine,Use Thread.sleep or delay to wait for state,"@Test fun test() = runTest { val vm = MyVM(); assertEquals(expected, vm.uiState.value) }","@Test fun test() { Thread.sleep(1000); assertEquals(expected, vm.state.value) }",High,https://developer.android.com/topic/architecture/recommendations
77
+ Architecture,Model Per Layer,"In complex apps create separate models per layer: remote DTOs, local entities, and UI models",Map between network/database/UI models at layer boundaries,Use single model class across all layers,data class UserDto(...); data class UserEntity(...); data class UserUiModel(...),data class User(...) // same class for API DB and UI,Medium,https://developer.android.com/topic/architecture/recommendations
78
+ Naming,Method Naming,Methods should be named as verb phrases,Use verb phrases for method names,Use noun phrases for methods,fun makePayment(),fun payment(),Low,https://developer.android.com/topic/architecture/recommendations
79
+ Naming,Property Naming,Properties should be named as noun phrases,Use noun phrases for property names,Use verb phrases for properties,val inProgressTopicSelection,val selectTopicsInProgress,Low,https://developer.android.com/topic/architecture/recommendations
80
+ Naming,Stream Naming,Name stream-returning functions as get{Model}Stream(). Use plural for lists: get{Models}Stream(),Follow get{Model}Stream() pattern for Flow/LiveData functions,Inconsistent naming for stream functions,fun getAuthorStream(): Flow<Author>; fun getAuthorsStream(): Flow<List<Author>>,fun fetchAuthor(): Flow<Author>; fun observeAuthors(): Flow<List<Author>>,Low,https://developer.android.com/topic/architecture/recommendations
81
+ Naming,Interface Implementation Naming,Use meaningful names for interface implementations. Use Default prefix if no better name. Fake prefix for test doubles.,Name implementations descriptively like OfflineFirstNewsRepo,Generic Impl suffix for all implementations,class OfflineFirstNewsRepository : NewsRepository; class FakeNewsRepository : NewsRepository,class NewsRepositoryImpl : NewsRepository // generic Impl,Low,https://developer.android.com/topic/architecture/recommendations
82
+ SOLID,Single Responsibility Principle,"Each class should have one reason to change. ViewModel owns UI logic, Repository owns data access, UseCase owns business rules. When a class changes for multiple unrelated reasons split it.",One class = one job. ViewModel for UI state; Repository for data; UseCase for business logic,God class handling networking + caching + UI state + validation in one place,"class AuthRepository @Inject constructor(private val api: AuthApi, private val dao: AuthDao) { suspend fun login(credentials: Credentials): Result<User> = try { val user = api.login(credentials); dao.insert(user); Result.success(user) } catch (e: Exception) { Result.failure(e) } }","class LoginViewModel : ViewModel() { private val retrofit = Retrofit.Builder().baseUrl(URL).build(); private val db = Room.databaseBuilder(ctx, AppDb::class.java, 'db').build(); fun login(email: String, pwd: String) { /* validates + calls API + caches + updates UI state */ } }",Critical,
83
+ SOLID,Open-Closed Principle,Software entities should be open for extension but closed for modification. Add new behavior through new implementations of interfaces rather than modifying existing code with conditional branches.,Define interfaces and add new implementations; avoid growing if/else or when chains,Modify existing classes every time a new variant is needed,interface NotificationSender { suspend fun send(notification: Notification) }; class PushNotificationSender @Inject constructor(private val fcm: FirebaseMessaging) : NotificationSender { override suspend fun send(n: Notification) { fcm.send(n.toRemoteMessage()) } }; class EmailNotificationSender @Inject constructor(private val mailer: Mailer) : NotificationSender { override suspend fun send(n: Notification) { mailer.send(n.toEmail()) } },"fun sendNotification(type: String, notification: Notification) { when (type) { 'push' -> { /* FCM logic */ }; 'email' -> { /* email logic */ }; 'sms' -> { /* sms logic */ } /* add new else branch for every type forever */ } }",High,
84
+ SOLID,Liskov Substitution Principle,Subtypes must be substitutable for their base types. Every implementation of an interface must fully honor its contract without throwing unexpected exceptions or silently skipping behavior.,Subclasses honor the full contract of the parent interface or class,Subclasses that throw UnsupportedOperationException or silently no-op required methods,interface UserRepository { fun getUsers(): Flow<List<User>>; suspend fun save(user: User) }; class LocalUserRepository @Inject constructor(private val dao: UserDao) : UserRepository { override fun getUsers() = dao.observeAll(); override suspend fun save(user: User) = dao.upsert(user) },class ReadOnlyUserRepository : UserRepository { override fun getUsers() = flowOf(cachedList); override suspend fun save(user: User) { throw UnsupportedOperationException('Read-only') /* violates contract */ } },High,
85
+ SOLID,Interface Segregation Principle,No client should be forced to depend on methods it does not use. Split fat interfaces into smaller role-specific ones so implementations and consumers only see what they need.,Small focused interfaces per role; classes implement only what they need,One large interface forcing every implementation to stub unused methods,interface UserReader { fun getUser(id: String): Flow<User> }; interface UserWriter { suspend fun saveUser(user: User) }; class ProfileViewModel @Inject constructor(private val reader: UserReader) : ViewModel() { val user = reader.getUser(userId) },interface UserRepository { fun getUser(id: String): Flow<User>; suspend fun saveUser(user: User); suspend fun deleteUser(id: String); suspend fun exportUsers(): File; suspend fun syncUsers() } /* ProfileScreen only needs getUser but depends on everything */ ,High,
86
+ SOLID,Dependency Inversion Principle,High-level modules should depend on abstractions not on concrete low-level details. ViewModel depends on a Repository interface not on Retrofit or Room directly. Bindings are configured in DI modules.,Depend on interfaces. Bind implementations in Hilt modules,ViewModel or UseCase directly creating or referencing Retrofit/Room/SharedPreferences,"interface OrderRepository { fun getOrders(): Flow<List<Order>> }; @HiltViewModel class OrderViewModel @Inject constructor(private val repo: OrderRepository) : ViewModel() { val orders = repo.getOrders().stateIn(viewModelScope, SharingStarted.WhileSubscribed(5_000), emptyList()) }",class OrderViewModel : ViewModel() { private val api = RetrofitClient.instance.create(OrderApi::class.java); private val dao = AppDatabase.instance.orderDao(); fun load() { viewModelScope.launch { val orders = api.getOrders(); dao.insertAll(orders) } } },Critical,
87
+ CleanCode,Small Focused Functions,Each function should do exactly one thing at one level of abstraction. If you need comments to separate sections inside a function it should be multiple functions. Aim for 5-20 lines per function.,One function = one task. Extract each logical step into a well-named private function,Functions longer than 30 lines doing multiple unrelated steps,suspend fun syncUser(id: String) { val remote = fetchRemoteUser(id); val merged = mergeWithLocal(remote); saveUser(merged) }; private suspend fun fetchRemoteUser(id: String): UserDto = api.getUser(id); private suspend fun mergeWithLocal(remote: UserDto): User { val local = dao.getUser(remote.id); return local?.merge(remote.toDomain()) ?: remote.toDomain() }; private suspend fun saveUser(user: User) = dao.upsert(user),"suspend fun syncUser(id: String) { /* 80 lines: fetch from API, check cache, parse JSON, transform model, handle 5 error cases, update DB, notify UI, log analytics */ }",High,
88
+ CleanCode,Meaningful Naming,Names should reveal intent and use domain vocabulary. Avoid abbreviations (mgr ctx btn) single-letter variables and generic names (data info temp handler). Longer scope = longer name.,Names describe what/why not how. Use domain terms. Full words over abbreviations,val isUserAuthenticated: Boolean; suspend fun fetchUserProfile(userId: String): User; class PaymentRepository; sealed interface CheckoutUiState,val flag: Boolean; fun proc(s: String): Any?; class Mgr; data class Data(val x: List<Any>),High,,
89
+ CleanCode,Avoid Magic Numbers and Strings,Replace every hardcoded literal with a named constant or enum. Named constants serve as documentation and create single-source-of-truth for values used in multiple places.,Named constants in companion object or top-level. Use enums for related sets,Raw numbers and strings scattered in logic and UI code,private companion object { const val MAX_RETRY = 3; const val TIMEOUT_MS = 5_000L; const val DEBOUNCE_MS = 300L }; retry(MAX_RETRY) { withTimeout(TIMEOUT_MS) { api.fetch() } },if (count < 3) { delay(5000); api.fetch() } // what is 3? what is 5000?,Medium,
90
+ CleanCode,Guard Clauses and Early Return,Validate preconditions at the top of functions and return/throw early. This eliminates nesting and keeps the happy path at the lowest indentation level.,Return or throw early for invalid cases. Happy path stays unnested at base indentation,Deeply nested if/else pyramids,fun processPayment(order: Order?): Result<Receipt> { if (order == null) return Result.failure(IllegalArgumentException('Order required')); if (order.items.isEmpty()) return Result.failure(EmptyCartException()); if (!order.isValid()) return Result.failure(InvalidOrderException()); return paymentGateway.charge(order) },fun processPayment(order: Order?): Result<Receipt> { if (order != null) { if (order.items.isNotEmpty()) { if (order.isValid()) { return paymentGateway.charge(order) } else { return Result.failure(InvalidOrderException()) } } else { return Result.failure(EmptyCartException()) } } else { return Result.failure(IllegalArgumentException('Order required')) } },Medium,
91
+ CleanCode,DRY - Extract Shared Logic,When the same logic appears in two or more places extract it into a single shared function or extension. Update all call sites to use the shared version.,Extract repeated logic into extension functions or shared helpers,Copy-pasting the same validation/formatting/mapping logic across classes,"fun String.toDisplayDate(): String = LocalDate.parse(this).format(DateTimeFormatter.ofPattern('MMM dd, yyyy')); /* used in OrderScreen, ProfileScreen, HistoryScreen */","// OrderScreen: LocalDate.parse(date).format(DateTimeFormatter.ofPattern('MMM dd, yyyy')); // ProfileScreen: LocalDate.parse(date).format(DateTimeFormatter.ofPattern('MMM dd, yyyy')); // HistoryScreen: same again",High,
92
+ CleanCode,Immutability by Default,Use val over var and immutable collections over mutable. Data classes with val properties are inherently safe to share across threads. Mutable state should be isolated and controlled.,val for everything. Immutable data classes. MutableStateFlow only inside ViewModel,var and MutableList scattered across classes,"data class UserUiState(val name: String, val email: String, val posts: ImmutableList<Post> = persistentListOf()); class UserViewModel : ViewModel() { private val _uiState = MutableStateFlow(UserUiState()); val uiState: StateFlow<UserUiState> = _uiState.asStateFlow() }",class UserViewModel : ViewModel() { var name = ''; var email = ''; var posts = mutableListOf<Post>(); var isLoading = false },High,
93
+ CleanCode,Kotlin Extension Functions,Use extension functions to add behavior to existing types instead of utility classes with static methods. Keep extensions focused and discoverable by placing them near their domain.,Extension functions on types for domain operations. Place near usage or in type-specific files,Java-style static utility classes,"fun User.toUiModel() = UserUiModel(displayName = '$firstName $lastName', avatar = avatarUrl.orDefault()); fun Long.toFormattedDuration(): String { val minutes = this / 60_000; val seconds = (this % 60_000) / 1000; return '${minutes}m ${seconds}s' }",object UserUtils { fun getDisplayName(user: User) = '${user.firstName} ${user.lastName}'; fun getAvatar(user: User) = user.avatarUrl ?: DEFAULT }; object TimeUtils { fun format(ms: Long): String { /* ... */ } },Medium,
94
+ CleanCode,Sealed Types for Exhaustive State,Model finite state sets with sealed class/interface so the compiler enforces exhaustive when branches. Adding a new subtype triggers compile errors at every unhandled when.,Sealed interface for UI state and result types. when expression handles all cases,Boolean flags or nullable fields creating impossible/ambiguous state combinations,sealed interface HomeUiState { data object Loading : HomeUiState; data class Success(val articles: List<Article>) : HomeUiState; data class Error(val message: String) : HomeUiState }; when (state) { is Loading -> CircularProgressIndicator(); is Success -> ArticleList(state.articles); is Error -> ErrorMessage(state.message) },"data class HomeUiState(val articles: List<Article>? = null, val isLoading: Boolean = false, val error: String? = null) // isLoading=true AND error!=null at same time?",High,
95
+ CleanCode,Avoid Deep Nesting,Maximum 2-3 levels of indentation. Use early returns and when expressions and function extraction to flatten deeply nested code. Deep nesting signals that a function is doing too much.,Max 2-3 indent levels. Flatten with early return + when + extract function,4+ levels of nested if/for/try blocks,fun getUserLabel(user: User?): String { val name = user?.name ?: return 'Guest'; if (name.isBlank()) return 'Anonymous'; return name.replaceFirstChar { it.uppercase() } },fun getUserLabel(user: User?): String { if (user != null) { if (user.name != null) { if (user.name.isNotBlank()) { return user.name.replaceFirstChar { it.uppercase() } } else { return 'Anonymous' } } else { return 'Guest' } } else { return 'Guest' } },Medium,
96
+ CleanCode,Composition Over Inheritance,Favor interfaces and delegation over class hierarchies. Kotlin class delegation (by keyword) makes composition as concise as inheritance while being more flexible and avoiding fragile base class issues.,Interface + delegation for shared behavior. Shallow hierarchy max 1-2 levels,Deep inheritance chains or abstract base classes with half-implemented methods,"interface Analytics { fun track(event: String) }; class ScreenViewModel(private val analytics: Analytics) : ViewModel(), Analytics by analytics",open class BaseViewModel : ViewModel() { /* shared stuff */ }; open class BaseListViewModel : BaseViewModel() { /* list stuff */ }; open class BaseSearchableListViewModel : BaseListViewModel() { /* search stuff */ }; class UserListViewModel : BaseSearchableListViewModel() // 4 levels deep,High,
97
+ DI,Constructor Injection Always,Always prefer constructor injection. It makes dependencies explicit visible in the class signature and trivially testable. Use field injection (@Inject lateinit var) only in Android framework classes where constructors are not controlled (Activity Fragment).,@Inject constructor for all classes. Field injection only in Activity/Fragment,Field injection in ViewModel Repository UseCase or any non-framework class,"class UserRepository @Inject constructor(private val api: UserApi, private val dao: UserDao, @IoDispatcher private val dispatcher: CoroutineDispatcher)",class UserRepository { @Inject lateinit var api: UserApi; @Inject lateinit var dao: UserDao /* hidden dependencies impossible to provide in tests */ },High,https://developer.android.com/training/dependency-injection#di-manual
98
+ DI,Hilt Module Organization,"Group Hilt modules by layer: DataModule for repositories and data sources, NetworkModule for Retrofit/OkHttp, DatabaseModule for Room. Use @Binds for interface-to-impl mapping and @Provides for third-party objects.",Separate modules per layer. @Binds for interfaces; @Provides for builders and third-party types,Single AppModule providing everything in one giant file,@Module @InstallIn(SingletonComponent::class) interface DataModule { @Binds fun bindUserRepo(impl: OfflineFirstUserRepo): UserRepository; @Binds fun bindOrderRepo(impl: DefaultOrderRepo): OrderRepository }; @Module @InstallIn(SingletonComponent::class) object NetworkModule { @Provides @Singleton fun provideOkHttp(): OkHttpClient = OkHttpClient.Builder().addInterceptor(AuthInterceptor()).build() },"@Module @InstallIn(SingletonComponent::class) object AppModule { @Provides fun provideDb(app: Application) = Room.databaseBuilder(...).build(); @Provides fun provideApi() = Retrofit.Builder()...; @Provides fun provideRepo(api: Api, db: Db) = UserRepo(api, db); /* 50+ provides in one file */ }",Medium,https://developer.android.com/training/dependency-injection/hilt-android#hilt-modules
99
+ DI,Qualifier Annotations,"Use @Qualifier to distinguish multiple bindings of the same type: different CoroutineDispatchers, different API base URLs, different OkHttpClient configs. Without qualifiers Hilt cannot tell which instance to inject.",Custom @Qualifier for same-type bindings like @IoDispatcher @DefaultDispatcher @MainDispatcher,Providing same type twice without qualifier causing Hilt compile error or wrong injection,@Qualifier @Retention(AnnotationRetention.BINARY) annotation class IoDispatcher; @Qualifier @Retention(AnnotationRetention.BINARY) annotation class DefaultDispatcher; @Module @InstallIn(SingletonComponent::class) object DispatcherModule { @Provides @IoDispatcher fun provideIo(): CoroutineDispatcher = Dispatchers.IO; @Provides @DefaultDispatcher fun provideDefault(): CoroutineDispatcher = Dispatchers.Default },@Provides fun provideDispatcher(): CoroutineDispatcher = Dispatchers.IO // only one binding; which dispatcher is this?,Medium,https://developer.android.com/training/dependency-injection/hilt-android#multiple-bindings
100
+ DI,Assisted Injection,Use @AssistedInject when a class needs both DI-provided dependencies and runtime parameters (like a screen ID or user input). Avoids writing manual ViewModelProvider.Factory boilerplate.,@AssistedInject + @AssistedFactory for combining DI deps with runtime params,Manual ViewModelProvider.Factory implementations for every ViewModel that needs an argument,"class DetailViewModel @AssistedInject constructor(@Assisted private val articleId: Long, private val repo: ArticleRepository) : ViewModel() { val article = repo.getArticle(articleId).stateIn(viewModelScope, SharingStarted.WhileSubscribed(5_000), null) }; @AssistedFactory interface Factory { fun create(articleId: Long): DetailViewModel }",class DetailViewModel(private val articleId: Long) : ViewModel() { class Factory(private val articleId: Long) : ViewModelProvider.Factory { override fun <T : ViewModel> create(modelClass: Class<T>): T = DetailViewModel(articleId) as T } } // manual factory boilerplate for every screen,Medium,https://developer.android.com/training/dependency-injection/hilt-android
101
+ DI,Scope to Component Lifecycle,"Match DI scope to the Android component lifecycle. @Singleton for app-wide singletons, @ActivityRetainedScoped to survive config changes, @ViewModelScoped for per-screen sharing. Over-scoping wastes memory; under-scoping causes redundant instances.",Match scope to lifecycle. @Singleton only for truly app-wide; @ViewModelScoped for screen-level sharing,@Singleton on everything or no scoping at all,@Module @InstallIn(ViewModelComponent::class) interface ScreenModule { @Binds @ViewModelScoped fun bindGetArticlesUseCase(impl: DefaultGetArticlesUseCase): GetArticlesUseCase }; @Module @InstallIn(SingletonComponent::class) interface AppModule { @Binds @Singleton fun bindAuthManager(impl: DefaultAuthManager): AuthManager },@Singleton class GetArticlesUseCase @Inject constructor(val repo: ArticleRepo) // lives forever even when no screen needs it; @Singleton on every class just to be safe,High,https://developer.android.com/training/dependency-injection/hilt-android#component-scopes
102
+ DI,Interface Abstraction for Testability,Every Repository and DataSource should have an interface. Inject the interface type everywhere. In tests provide a Fake implementation via @BindValue or a test Hilt module. This makes tests fast and deterministic.,Interface for every data layer class. Inject interface types. Swap with Fakes in tests,Inject concrete classes making tests require real network/database,"interface ArticleRepository { fun getArticles(): Flow<List<Article>>; suspend fun refresh() }; class OfflineFirstArticleRepository @Inject constructor(private val api: ArticleApi, private val dao: ArticleDao) : ArticleRepository { override fun getArticles() = dao.observeAll(); override suspend fun refresh() { dao.upsertAll(api.getAll().map { it.toEntity() }) } }; // In test: class FakeArticleRepository : ArticleRepository { private val articles = MutableStateFlow<List<Article>>(emptyList()); override fun getArticles() = articles; override suspend fun refresh() { articles.value = testArticles } }","class ArticleRepository @Inject constructor(private val api: ArticleApi, private val dao: ArticleDao) { /* concrete class injected everywhere; tests need real Room + Retrofit or heavy mocking */ }",Critical,https://developer.android.com/training/dependency-injection
103
+ Room,Entity Design,Use @Entity with explicit tableName and @PrimaryKey. Define indices for columns used in WHERE and ORDER BY clauses. Use @ColumnInfo to control column names independently from Kotlin property names.,Explicit tableName + indices + @ColumnInfo for query-heavy columns,Default table/column names and no indices,"@Entity(tableName = 'articles', indices = [Index(value = ['author_id']), Index(value = ['published_at'])]) data class ArticleEntity(@PrimaryKey val id: String, @ColumnInfo(name = 'author_id') val authorId: String, @ColumnInfo(name = 'published_at') val publishedAt: Long)","@Entity data class ArticleEntity(@PrimaryKey val id: String, val authorId: String, val publishedAt: Long) // no indices slow queries",High,https://developer.android.com/training/data-storage/room/defining-data
104
+ Room,DAO Return Flow,Return Flow from DAO queries to get automatic updates when underlying data changes. Use suspend for one-shot write operations. Never return LiveData from DAO in new code.,Flow for observable reads; suspend for writes,Returning raw List requiring manual refresh or LiveData in new code,@Dao interface ArticleDao { @Query('SELECT * FROM articles ORDER BY published_at DESC') fun observeAll(): Flow<List<ArticleEntity>>; @Insert(onConflict = OnConflictStrategy.REPLACE) suspend fun upsertAll(articles: List<ArticleEntity>); @Query('DELETE FROM articles') suspend fun deleteAll() },@Dao interface ArticleDao { @Query('SELECT * FROM articles') fun getAll(): List<ArticleEntity> // blocks thread no auto-update; @Query('SELECT * FROM articles') fun getAllLive(): LiveData<List<ArticleEntity>> // LiveData in DAO },High,https://developer.android.com/training/data-storage/room/async-queries
105
+ Room,Migration Strategy,Provide Migration objects for schema changes in production. Use fallbackToDestructiveMigration only in debug builds. Test migrations with MigrationTestHelper to prevent data loss.,Write Migration(oldVer newVer) for every schema change. Test with MigrationTestHelper,fallbackToDestructiveMigration in production losing user data,"Room.databaseBuilder(ctx, AppDb::class.java, 'app.db').addMigrations(MIGRATION_1_2, MIGRATION_2_3).build(); val MIGRATION_1_2 = object : Migration(1, 2) { override fun migrate(db: SupportSQLiteDatabase) { db.execSQL('ALTER TABLE articles ADD COLUMN category TEXT DEFAULT NULL') } }","Room.databaseBuilder(ctx, AppDb::class.java, 'app.db').fallbackToDestructiveMigration().build() // users lose all data on schema change",Critical,https://developer.android.com/training/data-storage/room/migrating-db-versions
106
+ Room,TypeConverters,"Use @TypeConverter for complex types (Date, Enum, List) that Room cannot store natively. Place converters on the Database class for global availability. Use kotlinx.serialization for JSON columns.",@TypeConverter for non-primitive types. JSON serialization for embedded lists,Storing complex objects without converters causing compile errors or flattening manually,"class Converters { @TypeConverter fun fromTimestamp(value: Long?): Instant? = value?.let { Instant.fromEpochMilliseconds(it) }; @TypeConverter fun toTimestamp(instant: Instant?): Long? = instant?.toEpochMilliseconds() }; @Database(entities = [...], version = 1) @TypeConverters(Converters::class) abstract class AppDatabase : RoomDatabase()",Storing Date as String with manual parsing everywhere; or adding 10 columns to flatten a nested object,Medium,https://developer.android.com/training/data-storage/room/referencing-data
107
+ Room,Embedded and Relations,Use @Embedded for value objects within entities and @Relation for one-to-many queries. Avoids manual JOIN parsing and keeps code type-safe.,@Embedded for value objects; @Relation with data class for JOINs,Manual SQL JOINs parsed into maps of maps,"data class AuthorWithArticles(@Embedded val author: AuthorEntity, @Relation(parentColumn = 'id', entityColumn = 'author_id') val articles: List<ArticleEntity>); @Query('SELECT * FROM authors WHERE id = :id') fun getAuthorWithArticles(id: String): Flow<AuthorWithArticles>","@Query('SELECT a.*, b.* FROM authors a JOIN articles b ON a.id = b.author_id WHERE a.id = :id') fun getJoined(id: String): Flow<List<Map<String, Any>>> // untyped manual parsing",Medium,https://developer.android.com/training/data-storage/room/relationships
108
+ Room,Database Inspector,Use Android Studio Database Inspector to browse Room tables in real time during debugging. Also write DAO unit tests with in-memory database for CI.,Database Inspector for debugging; in-memory Room for unit tests,Log.d with cursor dumps or only testing on device,"@Test fun insertAndRead() = runTest { val db = Room.inMemoryDatabaseBuilder(ctx, AppDb::class.java).build(); db.articleDao().upsertAll(testArticles); val result = db.articleDao().observeAll().first(); assertEquals(testArticles.size, result.size) }","Log.d('DB', cursor.getString(0)) // manual logging for debugging",Medium,https://developer.android.com/studio/inspect/database
109
+ DataStore,Preferences DataStore Over SharedPreferences,Replace SharedPreferences with Preferences DataStore for async non-blocking key-value storage. DataStore uses Kotlin coroutines and Flow guaranteeing no UI thread blocking and data consistency.,Preferences DataStore for simple key-value. Async access via Flow,SharedPreferences with commit/apply risking ANR and race conditions,val Context.settingsStore by preferencesDataStore(name = 'settings'); val DARK_MODE = booleanPreferencesKey('dark_mode'); val isDarkMode: Flow<Boolean> = ctx.settingsStore.data.map { prefs -> prefs[DARK_MODE] ?: false }; suspend fun setDarkMode(enabled: Boolean) { ctx.settingsStore.edit { it[DARK_MODE] = enabled } },"val prefs = getSharedPreferences('settings', MODE_PRIVATE); val isDark = prefs.getBoolean('dark_mode', false); prefs.edit().putBoolean('dark_mode', true).apply() // no type safety; possible ANR with commit()",High,https://developer.android.com/topic/libraries/architecture/datastore
110
+ DataStore,Proto DataStore for Typed Data,Use Proto DataStore with Protocol Buffers for complex structured settings that need type safety and schema evolution. Define a .proto schema for compile-time type checking.,Proto DataStore for structured settings with schema evolution,Preferences DataStore with dozens of keys for related settings,// settings.proto: message Settings { bool dark_mode = 1; string locale = 2; int32 font_size = 3; }; val settingsFlow: Flow<Settings> = protoDataStore.data; suspend fun updateLocale(locale: String) { protoDataStore.updateData { it.toBuilder().setLocale(locale).build() } },// 20+ individual PreferencesKeys for one settings screen; no schema validation; possible key typos,Medium,https://developer.android.com/topic/libraries/architecture/datastore#proto-datastore
111
+ DataStore,DataStore Singleton,Create DataStore as a singleton at the top level using the property delegate. Never create multiple DataStore instances for the same file as this causes corruption.,Single top-level property delegate per file. Inject via Hilt,Multiple DataStore instances for the same file in different classes,val Context.userPrefs by preferencesDataStore(name = 'user_prefs'); // single declaration at file top level; inject context to access,class RepoA { val store = PreferencesDataStoreFactory.create { File('user_prefs') } }; class RepoB { val store = PreferencesDataStoreFactory.create { File('user_prefs') } } // two instances = corruption,Critical,https://developer.android.com/topic/libraries/architecture/datastore#preferences-datastore
112
+ DataStore,DataStore Error Handling,Handle IOExceptions from DataStore reads by catching in the Flow chain with catch operator. Emit default values on error to prevent app crashes.,catch IOException in Flow and emit defaults,Uncaught exceptions crashing the app on corrupted DataStore,val settings: Flow<UserSettings> = dataStore.data.catch { exception -> if (exception is IOException) { emit(emptyPreferences()) } else { throw exception } }.map { prefs -> UserSettings(darkMode = prefs[DARK_MODE] ?: false) },val settings: Flow<Preferences> = dataStore.data // IOException crashes app if file corrupted,High,https://developer.android.com/topic/libraries/architecture/datastore
113
+ WorkManager,Use WorkManager for Deferrable Work,Use WorkManager for reliable background work that must complete even if the app exits or device restarts. Not for immediate in-process tasks which should use coroutines.,WorkManager for guaranteed deferrable tasks (sync upload backup),Coroutines for tasks that must survive process death; WorkManager for immediate tasks,"val syncRequest = OneTimeWorkRequestBuilder<SyncWorker>().setConstraints(Constraints.Builder().setRequiredNetworkType(NetworkType.CONNECTED).build()).build(); WorkManager.getInstance(ctx).enqueueUniqueWork('sync', ExistingWorkPolicy.KEEP, syncRequest)",viewModelScope.launch { syncToServer() } // lost if app killed; or WorkManager for 100ms in-process task,High,https://developer.android.com/topic/libraries/architecture/workmanager
114
+ WorkManager,CoroutineWorker,Extend CoroutineWorker instead of Worker for suspend function support. Return Result.success() or Result.retry() or Result.failure() from doWork().,CoroutineWorker with suspend doWork(),Worker with blocking Thread.sleep or manual coroutine launching,"class SyncWorker @AssistedInject constructor(@Assisted ctx: Context, @Assisted params: WorkerParameters, private val repo: SyncRepository) : CoroutineWorker(ctx, params) { override suspend fun doWork(): Result = try { repo.syncAll(); Result.success() } catch (e: Exception) { Result.retry() } }","class SyncWorker(ctx: Context, params: WorkerParameters) : Worker(ctx, params) { override fun doWork(): Result { Thread.sleep(5000); return Result.success() } // blocks worker thread }",High,https://developer.android.com/topic/libraries/architecture/workmanager/advanced/coroutineworker
115
+ WorkManager,Work Constraints,"Set constraints (network connectivity, battery level, charging state, storage) to ensure work runs only when conditions are met. Avoids wasted battery and failed requests.",Set appropriate constraints matching the work requirements,Enqueuing network work without network constraint,"val constraints = Constraints.Builder().setRequiredNetworkType(NetworkType.CONNECTED).setRequiresBatteryNotLow(true).build(); val uploadRequest = OneTimeWorkRequestBuilder<UploadWorker>().setConstraints(constraints).setBackoffCriteria(BackoffPolicy.EXPONENTIAL, 30, TimeUnit.SECONDS).build()",WorkManager.getInstance(ctx).enqueue(OneTimeWorkRequestBuilder<UploadWorker>().build()) // runs without network = guaranteed failure,Medium,https://developer.android.com/topic/libraries/architecture/workmanager/how-to/define-work
116
+ WorkManager,Unique Work,Use enqueueUniqueWork or enqueueUniquePeriodicWork to prevent duplicate work. Choose KEEP to skip if running or REPLACE to restart.,Unique work names with KEEP or REPLACE policy,Enqueuing same work repeatedly creating duplicate executions,"WorkManager.getInstance(ctx).enqueueUniquePeriodicWork('daily_sync', ExistingPeriodicWorkPolicy.KEEP, PeriodicWorkRequestBuilder<SyncWorker>(1, TimeUnit.DAYS).build())",WorkManager.getInstance(ctx).enqueue(periodicRequest) // called on every app start = duplicate syncs,High,https://developer.android.com/topic/libraries/architecture/workmanager/how-to/managing-work
117
+ WorkManager,Hilt Worker Injection,Use @HiltWorker with @AssistedInject to inject dependencies into Workers. Requires HiltWorkerFactory setup in Application class.,@HiltWorker + @AssistedInject for DI in workers,Manual dependency construction inside Worker,"@HiltWorker class UploadWorker @AssistedInject constructor(@Assisted ctx: Context, @Assisted params: WorkerParameters, private val uploadRepo: UploadRepository) : CoroutineWorker(ctx, params) { override suspend fun doWork(): Result { uploadRepo.uploadPending(); return Result.success() } }","class UploadWorker(ctx: Context, params: WorkerParameters) : CoroutineWorker(ctx, params) { override suspend fun doWork(): Result { val api = Retrofit.Builder()...create(Api::class.java); api.upload() } // manual DI in worker }",Medium,https://developer.android.com/training/dependency-injection/hilt-android#workmanager
118
+ Networking,Retrofit Interface Design,"Define Retrofit service interfaces with suspend functions. Use @Body for POST/PUT, @Query for query params, @Path for URL segments. Return domain types or Response<T> when you need HTTP status codes.",Suspend functions in Retrofit interface. Return Response<T> when status code matters,Callbacks or blocking execute() calls,"interface ArticleApi { @GET('articles') suspend fun getArticles(@Query('page') page: Int, @Query('size') size: Int): List<ArticleDto>; @GET('articles/{id}') suspend fun getArticle(@Path('id') id: String): ArticleDto; @POST('articles') suspend fun createArticle(@Body article: CreateArticleRequest): Response<ArticleDto> }",interface ArticleApi { @GET('articles') fun getArticles(): Call<List<ArticleDto>> // callback-based; or .execute() blocking main thread },High,https://developer.android.com/training/basics/network-ops
119
+ Networking,OkHttp Interceptors,"Use OkHttp interceptors for cross-cutting concerns: authentication headers, logging, retry logic, caching. Application interceptors run once; network interceptors see redirects.",Application interceptor for auth/logging; network interceptor for caching,Duplicating auth header logic in every API call,"val okHttp = OkHttpClient.Builder().addInterceptor { chain -> val request = chain.request().newBuilder().addHeader('Authorization', 'Bearer ${tokenProvider.get()}').build(); chain.proceed(request) }.addInterceptor(HttpLoggingInterceptor().apply { level = if (BuildConfig.DEBUG) BODY else NONE }).build()",// Adding auth header manually in every repository method: api.getArticles(authHeader = 'Bearer $token'),High,https://square.github.io/okhttp/features/interceptors/
120
+ Networking,Kotlinx Serialization Over Gson,"Use kotlinx.serialization for JSON parsing. It is Kotlin-native, supports multiplatform, generates no reflection code, and works with R8/ProGuard without keep rules.",kotlinx.serialization with @Serializable data classes,Gson or Moshi requiring reflection or code generation for every model,"@Serializable data class ArticleDto(val id: String, val title: String, @SerialName('published_at') val publishedAt: Long); val retrofit = Retrofit.Builder().baseUrl(BASE_URL).addConverterFactory(Json.asConverterFactory('application/json'.toMediaType())).build()","data class ArticleDto(val id: String, val title: String, @SerializedName('published_at') val publishedAt: Long) // Gson: requires ProGuard rules + reflection at runtime",Medium,https://github.com/Kotlin/kotlinx.serialization
121
+ Networking,Timeout Configuration,"Configure connect, read, and write timeouts on OkHttpClient. Use per-request timeout for operations that need different limits. Default 10s is too short for uploads and too long for fast APIs.",Explicit timeouts matching expected API response times,Default timeouts causing either premature failures or hung connections,"val okHttp = OkHttpClient.Builder().connectTimeout(10, TimeUnit.SECONDS).readTimeout(30, TimeUnit.SECONDS).writeTimeout(30, TimeUnit.SECONDS).build()",OkHttpClient() // default 10s for everything; upload fails on slow network; health check hangs 10s,Medium,https://square.github.io/okhttp/
122
+ Networking,Network Error Mapping,Map network exceptions to domain-specific error types in the repository layer. Translate HttpException codes and IOException into user-friendly sealed class results.,Repository catches network errors and returns domain Result/sealed class,Letting Retrofit exceptions propagate to ViewModel or UI layer,suspend fun getArticles(): Result<List<Article>> = try { val dto = api.getArticles(); Result.success(dto.map { it.toDomain() }) } catch (e: HttpException) { when (e.code()) { 401 -> Result.failure(AuthExpiredException()); 404 -> Result.failure(NotFoundException()); else -> Result.failure(ServerException(e.code())) } } catch (e: IOException) { Result.failure(NetworkException(e)) },suspend fun getArticles() = api.getArticles() // HttpException crashes ViewModel; IOException shows raw stacktrace to user,High,
123
+ Networking,Certificate Pinning,"Pin server certificates in OkHttp for sensitive apps (banking, health, fintech) to prevent MITM attacks from compromised CAs. Rotate pins before certificate expiry.",CertificatePinner with backup pins for certificate rotation,No pinning in apps handling sensitive financial or health data,"val okHttp = OkHttpClient.Builder().certificatePinner(CertificatePinner.Builder().add('api.mybank.com', 'sha256/AAAA...=').add('api.mybank.com', 'sha256/BBBB...=').build()).build()",OkHttpClient() // no pinning; MITM with rogue CA can intercept banking traffic,High,https://square.github.io/okhttp/features/https/
124
+ OfflineFirst,Single Source of Truth,Make the local database the single source of truth. Network responses write to DB and UI observes DB via Flow. Never expose network data directly to UI.,DB is source of truth. Network -> DB -> UI flow,UI observes network response directly with DB as optional cache,"class ArticleRepository @Inject constructor(private val api: ArticleApi, private val dao: ArticleDao) { fun getArticles(): Flow<List<Article>> = dao.observeAll().map { entities -> entities.map { it.toDomain() } }; suspend fun refresh() { val remote = api.getArticles(); dao.upsertAll(remote.map { it.toEntity() }) } }",class ArticleRepository { suspend fun getArticles(): List<Article> = try { api.getArticles() } catch (e: Exception) { dao.getAll() } // network first; DB fallback; no reactive updates },Critical,
125
+ OfflineFirst,NetworkBoundResource Pattern,Implement the observe-DB-then-fetch-and-update pattern: emit cached data immediately then fetch fresh data and update DB. UI automatically gets both cached and fresh data via Flow.,Emit cached first then fetch and update DB; UI sees both through Flow,Loading spinner until network completes even when cached data is available,fun getArticles(): Flow<Resource<List<Article>>> = flow { emit(Resource.Loading); val cached = dao.getAll(); if (cached.isNotEmpty()) emit(Resource.Success(cached)); try { val fresh = api.getArticles(); dao.upsertAll(fresh); } catch (e: Exception) { if (cached.isEmpty()) emit(Resource.Error(e)) } }.flatMapLatest { dao.observeAll().map { Resource.Success(it) } },fun getArticles(): Flow<List<Article>> = flow { emit(api.getArticles()) } // no cache; empty screen while loading; error = crash,High,
126
+ OfflineFirst,Optimistic Updates,"For user actions (like, bookmark, delete) update the local DB immediately and sync to server in the background. Revert on server failure. This makes the UI feel instant.",Update DB first show result immediately then sync to server in background,Wait for server response before updating UI,"suspend fun toggleBookmark(articleId: String) { dao.toggleBookmark(articleId); try { api.syncBookmark(articleId, dao.isBookmarked(articleId)) } catch (e: Exception) { dao.toggleBookmark(articleId) // revert on failure } }",suspend fun toggleBookmark(id: String) { val result = api.toggleBookmark(id); if (result.isSuccess) dao.toggleBookmark(id) } // 1-2s delay before UI updates; fails silently offline,Medium,
127
+ OfflineFirst,Sync Queue with WorkManager,Queue failed write operations and retry with WorkManager when connectivity returns. Use a pending_operations table to track unsynced changes.,Pending operations table + WorkManager for retry on connectivity,Silently dropping writes when offline or manual retry buttons,"@Entity data class PendingOperation(val id: String, val type: String, val payload: String, val createdAt: Long); class SyncWorker : CoroutineWorker() { override suspend fun doWork(): Result { val pending = dao.getPendingOps(); pending.forEach { api.execute(it); dao.deletePendingOp(it.id) }; return Result.success() } }",suspend fun save(item: Item) { try { api.save(item) } catch (e: IOException) { /* silently lost */ } } // offline writes disappear,Medium,
128
+ Permissions,Runtime Permission with Compose,Use rememberLauncherForActivityResult with ActivityResultContracts.RequestPermission for single permissions or RequestMultiplePermissions for multiple. Check permission state before requesting.,rememberLauncherForActivityResult + check shouldShowRationale before requesting,requestPermissions in Activity with onRequestPermissionsResult callback,"val permissionLauncher = rememberLauncherForActivityResult(ActivityResultContracts.RequestPermission()) { granted -> if (granted) onPermissionGranted() else onPermissionDenied() }; if (ContextCompat.checkSelfPermission(ctx, CAMERA) == GRANTED) { onPermissionGranted() } else { permissionLauncher.launch(CAMERA) }","override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<String>, results: IntArray) { /* deprecated callback pattern */ }",High,https://developer.android.com/training/permissions/requesting
129
+ Permissions,Permission Rationale,Show rationale UI explaining why the permission is needed before requesting. Use shouldShowRequestPermissionRationale to detect when the system suggests showing rationale.,Check shouldShowRationale and show explanation dialog before requesting,Request permission immediately without context; user denies permanently,"val activity = ctx as Activity; when { ContextCompat.checkSelfPermission(ctx, CAMERA) == GRANTED -> openCamera(); activity.shouldShowRequestPermissionRationale(CAMERA) -> showRationaleDialog(onConfirm = { launcher.launch(CAMERA) }); else -> launcher.launch(CAMERA) }",launcher.launch(CAMERA) // no explanation; user taps Deny twice = permanent denial with no recourse,High,https://developer.android.com/training/permissions/requesting#explain
130
+ Permissions,Minimum Permissions,"Request only the permissions your app actually needs. Use Photo Picker instead of READ_MEDIA_IMAGES, use MediaStore for media access, use FileProvider for sharing files.",Use platform APIs that avoid permissions entirely where possible,Requesting broad permissions when narrow alternatives exist,// Photo Picker - no permission needed: val pickMedia = rememberLauncherForActivityResult(PickVisualMedia()) { uri -> uri?.let { processImage(it) } }; pickMedia.launch(PickVisualMediaRequest(ImageOnly)),// Requesting READ_EXTERNAL_STORAGE just to pick one photo: launcher.launch(READ_EXTERNAL_STORAGE),Medium,https://developer.android.com/training/permissions/evaluating
131
+ Paging,Paging 3 PagingSource,Implement PagingSource for loading paginated data from a single source (API or DB). Define load() to fetch pages and return LoadResult.Page with prevKey/nextKey.,PagingSource with proper prevKey/nextKey for bidirectional paging,Manual page tracking in ViewModel with growing MutableList,"class ArticlePagingSource @Inject constructor(private val api: ArticleApi) : PagingSource<Int, ArticleDto>() { override suspend fun load(params: LoadParams<Int>): LoadResult<Int, ArticleDto> { val page = params.key ?: 1; return try { val response = api.getArticles(page, params.loadSize); LoadResult.Page(data = response, prevKey = if (page == 1) null else page - 1, nextKey = if (response.isEmpty()) null else page + 1) } catch (e: Exception) { LoadResult.Error(e) } } }",class ArticleVM : ViewModel() { var page = 1; val articles = mutableListOf<Article>(); fun loadMore() { viewModelScope.launch { articles.addAll(api.getArticles(page++)) } } } // manual pagination; no error/loading states,High,https://developer.android.com/topic/libraries/architecture/paging/v3-paged-data
132
+ Paging,RemoteMediator for Offline Paging,"Use RemoteMediator to implement offline-first pagination: load pages from network into Room, PagingSource reads from Room. Provides seamless offline scrolling with automatic refresh.",RemoteMediator for network-to-DB pagination; PagingSource from Room,Separate online/offline list implementations,"@OptIn(ExperimentalPagingApi::class) val pager = Pager(config = PagingConfig(pageSize = 20), remoteMediator = ArticleRemoteMediator(api, db), pagingSourceFactory = { db.articleDao().pagingSource() })",// Two separate code paths: if (isOnline) api.getPage(n) else db.getAll() // no automatic merge or refresh,Medium,https://developer.android.com/topic/libraries/architecture/paging/v3-network-db
133
+ Paging,collectAsLazyPagingItems,Use collectAsLazyPagingItems() in Compose to connect Pager Flow to LazyColumn. Handle loadState for loading/error indicators at top and bottom of list.,collectAsLazyPagingItems + loadState checks for append/refresh states,Manual loading/error state management alongside LazyColumn,"val articles = viewModel.articles.collectAsLazyPagingItems(); LazyColumn { items(articles.itemCount, key = articles.itemKey { it.id }) { index -> articles[index]?.let { ArticleCard(it) } }; item { if (articles.loadState.append is LoadState.Loading) CircularProgressIndicator() } }",LazyColumn { items(viewModel.list) { ArticleCard(it) }; item { Button(onClick = { viewModel.loadMore() }) { Text('Load More') } } } // manual load-more button,High,https://developer.android.com/topic/libraries/architecture/paging/v3-paged-data#consume-ui
134
+ Paging,PagingConfig Tuning,"Configure PagingConfig with appropriate pageSize (typically 20-30), prefetchDistance for preloading, and enablePlaceholders for stable scroll position.",pageSize=20-30 with prefetchDistance for smooth scrolling,Very small pages causing excessive API calls or very large pages wasting bandwidth,"val pager = Pager(config = PagingConfig(pageSize = 20, prefetchDistance = 5, initialLoadSize = 40, enablePlaceholders = false), pagingSourceFactory = { ArticlePagingSource(api) }).flow.cachedIn(viewModelScope)",Pager(PagingConfig(pageSize = 5)) // too small; constant network requests while scrolling; or pageSize = 200 loads too much,Medium,https://developer.android.com/topic/libraries/architecture/paging/v3-paged-data
135
+ Accessibility,Touch Target Size,"Ensure all interactive elements have a minimum touch target of 48dp x 48dp. Use Modifier.sizeIn(minWidth = 48.dp, minHeight = 48.dp) or Modifier.minimumInteractiveComponentSize().",Minimum 48dp touch targets. Use minimumInteractiveComponentSize() modifier,Small icons or text buttons with touch targets below 48dp,"IconButton(onClick = { onAction() }, modifier = Modifier.minimumInteractiveComponentSize()) { Icon(Icons.Default.Favorite, contentDescription = 'Add to favorites') }","Icon(Icons.Default.Favorite, contentDescription = null, modifier = Modifier.size(16.dp).clickable { }) // 16dp touch target; inaccessible",High,https://developer.android.com/develop/ui/compose/accessibility
136
+ Accessibility,Content Descriptions,Provide meaningful contentDescription for all images and icons. Use null only for purely decorative elements. Screen readers use these to describe UI to visually impaired users.,Descriptive contentDescription for functional elements; null for decorative only,contentDescription = null on all images or generic descriptions,"Icon(Icons.Default.Delete, contentDescription = 'Delete article'); Image(painter = painterResource(R.drawable.hero), contentDescription = null) // purely decorative background","Icon(Icons.Default.Delete, contentDescription = null) // TalkBack says nothing; Icon(Icons.Default.Settings, contentDescription = 'icon') // unhelpful",High,https://developer.android.com/develop/ui/compose/accessibility
137
+ Accessibility,Heading Semantics,Mark section headings with Modifier.semantics { heading() } so TalkBack users can navigate between sections using swipe gestures. Apply to screen titles and section headers.,semantics { heading() } on section titles and screen headers,No heading semantics making long screens difficult to navigate,"Text('Settings', style = MaterialTheme.typography.headlineMedium, modifier = Modifier.semantics { heading() })","Text('Settings', style = MaterialTheme.typography.headlineMedium) // no heading semantics; TalkBack reads as plain text",Medium,https://developer.android.com/develop/ui/compose/accessibility#headings
138
+ Accessibility,Color Contrast,"Ensure text and interactive elements meet WCAG AA contrast ratio: 4.5:1 for normal text, 3:1 for large text. Use Material3 color roles which are designed to meet contrast requirements.",Use Material3 color roles. Test with Accessibility Scanner,Low-contrast custom colors without checking ratios,"Text('Important', color = MaterialTheme.colorScheme.onSurface) // Material3 guarantees contrast; Surface(color = MaterialTheme.colorScheme.errorContainer) { Text('Error', color = MaterialTheme.colorScheme.onErrorContainer) }","Text('Warning', color = Color(0xFFFFCC00), modifier = Modifier.background(Color.White)) // yellow on white = 1.5:1 ratio; unreadable",Medium,https://developer.android.com/develop/ui/compose/accessibility
139
+ Security,Encrypted Storage,Use EncryptedSharedPreferences or EncryptedFile from AndroidX Security for storing sensitive data like tokens and API keys. Never store secrets in plain SharedPreferences or BuildConfig.,EncryptedSharedPreferences for tokens/secrets,Plain SharedPreferences or BuildConfig for sensitive data,"val masterKey = MasterKey.Builder(ctx).setKeyScheme(MasterKey.KeyScheme.AES256_GCM).build(); val securePrefs = EncryptedSharedPreferences.create(ctx, 'secret_prefs', masterKey, PrefKeyEncryptionScheme.AES256_SIV, PrefValueEncryptionScheme.AES256_GCM); securePrefs.edit().putString('auth_token', token).apply()","val prefs = getSharedPreferences('prefs', MODE_PRIVATE); prefs.edit().putString('auth_token', token).apply() // plaintext on disk; root access = stolen token",Critical,https://developer.android.com/topic/security/data
140
+ Security,Network Security Config,"Configure network_security_config.xml to enforce HTTPS, disable cleartext traffic, and pin certificates. Debug config can allow cleartext for local development only.",network_security_config with cleartextTrafficPermitted=false in production,Allowing cleartext HTTP in production or no security config,<!-- res/xml/network_security_config.xml --> <network-security-config><base-config cleartextTrafficPermitted='false'><trust-anchors><certificates src='system' /></trust-anchors></base-config><debug-overrides><trust-anchors><certificates src='user' /></trust-anchors></debug-overrides></network-security-config>,android:usesCleartextTraffic='true' in manifest // all HTTP traffic unencrypted in production,High,https://developer.android.com/privacy-and-security/security-config
141
+ Security,Biometric Authentication,Use BiometricPrompt from AndroidX Biometric for fingerprint/face authentication. Use CryptoObject for cryptographic operations tied to biometric auth.,BiometricPrompt with CryptoObject for sensitive operations,Custom fingerprint scanning or password-only for sensitive actions,"val biometricPrompt = BiometricPrompt(fragment, executor, object : AuthenticationCallback() { override fun onAuthenticationSucceeded(result: AuthenticationResult) { val cipher = result.cryptoObject?.cipher; decryptSensitiveData(cipher) } }); biometricPrompt.authenticate(promptInfo, BiometricPrompt.CryptoObject(cipher))",// Only username/password for banking transactions; or FingerprintManager (deprecated API),Medium,https://developer.android.com/training/sign-in/biometric-auth
142
+ Security,ProGuard Obfuscation,"Enable code shrinking and obfuscation with R8 in release builds. Write keep rules only for reflection-used classes (serialization models, JNI). Obfuscation makes reverse engineering harder.",R8 with minifyEnabled=true and proper keep rules for release,No obfuscation in release or keeping everything,"android { buildTypes { release { isMinifyEnabled = true; isShrinkResources = true; proguardFiles(getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro') } } }",buildTypes { release { isMinifyEnabled = false } } // full source code visible in APK; or -keep class ** { *; } keeping everything,High,https://developer.android.com/build/shrink-code
143
+ Security,SQL Injection Prevention,Always use parameterized queries with Room @Query or SupportSQLiteDatabase.query(). Never concatenate user input into SQL strings.,Parameterized queries via Room @Query with :param placeholders,String concatenation in raw SQL queries,@Query('SELECT * FROM users WHERE name = :searchName') fun findByName(searchName: String): Flow<List<UserEntity>> // Room parameterizes automatically,"val cursor = db.rawQuery('SELECT * FROM users WHERE name = ''' + userInput + '''', null) // SQL injection vulnerability",Critical,
144
+ ErrorHandling,Result Wrapper Pattern,"Define a sealed interface Result<T> with Success, Error, and Loading states. All repository methods return this type. ViewModel maps to UiState.",Sealed Result type returned from repository; ViewModel maps to UiState,Throwing exceptions from repository or using nullable return types,sealed interface Result<out T> { data class Success<T>(val data: T) : Result<T>; data class Error(val exception: Throwable) : Result<Nothing>; data object Loading : Result<Nothing> }; class ArticleRepo { suspend fun getArticle(id: String): Result<Article> = try { Result.Success(api.get(id).toDomain()) } catch (e: Exception) { Result.Error(e) } },suspend fun getArticle(id: String): Article? = try { api.get(id) } catch (e: Exception) { null } // null = error or not found? no error info; or throwing raw exceptions to ViewModel,High,
145
+ ErrorHandling,Retry with Exponential Backoff,Implement retry logic with exponential backoff for transient network failures. Limit retry count. Use Flow.retryWhen or custom retry loop.,Exponential backoff with max retries for transient failures,Infinite retries or immediate retries hammering a failing server,"suspend fun <T> retryWithBackoff(maxRetries: Int = 3, initialDelayMs: Long = 1000, block: suspend () -> T): T { var currentDelay = initialDelayMs; repeat(maxRetries) { attempt -> try { return block() } catch (e: IOException) { if (attempt == maxRetries - 1) throw e; delay(currentDelay); currentDelay *= 2 } }; throw IllegalStateException() }",suspend fun fetchWithRetry() { while (true) { try { return api.fetch() } catch (e: Exception) { delay(100) } } } // infinite retry; 100ms = hammering server,Medium,
146
+ ErrorHandling,Global Error Handler,Set up a centralized error handling mechanism using CoroutineExceptionHandler or a shared error channel. Avoid duplicate error handling logic across ViewModels.,Shared error handling utility or base pattern; centralized crash reporting,try-catch in every ViewModel method with duplicated error-to-message logic,class ErrorMapper @Inject constructor() { fun toUserMessage(e: Throwable): String = when (e) { is HttpException -> when (e.code()) { 401 -> 'Session expired'; 403 -> 'Access denied'; 500 -> 'Server error'; else -> 'Network error' }; is IOException -> 'No internet connection'; else -> 'Something went wrong' } },// Every ViewModel: try { repo.fetch() } catch (e: HttpException) { if (e.code() == 401) 'Session expired' else if (e.code() == 500) 'Server error' ... } // duplicated in 20 ViewModels,Medium,
147
+ AppStartup,App Startup Library,Use the App Startup library to initialize components lazily at app start using a single ContentProvider instead of multiple. Reduces startup time by batching initialization.,App Startup Initializer for SDK initialization with dependency ordering,Multiple ContentProviders each SDK registering its own,class AnalyticsInitializer : Initializer<Analytics> { override fun create(ctx: Context): Analytics { return Analytics.init(ctx) }; override fun dependencies() = listOf(WorkManagerInitializer::class.java) },// Each SDK adds its own ContentProvider in manifest; 5 ContentProviders = 5x provider creation overhead at startup,Medium,https://developer.android.com/topic/libraries/app-startup
148
+ AppStartup,Splash Screen API,Use the SplashScreen API (Android 12+) for a consistent branded launch experience. Avoid custom splash activities which add unnecessary Activity creation overhead.,SplashScreen API with installSplashScreen() in theme,Custom SplashActivity with a delay before navigating to main,class MainActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { val splashScreen = installSplashScreen(); splashScreen.setKeepOnScreenCondition { viewModel.isLoading.value }; super.onCreate(savedInstanceState) } },"class SplashActivity : AppCompatActivity() { override fun onCreate(s: Bundle?) { super.onCreate(s); setContentView(R.layout.splash); Handler().postDelayed({ startActivity(Intent(this, MainActivity::class.java)); finish() }, 2000) } // fake 2s delay; extra Activity overhead }",Medium,https://developer.android.com/develop/ui/views/launch/splash-screen
149
+ AppStartup,Lazy Initialization,Defer initialization of expensive objects until first use. Use Kotlin lazy delegation or Hilt's Provider<T> for on-demand creation. Avoids loading unused features at startup.,lazy {} or Provider<T> for expensive rarely-used dependencies,Initializing all dependencies eagerly in Application.onCreate(),val heavyAnalytics by lazy { HeavyAnalyticsEngine.init(ctx) } // only initialized when first accessed,// In Application.onCreate(): HeavyAnalyticsEngine.init(this); CrashReporter.init(this); FeatureFlags.init(this); AdSdk.init(this); // everything upfront even if user never uses ads,Medium,
150
+ AppStartup,Startup Tracing,Use Macrobenchmark with StartupTimingMetric to measure cold/warm/hot start times. Profile with Perfetto trace to identify initialization bottlenecks.,Macrobenchmark startup tests in CI; Perfetto traces for profiling,Guessing startup performance without measurement,"@RunWith(AndroidJUnit4::class) class StartupBenchmark { @get:Rule val rule = MacrobenchmarkRule(); @Test fun startupCompilationFull() { rule.measureRepeated(packageName = PACKAGE, metrics = listOf(StartupTimingMetric()), iterations = 10, startupMode = StartupMode.COLD) { pressHome(); startActivityAndWait() } }",// No startup benchmarks; 'feels fast enough' without data; surprised by Play Store vitals showing 3s cold start,Medium,https://developer.android.com/topic/performance/benchmarking/macrobenchmark-overview
151
+ Animation,AnimatedVisibility,"Use AnimatedVisibility for enter/exit animations on composables. Customize with fadeIn/fadeOut, slideIn/slideOut, expandIn/shrinkOut transitions.",AnimatedVisibility with appropriate enter/exit transitions,Conditional if/else with no animation,"AnimatedVisibility(visible = showDetails, enter = fadeIn() + expandVertically(), exit = fadeOut() + shrinkVertically()) { DetailContent() }",if (showDetails) { DetailContent() } // instant appear/disappear; jarring UX,Medium,https://developer.android.com/develop/ui/compose/animation/composables-modifiers
152
+ Animation,animate*AsState,"Use animateFloatAsState, animateColorAsState, animateDpAsState for smooth value transitions. Compose automatically animates between old and new values.",animate*AsState for property transitions with customizable animationSpec,Instant value changes with no interpolation,"val backgroundColor by animateColorAsState(targetValue = if (isSelected) MaterialTheme.colorScheme.primaryContainer else MaterialTheme.colorScheme.surface, animationSpec = tween(300)); Surface(color = backgroundColor) { content() }",Surface(color = if (isSelected) primaryContainer else surface) { content() } // instant color switch; no transition,Medium,https://developer.android.com/develop/ui/compose/animation/value-based
153
+ Animation,updateTransition,Use updateTransition for coordinating multiple animations that should run together based on a state change. Groups related animations with a single state source.,updateTransition for multi-property animations tied to one state,Multiple independent animate*AsState with manual state synchronization,"val transition = updateTransition(targetState = tabState, label = 'tab'); val indicatorOffset by transition.animateDp(label = 'offset') { state -> state.offset }; val indicatorWidth by transition.animateDp(label = 'width') { state -> state.width }; val indicatorColor by transition.animateColor(label = 'color') { state -> state.color }",val offset by animateDpAsState(state.offset); val width by animateDpAsState(state.width); val color by animateColorAsState(state.color) // three independent animations; may desync,Low,https://developer.android.com/develop/ui/compose/animation/value-based#updateTransition
154
+ Animation,Motion Specs,"Use spring() for natural feeling interactions and tween() for precise timing. Spring for user-triggered animations (press, drag); tween for system animations (enter/exit).",spring for interactive; tween for predictable; avoid hard-coded durations in multiple places,LinearEasing tween for everything or no animation spec,"val scale by animateFloatAsState(targetValue = if (pressed) 0.95f else 1f, animationSpec = spring(dampingRatio = Spring.DampingRatioMediumBouncy, stiffness = Spring.StiffnessLow))","val scale by animateFloatAsState(if (pressed) 0.95f else 1f, tween(100, easing = LinearEasing)) // robotic linear feel for press interaction",Low,https://developer.android.com/develop/ui/compose/animation/customize
155
+ Widgets,Glance for App Widgets,Use Jetpack Glance to build app widgets with Compose-like syntax. Glance translates composables to RemoteViews automatically. Replace legacy RemoteViews XML.,Glance composables for app widgets,Manual RemoteViews with XML layouts,"class ArticleWidget : GlanceAppWidget() { override suspend fun provideGlance(ctx: Context, id: GlanceId) { provideContent { val articles = getArticles(); LazyColumn { items(articles) { article -> Text(article.title) } } } } }","class ArticleWidget : AppWidgetProvider() { override fun onUpdate(ctx: Context, mgr: AppWidgetManager, ids: IntArray) { val views = RemoteViews(ctx.packageName, R.layout.widget); views.setTextViewText(R.id.title, 'Hello'); mgr.updateAppWidget(ids, views) } } // XML layouts; no Compose",Medium,https://developer.android.com/develop/ui/compose/glance
156
+ Widgets,GlanceStateDefinition,Use GlanceStateDefinition with DataStore to persist widget state. Update widget data in a CoroutineWorker and trigger widget refresh.,DataStore-backed state + WorkManager for background updates,SharedPreferences for widget state or fetching data in onUpdate,class ArticleWidgetReceiver : GlanceAppWidgetReceiver() { override val glanceAppWidget = ArticleWidget() }; class RefreshWorker : CoroutineWorker() { override suspend fun doWork(): Result { val articles = repo.fetchLatest(); updateWidgetState(articles); ArticleWidget().updateAll(ctx); return Result.success() } },"override fun onUpdate(...) { val data = prefs.getString('cached', ''); /* parse and display stale data; no background refresh */ }",Medium,https://developer.android.com/develop/ui/compose/glance/create-app-widget
157
+ Widgets,Widget Size Classes,"Handle different widget sizes using SizeMode.Responsive with multiple size layouts. Provide appropriate content density for small, medium, and large widget placements.",SizeMode.Responsive with breakpoints for different widget sizes,Fixed single-size layout that looks wrong when resized,"override val sizeMode = SizeMode.Responsive(setOf(DpSize(120.dp, 120.dp), DpSize(250.dp, 120.dp), DpSize(250.dp, 250.dp))); @Composable override fun Content() { val size = LocalSize.current; when { size.width < 200.dp -> CompactWidget(); size.height < 200.dp -> WideWidget(); else -> FullWidget() } }",override val sizeMode = SizeMode.Single // same layout stretched/squished for all widget sizes,Low,https://developer.android.com/develop/ui/compose/glance/create-app-widget#size
158
+ Notifications,Notification Channels,Create notification channels (required on Android 8+) organized by user-facing category. Allow users to control importance per channel. Create channels in Application.onCreate().,Separate channels per notification type created at app start,Single default channel for all notifications or creating channels lazily,"fun createChannels(ctx: Context) { val mgr = ctx.getSystemService<NotificationManager>(); mgr.createNotificationChannel(NotificationChannel('messages', 'Messages', IMPORTANCE_HIGH).apply { description = 'New message notifications' }); mgr.createNotificationChannel(NotificationChannel('updates', 'Updates', IMPORTANCE_LOW).apply { description = 'App update notifications' }) }","// Single channel: NotificationChannel('default', 'Default', IMPORTANCE_DEFAULT); // or no channel creation = notifications silently fail on Android 8+",High,https://developer.android.com/develop/ui/views/notifications/channels
159
+ Notifications,NotificationCompat Builder,"Use NotificationCompat.Builder from AndroidX for backward-compatible notifications. Set required fields: smallIcon, contentTitle, contentText, channelId. Add PendingIntent for tap action.",NotificationCompat.Builder with all required fields + PendingIntent + channelId,Notification.Builder (platform) without compat or missing required fields,"val intent = PendingIntent.getActivity(ctx, 0, Intent(ctx, MainActivity::class.java).apply { flags = Intent.FLAG_ACTIVITY_SINGLE_TOP; putExtra('articleId', id) }, PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT); val notification = NotificationCompat.Builder(ctx, 'messages').setSmallIcon(R.drawable.ic_notification).setContentTitle(title).setContentText(body).setContentIntent(intent).setAutoCancel(true).build()","Notification.Builder(ctx, 'messages').setContentTitle(title).build() // no smallIcon = crash on some devices; no PendingIntent = tap does nothing; no compat = API differences",High,https://developer.android.com/develop/ui/views/notifications/build-notification
160
+ Notifications,POST_NOTIFICATIONS Permission,Request POST_NOTIFICATIONS runtime permission on Android 13+ before showing notifications. Without it notifications are silently blocked.,Request POST_NOTIFICATIONS on Android 13+ at an appropriate moment,Showing notifications without permission on Android 13+ = silently dropped,"if (Build.VERSION.SDK_INT >= 33) { val launcher = rememberLauncherForActivityResult(RequestPermission()) { granted -> if (granted) scheduleNotifications() }; LaunchedEffect(Unit) { if (ContextCompat.checkSelfPermission(ctx, POST_NOTIFICATIONS) != GRANTED) launcher.launch(POST_NOTIFICATIONS) } }",// No permission request; notifications silently fail on Android 13+; user never sees them,High,https://developer.android.com/develop/ui/views/notifications/notification-permission
161
+ DeepLinks,Navigation Deep Links,Define deep links in Navigation Compose using deepLinks parameter on composable destinations. Handle both custom scheme and https links.,Deep links declared on composable destinations with proper URI patterns,Manual intent parsing in Activity with complex when/if chains,composable<ArticleRoute>(deepLinks = listOf(navDeepLink<ArticleRoute>(basePath = 'https://myapp.com/articles'))) { entry -> val route = entry.toRoute<ArticleRoute>(); ArticleScreen(route.id) },// In MainActivity: override fun onNewIntent(intent: Intent) { val uri = intent.data; if (uri?.path?.startsWith('/articles') == true) { val id = uri.lastPathSegment; navController.navigate('article/$id') } } // fragile manual parsing,High,https://developer.android.com/guide/navigation/design/deep-link
162
+ DeepLinks,App Links Verification,Set up Android App Links with Digital Asset Links for verified https deep links. This makes your app the default handler without disambiguation dialog.,Verified App Links with assetlinks.json on server,Unverified deep links showing disambiguation dialog every time,<!-- AndroidManifest.xml --> <intent-filter android:autoVerify='true'><action android:name='android.intent.action.VIEW' /><category android:name='android.intent.category.DEFAULT' /><category android:name='android.intent.category.BROWSABLE' /><data android:scheme='https' android:host='myapp.com' /></intent-filter>; /* Plus .well-known/assetlinks.json on server */,<!-- No autoVerify --> <intent-filter><data android:scheme='https' android:host='myapp.com' /></intent-filter> // disambiguation dialog every time; poor UX,Medium,https://developer.android.com/training/app-links/verify-android-applinks
163
+ DeepLinks,Deep Link Testing,Test deep links using adb commands and Navigation testing APIs. Verify all deep link routes resolve correctly in both cold start and warm start scenarios.,adb shell am start for manual testing; Navigation test for automated,Only testing deep links by clicking actual links in browser,"adb shell am start -a android.intent.action.VIEW -d 'https://myapp.com/articles/123' com.myapp; /* Plus automated: */ @Test fun articleDeepLink_navigatesToDetail() { val navController = TestNavHostController(ctx); navController.handleDeepLink(Intent().apply { data = Uri.parse('https://myapp.com/articles/123') }); assertEquals(ArticleRoute(id = '123'), navController.currentDestination) }",// Only test by manually opening links in Chrome; no automated testing; broken deep links found by users,Medium,https://developer.android.com/guide/navigation/design/deep-link
164
+ CICD,GitHub Actions Android CI,"Set up GitHub Actions CI with build, lint, and test steps. Cache Gradle dependencies for faster builds. Run on pull requests to catch issues early.",CI pipeline: build + lint + test on every PR with Gradle caching,No CI; manual builds on developer machines only,# .github/workflows/android.yml: jobs: build: runs-on: ubuntu-latest; steps: - uses: actions/checkout@v4; - uses: actions/setup-java@v4 with: java-version: 17; - uses: gradle/actions/setup-gradle@v3; - run: ./gradlew build lintDebug testDebugUnitTest,No CI pipeline; bugs found after merging to main; manual './gradlew build' before each commit,Medium,
165
+ CICD,Gradle Build Cache,Enable Gradle build cache and configuration cache for faster CI and local builds. Remote cache shared across team members eliminates redundant compilation.,Build cache + configuration cache enabled; remote cache for team,No caching; full rebuild every CI run,# gradle.properties: org.gradle.caching=true; org.gradle.configuration-cache=true; # settings.gradle.kts: buildCache { local { isEnabled = true }; remote<HttpBuildCache> { url = uri('https://cache.myteam.com/'); isPush = System.getenv('CI') != null } },# Every CI run: 8-minute full rebuild; every developer rebuilds everything locally,Medium,https://developer.android.com/build/optimize-your-build
166
+ CICD,Automated Release with Signing,Automate APK/AAB signing in CI using environment secrets for keystore. Never commit keystores to version control. Use bundletool for local AAB testing.,Keystore secrets in CI environment variables; AAB for Play Store,Keystore committed to git or manual signing on developer machine,# CI: KEYSTORE_BASE64 and KEY_PASSWORD as secrets; android { signingConfigs { release { storeFile = file(System.getenv('KEYSTORE_PATH')); storePassword = System.getenv('STORE_PASSWORD'); keyAlias = System.getenv('KEY_ALIAS'); keyPassword = System.getenv('KEY_PASSWORD') } } },// keystore.jks committed to git; passwords in gradle.properties pushed to remote // OR: manual signing on one developers machine before each release,High,
167
+ CICD,Detekt and Ktlint,"Integrate static analysis (Detekt for code smells, Ktlint for formatting) in CI. Fail the build on violations to maintain code quality standards.",Detekt + Ktlint in CI pipeline failing on violations,No static analysis; code style debates in PR reviews,// build.gradle.kts: plugins { id('io.gitlab.arturbosch.detekt'); id('org.jlleitschuh.gradle.ktlint') }; detekt { config.setFrom('config/detekt.yml'); buildUponDefaultConfig = true }; // CI step: ./gradlew detekt ktlintCheck,No automated style checks; inconsistent formatting across team; 50-comment PR reviews about spacing,Medium,https://detekt.dev/docs/intro
168
+ Android 10 (API 29),Scoped Storage,Apps targeting API 29+ get scoped access to external storage by default limiting access to app-specific directories and media the app created. Apps can temporarily opt out via requestLegacyExternalStorage=true.,Use MediaStore or SAF for shared files; app-specific dirs for private files,Directly access arbitrary files on external storage via file paths,getExternalFilesDir(null) for private; MediaStore.Images for shared photos,File('/sdcard/Download/file.txt') // direct path access fails on API 29+,Critical,https://developer.android.com/about/versions/10/privacy/changes
169
+ Android 10 (API 29),Background Activity Restrictions,Apps running in the background cannot start activities directly on API 29+. Must use high-priority notifications with full-screen intents instead.,Use high-priority notification with fullScreenIntent for urgent alerts,Start activities from background services or broadcast receivers,"NotificationCompat.Builder(ctx, 'urgent').setFullScreenIntent(pendingIntent, true).setPriority(PRIORITY_HIGH).build()",startActivity(intent) // from background service; blocked on API 29+,High,https://developer.android.com/guide/components/activities/background-starts
170
+ Android 10 (API 29),Background Location Permission,Apps must request ACCESS_BACKGROUND_LOCATION to access location in the background on API 29+. Foreground services accessing location must declare foregroundServiceType=location.,Request ACCESS_BACKGROUND_LOCATION separately; declare foregroundServiceType=location,Assume location access works in background with only ACCESS_FINE_LOCATION,<service android:foregroundServiceType='location' />; requestPermissions(arrayOf(ACCESS_BACKGROUND_LOCATION)),// Only ACCESS_FINE_LOCATION requested; location stops working in background,High,https://developer.android.com/develop/sensors-and-location/location/permissions
171
+ Android 11 (API 30),Scoped Storage Enforcement,Scoped storage is fully enforced on API 30+. The requestLegacyExternalStorage manifest flag is ignored. Apps must use MediaStore SAF or app-specific directories for all file access.,Use MediaStore APIs SAF or app-specific directories exclusively,Rely on requestLegacyExternalStorage flag; it is ignored on API 30+,"contentResolver.query(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, projection, selection, null, null)",android:requestLegacyExternalStorage='true' // ignored on API 30+; File() paths fail,Critical,https://developer.android.com/about/versions/11/privacy/storage
172
+ Android 11 (API 30),Package Visibility,Apps targeting API 30+ cannot query all installed packages by default. Must declare specific packages or intent filters using the queries element in the manifest to discover other apps.,Declare queries element in manifest for packages your app needs to interact with,Call queryIntentActivities or getInstalledPackages without queries declaration,<queries><package android:name='com.example.app' /><intent><action android:name='android.intent.action.SEND' /></intent></queries>,// packageManager.getInstalledPackages(0) returns empty on API 30+ without queries,High,https://developer.android.com/about/versions/11/privacy/package-visibility
173
+ Android 11 (API 30),One-Time Permissions,Users can grant location camera and microphone permissions as Only this time on API 30+. Permission is auto-revoked when the app activity is no longer visible and no foreground service is running.,Handle permission being revoked between sessions; re-check before use,Assume permission persists after a single grant,if (checkSelfPermission(CAMERA) == GRANTED) { openCamera() } else { requestPermission() },// Assume camera permission granted once lasts forever; crash when revoked,Medium,https://developer.android.com/about/versions/11/privacy/permissions
174
+ Android 11 (API 30),Foreground Service Camera Mic Type,Apps targeting API 30+ that access camera or microphone from a foreground service must declare foregroundServiceType camera or microphone in manifest.,Declare foregroundServiceType=camera|microphone for services using these sensors,Access camera or microphone from foreground service without declaring type,<service android:name='.RecordingService' android:foregroundServiceType='camera|microphone' />,<service android:name='.RecordingService' /> // no type; camera/mic access denied from foreground service,High,https://developer.android.com/about/versions/11/privacy/foreground-services
175
+ Android 12 (API 31),Exported Components Required,Any activity service or broadcast receiver with intent filters must explicitly set android:exported=true or false. Missing this causes an installation error on API 31+.,Explicitly set android:exported on all components with intent-filters,Omit android:exported attribute on components with intent filters,<activity android:name='.MainActivity' android:exported='true'><intent-filter>...</intent-filter></activity>,<activity android:name='.MainActivity'><intent-filter>...</intent-filter></activity> // install fails on API 31+,Critical,https://developer.android.com/about/versions/12/behavior-changes-12
176
+ Android 12 (API 31),Exact Alarm Permission,Apps must declare SCHEDULE_EXACT_ALARM and verify via canScheduleExactAlarms() before calling setExact() or setExactAndAllowWhileIdle() on AlarmManager.,Declare SCHEDULE_EXACT_ALARM; check canScheduleExactAlarms() before use,Call setExact() without permission; crashes on API 31+,"if (alarmManager.canScheduleExactAlarms()) { alarmManager.setExact(AlarmManager.RTC_WAKEUP, triggerTime, pendingIntent) }","alarmManager.setExact(AlarmManager.RTC_WAKEUP, triggerTime, pendingIntent) // SecurityException on API 31+",High,https://developer.android.com/develop/background-work/services/alarms
177
+ Android 12 (API 31),SplashScreen API Migration,Android 12 applies a system default splash screen on cold/warm starts for all apps. Apps must migrate to the SplashScreen API to avoid duplicate splash screens.,Use installSplashScreen() and migrate from custom splash Activity,Keep custom SplashActivity with delay; causes duplicate splash on Android 12+,installSplashScreen().setKeepOnScreenCondition { viewModel.isLoading.value },// Custom SplashActivity + Handler.postDelayed = user sees TWO splash screens on Android 12+,High,https://developer.android.com/develop/ui/views/launch/splash-screen/migrate
178
+ Android 12 (API 31),Approximate Location Option,Users can grant only approximate (coarse) location even when apps request fine location on API 31+. Apps must declare both ACCESS_FINE_LOCATION and ACCESS_COARSE_LOCATION.,Declare both ACCESS_FINE_LOCATION and ACCESS_COARSE_LOCATION; handle approximate grants,Request only ACCESS_FINE_LOCATION; system ignores the request on API 31+,"requestPermissions(arrayOf(ACCESS_FINE_LOCATION, ACCESS_COARSE_LOCATION), REQUEST_CODE)",requestPermissions(arrayOf(ACCESS_FINE_LOCATION)) // system ignores; no permission dialog shown on API 31+,High,https://developer.android.com/about/versions/12/behavior-changes-all
179
+ Android 12 (API 31),New Bluetooth Permissions,Apps targeting API 31+ must use BLUETOOTH_SCAN BLUETOOTH_CONNECT and BLUETOOTH_ADVERTISE instead of legacy BLUETOOTH and BLUETOOTH_ADMIN. Location no longer required for scanning if neverForLocation is set.,Use new granular Bluetooth permissions; set neverForLocation on BLUETOOTH_SCAN,Use deprecated BLUETOOTH/BLUETOOTH_ADMIN permissions on API 31+,<uses-permission android:name='android.permission.BLUETOOTH_CONNECT' />; <uses-permission android:name='android.permission.BLUETOOTH_SCAN' android:usesPermissionFlags='neverForLocation' />,<uses-permission android:name='android.permission.BLUETOOTH' /> // deprecated; does nothing on API 31+,High,https://developer.android.com/develop/connectivity/bluetooth/bt-permissions
180
+ Android 12 (API 31),PendingIntent Mutability Flag,Every PendingIntent must specify FLAG_MUTABLE or FLAG_IMMUTABLE on API 31+. Creating a PendingIntent without one of these flags throws IllegalArgumentException.,Always specify FLAG_IMMUTABLE (default) or FLAG_MUTABLE when needed,Create PendingIntent without mutability flag; crashes on API 31+,"PendingIntent.getActivity(ctx, 0, intent, PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT)","PendingIntent.getActivity(ctx, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT) // IllegalArgumentException on API 31+",Critical,https://developer.android.com/about/versions/12/behavior-changes-12
181
+ Android 13 (API 33),Photo Picker,Android 13 introduces a system photo picker for selecting images and videos without broad storage permissions. No runtime permissions needed to use it.,Use the system Photo Picker via PickVisualMedia contract for media selection,Request READ_EXTERNAL_STORAGE for simple media selection; use custom gallery pickers,val pickMedia = rememberLauncherForActivityResult(PickVisualMedia()) { uri -> uri?.let { processImage(it) } }; pickMedia.launch(PickVisualMediaRequest(PickVisualMedia.ImageOnly)),// Custom file picker with READ_EXTERNAL_STORAGE; or ACTION_GET_CONTENT for media selection,High,https://developer.android.com/training/data-storage/shared/photo-picker
182
+ Android 13 (API 33),Granular Media Permissions,READ_EXTERNAL_STORAGE is replaced by READ_MEDIA_IMAGES READ_MEDIA_VIDEO and READ_MEDIA_AUDIO on API 33+. Each must be requested individually based on the type of media accessed.,Request specific READ_MEDIA_* permissions matching the media types your app uses,Request READ_EXTERNAL_STORAGE on API 33+; it is denied automatically,"if (Build.VERSION.SDK_INT >= 33) requestPermissions(arrayOf(READ_MEDIA_IMAGES, READ_MEDIA_VIDEO))",requestPermissions(arrayOf(READ_EXTERNAL_STORAGE)) // auto-denied on API 33+; no media access,Critical,https://developer.android.com/about/versions/13/behavior-changes-13
183
+ Android 13 (API 33),POST_NOTIFICATIONS Permission,Apps targeting API 33+ must request POST_NOTIFICATIONS runtime permission before sending notifications. Without it all non-exempt notifications are silently blocked.,Request POST_NOTIFICATIONS at an appropriate moment before sending notifications,Skip permission request; notifications silently fail on API 33+,"if (Build.VERSION.SDK_INT >= 33) { requestPermissions(arrayOf(Manifest.permission.POST_NOTIFICATIONS), REQUEST_CODE) }",// Show notifications without POST_NOTIFICATIONS permission; users never see them on API 33+,Critical,https://developer.android.com/develop/ui/views/notifications/notification-permission
184
+ Android 13 (API 33),Per-App Language Preferences,Android 13 adds system APIs for per-app language that let users set a different language per app. Apps should migrate from custom in-app language pickers to LocaleManager.,Use LocaleManager.setApplicationLocales() / getApplicationLocales() and declare localeConfig in manifest,Use custom SharedPreferences-based locale switching or AppCompatDelegate.setApplicationLocales only,<locale-config> in res/xml; LocaleManager.getApplicationLocales() for reading user preference,// Custom locale logic: AppCompatDelegate.setApplicationLocales() without system integration; no localeConfig,Medium,https://developer.android.com/guide/topics/resources/app-languages
185
+ Android 13 (API 33),Exact Alarm Pre-Grant Removed,SCHEDULE_EXACT_ALARM is no longer pre-granted for fresh installs on API 33+. Apps that are not alarm/timer/calendar apps should migrate to USE_EXACT_ALARM or inexact alarms.,Use USE_EXACT_ALARM for alarm-clock apps; migrate others to inexact alarms or WorkManager,Assume SCHEDULE_EXACT_ALARM is granted by default on API 33+,// For alarm apps: <uses-permission android:name='android.permission.USE_EXACT_ALARM' />; // For others: alarmManager.setWindow() or WorkManager,// SCHEDULE_EXACT_ALARM assumed granted; alarms silently fail on fresh install API 33+,High,https://developer.android.com/about/versions/14/changes/schedule-exact-alarms
186
+ Android 14 (API 34),Foreground Service Types Required,All foreground services must declare a specific foregroundServiceType in the manifest on API 34+. Starting a service without a declared type throws MissingForegroundServiceTypeException.,Declare foregroundServiceType for every foreground service in manifest,Start foreground service without declaring foregroundServiceType; crashes on API 34+,"<service android:name='.SyncService' android:foregroundServiceType='dataSync' />; startForeground(id, notification, FOREGROUND_SERVICE_TYPE_DATA_SYNC)",<service android:name='.SyncService' /> // MissingForegroundServiceTypeException on API 34+,Critical,https://developer.android.com/about/versions/14/changes/fgs-types-required
187
+ Android 14 (API 34),Partial Photo Video Access,Users can grant access to selected photos/videos only instead of the entire library on API 34+. Apps should request READ_MEDIA_VISUAL_USER_SELECTED and handle partial grants with re-selection.,Request READ_MEDIA_VISUAL_USER_SELECTED; handle partial grants and prompt re-selection when needed,Assume full media library access after permission grant on API 34+,"requestPermissions(arrayOf(READ_MEDIA_IMAGES, READ_MEDIA_VISUAL_USER_SELECTED)); // Handle PERMISSION_GRANTED vs partial",// Assume all photos accessible after grant; app shows empty gallery when user selected only a few photos,High,https://developer.android.com/about/versions/14/changes/partial-photo-video-access
188
+ Android 14 (API 34),Screenshot Detection API,A new privacy-preserving API lets apps register per-activity callbacks when users take a screenshot. The callback notifies the app but does not provide the screenshot image itself.,Register Activity.ScreenCaptureCallback for screenshot-sensitive screens,Use FileObserver on screenshot directories or no screenshot awareness,"registerScreenCaptureCallback(mainExecutor, callback); // callback.onScreenCaptured() triggers UI notice",// FileObserver on /Pictures/Screenshots; or no detection at all for sensitive banking screens,Medium,https://developer.android.com/about/versions/14/features/screenshot-detection
189
+ Android 14 (API 34),Full-Screen Intent Restriction,USE_FULL_SCREEN_INTENT is restricted to calling and alarm apps only on API 34+. Google Play revokes this permission for non-qualifying apps. Check canUseFullScreenIntent() before use.,Check NotificationManager.canUseFullScreenIntent(); only use for calls/alarms,Use USE_FULL_SCREEN_INTENT for marketing notifications or non-urgent alerts,"if (notificationManager.canUseFullScreenIntent()) { builder.setFullScreenIntent(pendingIntent, true) }",// USE_FULL_SCREEN_INTENT for promotional notifications; permission revoked by Play on API 34+,High,https://developer.android.com/about/versions/14/behavior-changes-14
190
+ Android 14 (API 34),Implicit Intent Restrictions,Implicit intents are only delivered to exported components on API 34+. Sending an implicit intent to a non-exported component or mutable PendingIntent without specifying component throws an exception.,Use explicit intents for internal components; specify component/package on PendingIntents,Send implicit intents to internal components without exported=true,"val intent = Intent(ctx, MyReceiver::class.java) // explicit intent for internal component",sendBroadcast(Intent('com.myapp.ACTION')) // implicit intent to internal receiver; exception on API 34+,High,https://developer.android.com/about/versions/14/behavior-changes-14
191
+ Android 14 (API 34),Minimum targetSdkVersion 23,Apps with targetSdkVersion lower than 23 cannot be installed on Android 14+ devices. This ensures all apps support the runtime permissions model from Android 6.0.,Ensure targetSdkVersion >= 23 (ideally latest); follow Play Store target API requirements,Target SDK versions below 23; app cannot be installed on Android 14+,android { defaultConfig { targetSdk = 35 } },android { defaultConfig { targetSdk = 22 } } // cannot be installed on Android 14+ devices,Critical,https://developer.android.com/about/versions/14/behavior-changes-14
192
+ Android 15 (API 35),Edge-to-Edge Enforced,Apps targeting API 35 are displayed edge-to-edge by default drawing behind status bar and navigation bar. Apps must handle window insets properly to prevent UI from being obscured by system bars.,Handle WindowInsets with Modifier.windowInsetsPadding or Scaffold contentPadding,Ignore insets; UI content hidden behind status bar and navigation bar on API 35+,Scaffold { innerPadding -> Content(Modifier.padding(innerPadding)) }; // Or: Modifier.windowInsetsPadding(WindowInsets.systemBars),// No inset handling; text and buttons hidden behind status bar and gesture navigation bar on API 35+,Critical,https://developer.android.com/about/versions/15/behavior-changes-15
193
+ Android 15 (API 35),16KB Page Size Support,Apps using NDK libraries must be rebuilt to support 16KB memory page sizes. Google Play requires all apps with native code targeting Android 15 to support 16KB pages.,Rebuild native libraries with 16KB page alignment; test on 16KB devices/emulators,Ship native libraries built with 4KB page alignment only; crashes on 16KB devices,// build.gradle: android.defaultConfig.ndk.abiFilters 'arm64-v8a'; // CMake: -DCMAKE_ANDROID_PAGE_SIZE=16384,// Native libraries compiled with 4KB page size only; crash or fail to load on 16KB page devices,Critical,https://developer.android.com/guide/practices/page-sizes
194
+ Android 15 (API 35),Predictive Back Enforced,The developer option for predictive back animations is removed on API 35. System back animations (back-to-home cross-task cross-activity) are now active by default for opted-in apps.,Set enableOnBackInvokedCallback=true; use OnBackPressedCallback for custom animations,Override onBackPressed or rely on deprecated back handling,<application android:enableOnBackInvokedCallback='true'>; // Use OnBackPressedCallback for custom logic,override fun onBackPressed() {} // deprecated; breaks predictive back animation on API 35+,High,https://developer.android.com/guide/navigation/custom-back/predictive-back-gesture
195
+ Android 15 (API 35),Private Space Support,Android 15 introduces Private Space a separate user profile for sensitive apps. Apps cannot detect if they run in Private Space. Launchers must declare ACCESS_HIDDEN_PROFILES to interact with it.,Support Private Space by using standard profile-aware APIs; handle multiple user profiles,Try to detect Private Space; assume single user profile only,// Use LauncherApps.getProfiles() for multi-profile support; declare ACCESS_HIDDEN_PROFILES for launchers,// Assume single user profile; launcher ignores Private Space apps entirely,Medium,https://developer.android.com/about/versions/15/behavior-changes-all
196
+ Android 15 (API 35),Non-SDK Interface Restrictions,Android 15 updates restricted non-SDK (hidden/internal) interface lists. Apps using blocked non-SDK methods will crash. Must migrate to public SDK alternatives.,Use only public SDK APIs; check for non-SDK usage with veridex tool,Use reflection to access hidden Android framework APIs,// Use official public APIs; run veridex scan: ./veridex --dex=app.apk,"// val method = Class.forName('android.os.SystemProperties').getMethod('get', String::class.java); method.invoke(null, 'ro.build.version') // blocked on API 35",High,https://developer.android.com/about/versions/15/changes/non-sdk-15
197
+ Android 15 (API 35),Media Projection Consent Per Session,Each MediaProjection capture session requires fresh user consent on API 35. Apps cannot reuse the Intent from createScreenCaptureIntent() across sessions. Calling createVirtualDisplay() more than once throws SecurityException.,Request fresh consent for each screen capture session via createScreenCaptureIntent(),Cache and reuse MediaProjection intent token across multiple sessions,val mediaProjectionManager = getSystemService<MediaProjectionManager>(); launcher.launch(mediaProjectionManager.createScreenCaptureIntent()) // every session,// Cache resultData from onActivityResult; reuse across sessions = SecurityException on API 35+,High,https://developer.android.com/media/grow/media-projection
198
+ Android 16 (API 36),Edge-to-Edge No Opt-Out,Apps targeting API 36 can no longer opt out of edge-to-edge display. The windowOptOutEdgeToEdgeEnforcement attribute is deprecated and disabled. Apps MUST handle drawing behind system bars.,Use enableEdgeToEdge() and handle WindowInsets with Modifier.windowInsetsPadding or Scaffold contentPadding,Use windowOptOutEdgeToEdgeEnforcement; it is ignored on API 36+,enableEdgeToEdge(); Scaffold { innerPadding -> Content(Modifier.padding(innerPadding)) },<item name='android:windowOptOutEdgeToEdgeEnforcement'>true</item> // ignored on API 36+,Critical,https://developer.android.com/about/versions/16/behavior-changes-16
199
+ Android 16 (API 36),Predictive Back Mandatory,Predictive back system animations are enabled by default on API 36. onBackPressed() is no longer called and KeyEvent.KEYCODE_BACK is not dispatched for back interception. Apps must migrate to OnBackPressedCallback.,Use BackHandler composable or OnBackPressedCallback for custom back handling,Override onBackPressed() or intercept KEYCODE_BACK; no longer called on API 36+,BackHandler(enabled = hasUnsavedChanges) { showConfirmDialog = true },override fun onBackPressed() { if (hasUnsavedChanges) showDialog() } // never called on API 36+,Critical,https://developer.android.com/guide/navigation/custom-back/predictive-back-gesture
200
+ Android 16 (API 36),Adaptive Layouts Large Screens,On displays with smallest width >= 600dp (tablets foldables Chromebooks) all orientation resizability and aspect ratio manifest restrictions are ignored on API 36+. Apps fill the entire display window.,Build adaptive layouts using WindowSizeClass; support all orientations and multi-window,Lock orientation with screenOrientation=portrait or set resizableActivity=false; ignored on large screens,@Composable fun AdaptiveScreen(wsc: WindowSizeClass) { when (wsc.widthSizeClass) { Compact -> ListScreen(); else -> Row { ListPane(Modifier.weight(0.4f)); DetailPane(Modifier.weight(0.6f)) } } },<activity android:screenOrientation='portrait' android:resizableActivity='false' /> // IGNORED on >= 600dp screens,Critical,https://developer.android.com/about/versions/16/behavior-changes-16
201
+ Android 16 (API 36),Health Permissions Granularization,BODY_SENSORS and BODY_SENSORS_BACKGROUND are replaced by granular android.permission.health.* permissions on API 36+. Apps MUST declare a privacy policy activity or permissions will be revoked.,Replace BODY_SENSORS with specific health.READ_HEART_RATE etc; add maxSdkVersion=35 to legacy permission; declare privacy policy activity,Continue using BODY_SENSORS without granular permissions; forget privacy policy activity,<uses-permission android:name='android.permission.BODY_SENSORS' android:maxSdkVersion='35' />; <uses-permission android:name='android.permission.health.READ_HEART_RATE' />,<uses-permission android:name='android.permission.BODY_SENSORS' /> // does not work on API 36+; no privacy policy = permissions revoked,High,https://developer.android.com/about/versions/16/behavior-changes-16
202
+ Android 16 (API 36),Local Network Permission Preview,Apps will need NEARBY_WIFI_DEVICES permission for local network access (RFC1918 mDNS broadcast). Currently opt-in for testing but will become mandatory. Affects OkHttp Retrofit WebViews raw sockets NsdManager.,Start testing with restriction enabled; declare NEARBY_WIFI_DEVICES; handle permission denial gracefully,Ignore this change; assume local network access is always available; fail to handle EPERM errors,<uses-permission android:name='android.permission.NEARBY_WIFI_DEVICES' />; if (checkSelfPermission(NEARBY_WIFI_DEVICES) == GRANTED) connectToLocalDevice(),// Direct local network call without permission; crashes with EPERM when restriction is enforced,High,https://developer.android.com/about/versions/16/behavior-changes-16
203
+ Android 16 (API 36),JobScheduler Quota Tightened,Job execution runtime quotas are enforced based on app standby bucket even when a foreground service is running. Affects JobScheduler WorkManager and DownloadManager on all apps running on Android 16.,Log stop reasons via WorkInfo.getStopReason(); use getPendingJobReasonsHistory() to diagnose; keep jobs short,Assume foreground service exempts jobs from quotas; run long background jobs without monitoring stop reasons,"override fun onStopJob(params: JobParameters): Boolean { Log.w('Jobs', 'Stopped reason: ${params.stopReason}'); return true }",startForegroundService(intent); // Jobs still get throttled on Android 16; assuming FGS prevents quota enforcement,Critical,https://developer.android.com/about/versions/16/behavior-changes-all
204
+ Android 16 (API 36),Abandoned Jobs Detection,When JobParameters is garbage collected before jobFinished() is called the system reports STOP_REASON_TIMEOUT_ABANDONED. Frequent abandoned jobs trigger system mitigation reducing job frequency.,Always maintain strong reference to JobParameters; always call jobFinished() when job completes,Let JobParameters get GC'd without calling jobFinished(); ignore abandoned stop reason,"class MyJobService : JobService() { private var params: JobParameters? = null; override fun onStartJob(p: JobParameters): Boolean { params = p; scope.launch { try { doWork() } finally { jobFinished(p, false) } }; return true } }",override fun onStartJob(params: JobParameters): Boolean { GlobalScope.launch { doWork() } // params GC'd; jobFinished never called; STOP_REASON_TIMEOUT_ABANDONED; return true },High,https://developer.android.com/about/versions/16/behavior-changes-all
205
+ Android 16 (API 36),16KB Page Size Compat Mode,Android 16 shows a compatibility dialog for apps with 4KB-aligned native code on 16KB page devices. For best performance upgrade to AGP 8.5.1+ and NDK r28+ which produce 16KB-aligned binaries by default.,Upgrade AGP and NDK; replace hardcoded 4096 with getpagesize() or sysconf(_SC_PAGESIZE); test on 16KB emulator,Hardcode PAGE_SIZE=4096 in native code; use old NDK producing 4KB-aligned binaries; ignore compatibility dialog,"size_t page = sysconf(_SC_PAGESIZE); void* p = mmap(NULL, size, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0);","#define PAGE_SIZE 4096; void* p = memalign(PAGE_SIZE, size); // fails on 16KB devices",High,https://developer.android.com/guide/practices/page-sizes
206
+ Android 16 (API 36),Safer Intents Strict Matching,New security feature: receiving apps can opt in to enforceIntentFilter matching. When enabled explicit intents must match the target intent filter and intents without an action cannot match any filter.,Opt in with intentMatchingFlags=enforceIntentFilter on receivers; test all intent flows after enabling,Send intents without an action to components with enforceIntentFilter; blindly opt out with removeLaunchSecurityProtection(),<application android:intentMatchingFlags='enforceIntentFilter'>; <receiver android:name='.MyReceiver' android:exported='true'><intent-filter><action android:name='com.example.ACTION' /></intent-filter></receiver>,"val intent = Intent(); intent.component = ComponentName('com.other', 'Receiver'); sendBroadcast(intent) // BLOCKED: no action to match filter",Medium,https://developer.android.com/about/versions/16/behavior-changes-16
207
+ Android 16 (API 36),Accessibility Announcements Deprecated,announceForAccessibility() and TYPE_ANNOUNCEMENT events are deprecated on Android 16. Apps must use structured alternatives like paneTitle liveRegion or setError() instead.,Use Modifier.semantics { paneTitle } for screen changes; liveRegion for real-time updates; setError() for errors,Use announceForAccessibility() or dispatch TYPE_ANNOUNCEMENT events,"Text(text = score, modifier = Modifier.semantics { liveRegion = LiveRegionMode.Polite })",view.announceForAccessibility('Results loaded') // deprecated; inconsistent TalkBack experience,Medium,https://developer.android.com/about/versions/16/behavior-changes-all
208
+ Android 16 (API 36),Broadcast Priority Scoped to Process,android:priority on broadcast receivers no longer guarantees delivery order across different processes on Android 16. Priority ordering is only respected within the same app process.,Avoid relying on cross-process broadcast ordering; use bound services or ContentProviders for ordered IPC,Rely on android:priority to control broadcast order across different apps or processes,"// Use explicit IPC: bindService(Intent(this, OrderedService::class.java), conn, BIND_AUTO_CREATE)",<receiver android:name='.HighPriority' android:priority='999'> // no longer guarantees order across processes,Medium,https://developer.android.com/about/versions/16/behavior-changes-16
209
+ Android 16 (API 36),Intent Redirection Security,Android 16 blocks potentially dangerous intent redirection for all apps. When an app launches a sub-level intent from extras of an incoming intent the system blocks unsafe launches by default.,Validate all incoming intents before launching sub-level intents; only use removeLaunchSecurityProtection() after security review,Blindly call removeLaunchSecurityProtection() on all intents; launch unvalidated sub-level intents from extras,"val sub: Intent? = intent.getParcelableExtra('sub', Intent::class.java); if (sub != null && isAllowedComponent(sub.component)) startActivity(sub)","val sub: Intent? = intent.getParcelableExtra('sub', Intent::class.java); sub?.removeLaunchSecurityProtection(); sub?.let { startActivity(it) } // blindly opted out",Medium,https://developer.android.com/about/versions/16/behavior-changes-16
210
+ Android 16 (API 36),Predictive Back 3-Button Navigation,Predictive back animations now work with 3-button navigation on Android 16 via long-press on the back button. Affects all apps that have opted in to predictive back.,Test predictive back with both gesture and 3-button navigation modes,Assume predictive back only applies to gesture navigation; skip 3-button testing,// Test with: adb shell settings put secure navigation_mode 0 (3-button); long-press back to preview,// Only tested with gesture nav; broken back animation with 3-button nav on Android 16,Medium,https://developer.android.com/about/versions/16/behavior-changes-16
211
+ Android 16 (API 36),Monochrome Adaptive Icon,Starting Android 16 QPR2 the system auto-generates themed icons for apps without a monochrome layer. Provide your own monochrome layer in adaptive-icon XML to control the themed appearance.,Include monochrome drawable in adaptive-icon XML for proper themed icon appearance,Skip monochrome layer and rely on auto-generated themed icon which may not match brand,<adaptive-icon><background android:drawable='@color/bg'/><foreground android:drawable='@drawable/fg'/><monochrome android:drawable='@drawable/ic_mono'/></adaptive-icon>,<adaptive-icon><background android:drawable='@color/bg'/><foreground android:drawable='@drawable/fg'/></adaptive-icon> // auto-themed icon may look wrong,Low,https://developer.android.com/about/versions/16/behavior-changes-16
212
+ Performance,Remember for Expensive Calculations,Composable functions can recompose very frequently (every animation frame). Store expensive calculation results with remember to avoid recalculating on every recomposition. Use keys with remember to recalculate only when dependencies change.,Cache expensive calculations using remember with dependency keys,Perform expensive calculations directly in composable body,"val sortedList = remember(items, sortBy) { items.sortedBy { sortBy(it) } }; LazyColumn { items(sortedList, key = { it.id }) { ItemCard(it) } }",LazyColumn { items(items.sortedBy { sortBy(it) }) { ItemCard(it) } } // sorts on every recomposition,High,https://developer.android.com/develop/ui/compose/performance/bestpractices
213
+ Performance,DerivedStateOf Optimization,Use derivedStateOf when you want to observe a computed value from other state but only recompose when the computation result changes. This prevents recomposition when intermediate state changes rapidly but the derived result stays the same (e.g. scroll position changes every pixel but 'show button' is boolean).,Wrap frequently-changing state computations in derivedStateOf to reduce recompositions,Read rapidly-changing state directly causing excessive recomposition,val showButton by remember { derivedStateOf { listState.firstVisibleItemIndex > 0 } }; AnimatedVisibility(visible = showButton) { ScrollToTopButton() },val showButton = listState.firstVisibleItemIndex > 0; AnimatedVisibility(visible = showButton) { ScrollToTopButton() } // recomposes on every scroll pixel,High,https://developer.android.com/develop/ui/compose/performance/bestpractices
214
+ Performance,Defer State Reads with Lambdas,Pass state as lambdas to child composables instead of reading state values directly in parent scope. This defers the state read until it's actually needed narrowing the recomposition scope. For layout-only operations use Modifier.offset { } lambda variant to skip recomposition entirely.,Pass state as lambda parameters to narrow recomposition scope,Read state in parent causing wide recomposition,"@Composable fun Title(scrollProvider: () -> Int) { Column(Modifier.offset { IntOffset(x = 0, y = scrollProvider()) }) { /* content */ } }",@Composable fun Title(scrollValue: Int) { Column(Modifier.offset(y = scrollValue.dp)) { /* content */ } } // parent recomposes on every scroll,Medium,https://developer.android.com/develop/ui/compose/performance/bestpractices
215
+ Startup,Baseline Profiles Production Setup,Baseline Profiles improve startup by 30% and navigation smoothness from first launch. Use Macrobenchmark to generate profiles covering critical user journeys. Deploy profiles with release builds via the baseline-prof.txt file in src/main. Profiles are ahead-of-time compiled by Play Store on device installation.,Generate baseline profiles using Macrobenchmark; deploy with release builds; include critical user journeys,Ship app without baseline profiles; only profile development builds; skip common navigation paths,"// In benchmark module: @Test fun generateBaselineProfile() = macrobenchmarkWithStartupMode(packageName = 'com.app', StartupMode.COLD, iterations = 3) { pressHome(); startActivityAndWait(); device.findObject(By.text('Settings')).click(); device.wait(Until.hasObject(By.text('Profile')), 5000) }; // baseline-prof.txt auto-generated in src/main",// No Baseline Profile - app relies only on JIT causing slower first launch and navigation,High,https://developer.android.com/topic/performance/baselineprofiles/overview
216
+ Startup,App Startup Library Integration,The App Startup library consolidates component initialization into a single ContentProvider reducing startup overhead. Migrate from auto-initializing ContentProviders to Initializer components with explicit dependency ordering. Use WorkManager.Configuration.Provider interface for on-demand initialization pattern.,Implement Initializer<T> with dependencies(); share InitializationProvider; lazy-init non-critical components,Keep multiple auto-init ContentProviders; no dependency management; initialize everything eagerly,class LoggerInitializer : Initializer<Logger> { override fun create(context: Context) = Logger.getInstance(context); override fun dependencies() = emptyList<Class<out Initializer<*>>>() }; class AnalyticsInitializer : Initializer<Analytics> { override fun dependencies() = listOf(LoggerInitializer::class.java) }; // In AndroidManifest: <provider android:name='androidx.startup.InitializationProvider' tools:node='merge'><meta-data android:name='com.app.LoggerInitializer' android:value='androidx.startup' /></provider>,<provider android:name='com.logger.LoggerProvider' /> <provider android:name='com.analytics.AnalyticsProvider' /> <provider android:name='com.crashlytics.CrashlyticsProvider' /> // 3 separate providers slow startup,High,https://developer.android.com/topic/libraries/app-startup
217
+ Startup,WorkManager Lazy Initialization,Disable WorkManager auto-initialization by removing the default provider and implementing Configuration.Provider for on-demand initialization. Call WorkManager.initialize() only when scheduling first work reducing cold start time when WorkManager isn't immediately needed.,Implement Configuration.Provider; remove default initializer; call initialize() on-demand,Let WorkManager auto-initialize on every app start via default ContentProvider,"// In AndroidManifest: <provider android:name='androidx.work.impl.WorkManagerInitializer' tools:node='remove' />; // In Application: class App : Application(), Configuration.Provider { override val workManagerConfiguration get() = Configuration.Builder().setMinimumLoggingLevel(Log.INFO).build() }; // Initialize on-demand: fun scheduleWork() { WorkManager.getInstance(context).enqueue(request) }",// WorkManager auto-inits on startup even if no work scheduled - wasted time and memory,Medium,https://developer.android.com/topic/libraries/architecture/workmanager/advanced/custom-configuration
218
+ Startup,ViewStub for Deferred UI,ViewStub is a zero-size placeholder for lazily inflating complex view hierarchies after initial frame. Use for infrequently shown UI like error states empty views or premium features. In Compose use conditional state-based rendering with LaunchedEffect to defer expensive composables.,Defer rare UI with ViewStub; inflate only when needed; use state conditions in Compose,Inflate entire view hierarchy upfront including rarely-shown UI,// XML: <ViewStub android:id='@+id/error_stub' android:layout='@layout/error_view' android:inflatedId='@+id/error_view' />; // Kotlin: val stub: ViewStub = findViewById(R.id.error_stub); if (hasError) { val errorView = stub.inflate() }; // Compose: var showError by remember { mutableStateOf(false) }; if (showError) { ErrorView() } // only compose when needed,<include layout='@layout/error_view' android:visibility='gone' /> // still inflated on startup wasting time,Medium,https://developer.android.com/topic/performance/appstartup/best-practices
219
+ Startup,Performance Class API for Media,Android 12+ PerformanceClass API identifies devices meeting performance thresholds (PerformanceClass.MEDIA_PERFORMANCE_CLASS). Use to adapt media quality resolution and features based on device capabilities. Check Build.VERSION.MEDIA_PERFORMANCE_CLASS to deliver optimal experience without degrading on lower-tier devices.,Check MEDIA_PERFORMANCE_CLASS; adjust video quality and features; gracefully degrade on older devices,Assume all devices handle max quality; ignore device performance tier,if (Build.VERSION.SDK_INT >= 31) { when { Build.VERSION.MEDIA_PERFORMANCE_CLASS >= 33 -> enableHighQuality() // 4K 60fps; Build.VERSION.MEDIA_PERFORMANCE_CLASS >= 31 -> enableMediumQuality() // 1080p 60fps; else -> enableBasicQuality() // 720p 30fps } },// Always load 4K assets regardless of device causing jank and crashes on low-end devices,Medium,https://developer.android.com/about/versions/12/features/performance-class
220
+ Security,Android 16 Advanced Protection Mode,Android 16 Advanced Protection Mode provides comprehensive security shield integrating verified boot strong sandboxing memory tagging USB lockdown and automatic device rebooting. When enabled prevents deactivation of core security features blocks sideloading filters spam/scam texts restricts insecure 2G connections and tightens Chrome protections. Comparable to Apple Lockdown Mode for high-risk users.,Enable Advanced Protection Mode in Settings → Security for users needing maximum security; educate users about trade-offs,Ignore Advanced Protection Mode; not recommend to high-risk users like journalists activists executives,Settings.ACTION_ADVANCED_PROTECTION_SETTINGS; // Guide users: Settings → Security → Advanced Protection → Enable all modules,// No Advanced Protection guidance - high-risk users vulnerable to targeted attacks,High,https://developer.android.com/about/versions/16/features/advanced-protection
221
+ Security,Android 16 Identity Check,Identity Check (biometric gating) requires biometric authentication for sensitive operations when device is outside trusted locations. Automatically prompts for fingerprint or face scan instead of PIN when changing device PIN turning off theft protection or accessing Google account settings outside home/work. Protects against shoulder surfing and coercion attacks.,Leverage Identity Check for high-security flows; inform users to enable in Settings → Security → Identity Check,Rely only on PIN/password without location-aware biometric gating,// User setting: Settings → Security → Identity Check → Enable; Add trusted locations; // Android automatically enforces biometric prompt for sensitive actions outside trusted zones,// No Identity Check - PIN vulnerable to shoulder surfing when user outside home,High,https://developer.android.com/privacy-and-security/identity-check
222
+ Security,Android 16 OTP Shield,Android 16 OTP Shield (Notification Privacy) intelligently redacts sensitive information like one-time passwords from lock screen notifications in higher-risk scenarios. Prevents OTP exposure even if user configured settings to show sensitive content on lock screen ensuring privacy when phone might be in unfamiliar hands or locations.,Trust Android 16 OTP Shield automatic protection; no app changes needed; test notification privacy in various risk scenarios,Display OTPs in full notification content regardless of device state,// Android 16+ automatically redacts OTPs on lock screen in high-risk contexts; // In NotificationCompat.Builder: setCategory(CATEGORY_MESSAGE).setVisibility(VISIBILITY_PRIVATE),NotificationCompat.Builder().setVisibility(VISIBILITY_PUBLIC).setContentText('Your code: 123456') // always visible on lock screen,Medium,https://developer.android.com/about/versions/16/features/notification-privacy
223
+ Security,Biometric Authentication Best Practices,Biometric authentication provides secure convenient user verification. Use BIOMETRIC_STRONG (Class 3 biometrics with hardware security) for sensitive operations like payments account changes. BIOMETRIC_WEAK (Class 2) acceptable for app unlock convenience features. Always provide fallback authentication method. BiometricPrompt API replaces deprecated FingerprintManager since Android 9.,Implement BiometricPrompt with BIOMETRIC_STRONG for sensitive operations; set deviceCredential fallback; handle authentication errors gracefully,Use deprecated FingerprintManager; use BIOMETRIC_WEAK for payments; no fallback method,"val promptInfo = BiometricPrompt.PromptInfo.Builder().setTitle('Authenticate').setAllowedAuthenticators(BIOMETRIC_STRONG or DEVICE_CREDENTIAL).build(); BiometricPrompt(activity, executor, object : AuthenticationCallback() { override fun onAuthenticationSucceeded(result: AuthenticationResult) { /* proceed */ } }).authenticate(promptInfo)",@Deprecated FingerprintManager.authenticate() // deprecated since API 28; no fallback,High,https://developer.android.com/training/sign-in/biometric-auth
224
+ Security,Android Keystore Cryptographic Keys,Android Keystore system provides hardware-backed secure storage for cryptographic keys making them difficult to extract from device. Keys can be configured to require user authentication be time-limited or bound to specific purposes (encrypt/decrypt sign/verify). Use for API keys encryption keys signing keys OAuth tokens. Supports AES RSA EC algorithms.,Generate keys in Keystore with setUserAuthenticationRequired(); use for all sensitive cryptographic operations; leverage StrongBox on supported devices,Store encryption keys in code SharedPreferences or app-private files; hardcode API keys in BuildConfig,"val keyGenParameterSpec = KeyGenParameterSpec.Builder('myKey', PURPOSE_ENCRYPT or PURPOSE_DECRYPT).setBlockModes(BLOCK_MODE_GCM).setEncryptionPaddings(ENCRYPTION_PADDING_NONE).setUserAuthenticationRequired(true).setUserAuthenticationValidityDurationSeconds(30).build(); KeyGenerator.getInstance(AES, 'AndroidKeyStore').init(keyGenParameterSpec)",const val API_KEY = 'sk_live_hardcoded_key_123' // hardcoded in code easily extracted,Critical,https://developer.android.com/privacy-and-security/keystore
225
+ Security,Network Security Configuration,Network Security Config (network_security_config.xml) provides declarative TLS/SSL configuration without code changes. Configure cleartext traffic policies certificate pinning custom trust anchors debug overrides per-domain settings. Enforced at framework level preventing accidental insecure connections. Android 9+ blocks cleartext by default for targetSdk 28+.,Create res/xml/network_security_config.xml with base-config and domain-specific rules; reference in AndroidManifest; pin production certificates; use debug-overrides for development,Implement custom TrustManager accepting all certificates; ignore certificate validation errors; use android:usesCleartextTraffic='true',<!-- res/xml/network_security_config.xml --><network-security-config><base-config cleartextTrafficPermitted='false'><trust-anchors><certificates src='system'/></trust-anchors></base-config><domain-config><domain includeSubdomains='true'>api.example.com</domain><pin-set expiration='2027-01-01'><pin digest='SHA-256'>base64hash==</pin><pin digest='SHA-256'>backup_hash==</pin></pin-set></domain-config></network-security-config>,"val trustAllCerts = arrayOf<TrustManager>(object : X509TrustManager { override fun checkClientTrusted(chain: Array<X509Certificate>, authType: String) {} override fun checkServerTrusted(chain: Array<X509Certificate>, authType: String) {} override fun getAcceptedIssuers() = arrayOf<X509Certificate>() }) // DANGEROUS",Critical,https://developer.android.com/privacy-and-security/security-config
226
+ Security,Certificate Pinning Implementation,Certificate pinning prevents man-in-the-middle attacks by validating server certificates against known good values. Pin leaf certificate (rotates more frequently) or intermediate/root CA certificate (more stable). Include backup pins for certificate rotation. Monitor pin expiration dates. Use Network Security Config (recommended) or OkHttp CertificatePinner.,Pin certificates in network_security_config.xml; include backup pins; set expiration dates; test with production certificates in staging,Skip certificate pinning; pin only in production not staging; no backup pins; hardcode pins in code,"<!-- Recommended: Network Security Config --><domain-config><domain>api.bank.com</domain><pin-set expiration='2027-06-01'><pin digest='SHA-256'>primary_cert_hash==</pin><pin digest='SHA-256'>backup_cert_hash==</pin></pin-set></domain-config>; // Alternative: OkHttp; val pinner = CertificatePinner.Builder().add('api.bank.com', 'sha256/hash==').build()",// No certificate pinning - vulnerable to MITM attacks with rogue certificates,Critical,https://developer.android.com/privacy-and-security/security-config#CertificatePinning
227
+ Security,Play Integrity API Integration,Play Integrity API helps detect app tampering repackaging emulator environments rooted devices. Provides three verdicts: MEETS_DEVICE_INTEGRITY (genuine Google Play device) MEETS_BASIC_INTEGRITY (passes basic integrity checks) MEETS_STRONG_INTEGRITY (strongest assurance). Use server-side to validate token before allowing sensitive operations. Replaces deprecated SafetyNet Attestation API.,Request integrity token before sensitive operations; validate server-side; check device integrity verdict; handle PLAY_STORE_VERSION_UNKNOWN gracefully; rate limit requests,Skip integrity checks; validate client-side; use deprecated SafetyNet; no rate limiting,// Request token: IntegrityManagerFactory.create(context).requestIntegrityToken(IntegrityTokenRequest.builder().setCloudProjectNumber(projectId).build()).addOnSuccessListener { response -> sendToServer(response.token()) }; // Server-side validation: POST https://playintegrity.googleapis.com/v1/projects/{projectId}/apps:checkIntegrity with token; verify deviceIntegrity verdict,// No integrity check - modified APKs tampered apps can access backend APIs,High,https://developer.android.com/google/play/integrity
228
+ Security,Android 16 Private Space,Android 16 Private Space creates sandboxed environment for sensitive apps (banking health identity management). Apps in Private Space are invisible to rest of OS show no lock screen notifications require separate authentication cannot be accessed by other apps. Separate app data settings profiles. User can quickly lock/unlock Private Space via Settings or quick tile.,Recommend users enable Private Space for sensitive apps in Settings → Security → Private Space; guide users to move banking/health apps into Private Space,Assume all apps have same security context; no guidance on isolating sensitive apps,// User setup: Settings → Security → Private Space → Set up; Move sensitive apps; Set separate unlock; // No code changes needed; Android handles isolation automatically; // Quick lock: Swipe down → Private Space tile → Lock,// No Private Space usage - banking app accessible from any app via intent; notifications visible on lock screen,Medium,https://developer.android.com/about/versions/16/features/private-space
229
+ Security,Intent Security Best Practices,Intent security critical for preventing data leaks and hijacking attacks. Use explicit intents (specify component class) for intra-app communication. For exported components validate incoming intent data check caller identity use permissions. PendingIntents must use FLAG_IMMUTABLE (required Android 12+) specify explicit component. Avoid Intent redirection vulnerabilities by sanitizing extras before forwarding.,Use explicit intents for app components; validate all intent extras; set android:exported='false' by default; use FLAG_IMMUTABLE for PendingIntents; define custom permissions for sensitive components,Use implicit intents everywhere; trust intent data without validation; use FLAG_MUTABLE; export all components; forward intents without sanitization,"// Explicit intent: Intent(context, TargetActivity::class.java); // PendingIntent: PendingIntent.getActivity(context, 0, intent, FLAG_IMMUTABLE or FLAG_UPDATE_CURRENT); // Validate extras: val userId = intent.getStringExtra('user_id')?.takeIf { it.matches(Regex('[0-9]+')) } ?: return; // Manifest: <activity android:name='.SecureActivity' android:exported='false' />",// Implicit intent: Intent('com.example.ACTION_VIEW'); // PendingIntent: FLAG_MUTABLE // DANGEROUS; // No validation: val data = intent.getStringExtra('data'); startActivity(Intent(data)) // Intent redirection vulnerability,Critical,https://developer.android.com/privacy-and-security/risks/pending-intent
230
+ Security,WebView Security Configuration,WebView can expose apps to web vulnerabilities if misconfigured. Disable JavaScript unless absolutely required. Disable file access (setAllowFileAccess setAllowFileAccessFromFileURLs setAllowUniversalAccessFromFileURLs) to prevent local file inclusion attacks. Only use addJavascriptInterface with @JavascriptInterface annotation on API 17+ and load only trusted HTTPS content. Enable Safe Browsing.,Disable JavaScript by default; disable all file access methods; use @JavascriptInterface annotation; load only HTTPS trusted content; enable Safe Browsing,Enable JavaScript for all content; allow file access; expose native methods without @JavascriptInterface; load HTTP or untrusted content,"webView.settings.apply { javaScriptEnabled = false; allowFileAccess = false; allowFileAccessFromFileURLs = false; allowUniversalAccessFromFileURLs = false; safeBrowsingEnabled = true }; // Only for trusted content: class SecureBridge { @JavascriptInterface fun safeMethod() { } }; webView.addJavascriptInterface(SecureBridge(), 'bridge')","webView.settings.javaScriptEnabled = true; webView.settings.allowFileAccess = true; // File XSS vulnerability; webView.addJavascriptInterface(AppAPI(), 'api') // All public methods exposed",Critical,https://developer.android.com/privacy-and-security/risks/webview-unsafe-file-inclusion
231
+ Security,Tapjacking Prevention,Tapjacking (clickjacking for Android) tricks users into clicking hidden UI elements via overlay attacks. Use FLAG_SECURE to prevent screen recording and overlays on sensitive screens. Use filterTouchesWhenObscured to ignore touches when window is obscured. Set setObscuredTouchesFilter on sensitive views. Require SYSTEM_ALERT_WINDOW permission checks for overlays.,Use FLAG_SECURE for payment/auth screens; set filterTouchesWhenObscured on sensitive buttons; check for overlay permissions before sensitive operations,No overlay protection; allow touches when obscured; no FLAG_SECURE on sensitive screens,"// In Activity onCreate: window.setFlags(WindowManager.LayoutParams.FLAG_SECURE, WindowManager.LayoutParams.FLAG_SECURE); // For sensitive buttons: confirmButton.filterTouchesWhenObscured = true; // Or in XML: android:filterTouchesWhenObscured='true'; // Check for overlays: if (Settings.canDrawOverlays(context)) { showOverlayWarning() }",// No protection - overlay can hijack payment confirmation clicks; // Sensitive screens can be recorded,High,https://developer.android.com/privacy-and-security/risks/tapjacking
232
+ Security,Logging Security Best Practices,Logging sensitive data exposes PII credentials tokens to logcat which can be read by apps with READ_LOGS permission on some devices. Never log sensitive data. Use BuildConfig.DEBUG conditional logging for debug statements. Configure R8/ProGuard to strip all debug and verbose logs from release builds using -assumenosideeffects. Use internal storage for production logs if needed not logcat.,Wrap logs in BuildConfig.DEBUG; configure R8 to strip logs; never log PII passwords tokens API keys; use internal storage for production logs,Log sensitive data unconditionally; no R8 log stripping; log to logcat in production,"if (BuildConfig.DEBUG) { Log.d(TAG, 'Debug: $nonSensitiveData') }; // In proguard-rules.pro: -assumenosideeffects class android.util.Log { public static *** d(...); public static *** v(...); public static *** i(...); }; // Production logging: writeToInternalLog(sanitizedData)","Log.d(TAG, 'User token: $authToken'); Log.d(TAG, 'Password: $password'); // Always logs to logcat even in production",Critical,https://developer.android.com/privacy-and-security/risks/log-info-disclosure
233
+ Security,Backup Security,Android automatic backup can expose sensitive data via ADB or cloud backup. Use android:allowBackup='false' to disable or android:fullBackupContent to specify backup rules excluding sensitive files (databases tokens keys cache). Data backup rules use include/exclude tags. Mark sensitive activities with android:excludeFromRecents. Private Space provides additional backup isolation.,Set android:allowBackup='false' or create backup rules excluding sensitive data; use excludeFromRecents for sensitive activities; leverage Private Space,Allow full backup without restrictions; include sensitive databases tokens in backup; no excludeFromRecents,<!-- AndroidManifest.xml --> <application android:allowBackup='true' android:fullBackupContent='@xml/backup_rules'>; <!-- res/xml/backup_rules.xml --> <full-backup-content><exclude domain='database' path='sensitive.db'/><exclude domain='sharedpref' path='auth_prefs.xml'/><exclude domain='file' path='keys/'/></full-backup-content>; <!-- For highly sensitive apps --> <application android:allowBackup='false'>,<application android:allowBackup='true'> <!-- Everything backed up including tokens passwords databases,High,https://developer.android.com/identity/data/backup
234
+ Security,Clipboard Security,Clipboard data is accessible to all apps when device is unlocked exposing sensitive data like passwords tokens OTPs. Use ClipData.newPlainText with setSensitiveContent(true) on Android 13+ to prevent clipboard preview and cross-device sync. Clear clipboard after paste. Avoid auto-copying sensitive data. Monitor clipboard for data leakage.,Use setSensitiveContent(true) for sensitive clipboard data; clear clipboard after paste; avoid auto-copying passwords OTPs,Copy sensitive data to clipboard without restrictions; never clear clipboard; show full preview,"// Android 13+ sensitive clipboard: val clip = ClipData.newPlainText('password', sensitiveData).apply { description.extras = PersistableBundle().apply { putBoolean(ClipDescription.EXTRA_IS_SENSITIVE, true) } }; clipboardManager.setPrimaryClip(clip); // Clear after use: Handler(Looper.getMainLooper()).postDelayed({ clipboardManager.clearPrimaryClip() }, 30000)","clipboardManager.setPrimaryClip(ClipData.newPlainText('label', password)) // Visible in preview; syncs to other devices",Medium,https://developer.android.com/develop/ui/views/touch-and-input/copy-paste
235
+ Security,Screen Capture Prevention,WindowManager.LayoutParams.FLAG_SECURE prevents screenshots screen recording and display in recent apps for sensitive content. Essential for payment screens authentication banking health apps. Note: FLAG_SECURE doesn't work in accessibility services or rooted devices. Combine with root detection for comprehensive protection.,Use FLAG_SECURE for sensitive screens (payment auth banking); combine with root detection; inform users why screenshots disabled,No screen capture prevention; allow screenshots of sensitive data; visible in recent apps,"// In Activity or Fragment: override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState); window.setFlags(WindowManager.LayoutParams.FLAG_SECURE, WindowManager.LayoutParams.FLAG_SECURE) }; // Or in Application for all activities: registerActivityLifecycleCallbacks(object : ActivityLifecycleCallbacks { override fun onActivityCreated(activity: Activity, bundle: Bundle?) { activity.window.setFlags(FLAG_SECURE, FLAG_SECURE) } })",// No FLAG_SECURE - users can screenshot passwords; payment info visible in recent apps; screen can be recorded,High,https://developer.android.com/privacy-and-security/risks
236
+ Security,Deep Link Validation,Deep links (Intent filters App Links) can be exploited if parameters aren't validated. Attackers can craft malicious URIs to trigger unintended actions inject data or access unauthorized content. Validate all URI parameters against whitelist regex. Verify host and scheme. Don't trust deep link data for sensitive operations without authentication. Use App Links (https scheme) with Digital Asset Links for verified domains.,Validate all URI parameters with regex whitelist; verify host and scheme; require authentication for sensitive deep link actions; use verified App Links,Trust all deep link parameters; no validation; allow any host; perform sensitive actions from unverified deep links,"// Validate deep link: val uri = intent.data ?: return; if (uri.scheme != 'https' || uri.host != 'myapp.com') { finish(); return }; val itemId = uri.getQueryParameter('id')?.takeIf { it.matches(Regex('^[a-zA-Z0-9]{1,20}$')) } ?: run { finish(); return }; // Verify domain with App Links: <intent-filter android:autoVerify='true'><data android:scheme='https' android:host='myapp.com' /></intent-filter>",val uri = intent.data; val action = uri.getQueryParameter('action'); executeAction(action) // Can execute arbitrary malicious actions,Critical,https://developer.android.com/privacy-and-security/risks/unsafe-use-of-deeplinks
237
+ Security,SQL Injection Prevention Best Practices,SQL injection occurs when user input is concatenated into SQL queries allowing attackers to execute arbitrary SQL. Always use parameterized queries with Room @Query or SQLiteDatabase query() methods with selectionArgs. Never use rawQuery with string concatenation. Validate input before queries. Use ContentProvider with proper URI validation. Room compile-time verification prevents most SQL injection.,Use Room @Query with parameters; use query() with selectionArgs; validate inputs; never concatenate user input into SQL,Use rawQuery with string concatenation; concatenate user input into WHERE clauses; no input validation,"// Room (recommended): @Query('SELECT * FROM users WHERE email = :email AND active = :active') fun findUser(email: String, active: Boolean): User?; // SQLiteDatabase: val cursor = db.query('users', null, 'email = ? AND active = ?', arrayOf(email, active.toString()), null); // Input validation: val safeInput = input.takeIf { it.matches(Regex('^[a-zA-Z0-9@._-]+$')) } ?: throw IllegalArgumentException()",db.rawQuery('SELECT * FROM users WHERE email = $userInput') // Attacker can input: admin@example.com' OR '1'='1,Critical,https://developer.android.com/training/data-storage/room/accessing-data
238
+ Security,Google Play Data Safety Declaration,As of 2026 Google Play requires all apps to accurately complete the Data Safety form disclosing data collection sharing and security practices. Developers must declare data handled by third-party libraries/SDKs. Update declarations when data practices change. Failure to accurately disclose leads to app rejection or removal. Use Play Console Data Safety section to declare: data types collected purposes sharing practices retention encryption.,Accurately complete Data Safety form; declare all data collection including third-party SDKs; update when practices change; review before each release,Skip Data Safety declaration; incomplete or inaccurate information; don't declare SDK data collection; never update,// Review all dependencies' privacy policies: dependencies.forEach { reviewPrivacyPolicy(it) }; // Data Safety checklist: Location: Collected for maps analytics? User Data: Email name collected? Shared with third parties? Encrypted in transit/at rest? // Update in Play Console before every release; // Document SDK data practices in internal privacy audit,No Data Safety form or incomplete declaration; SDK data collection not disclosed,Critical,https://support.google.com/googleplay/android-developer/answer/10787469
239
+ Security,Photo/Video Permission Declaration,Google Play 2026 policy requires apps requesting READ_MEDIA_IMAGES or READ_MEDIA_VIDEO to submit declaration form justifying use. Apps must demonstrate these permissions are core to app functionality or remove them. Alternatives: use photo picker (no permission needed) request specific media types only use scoped storage. Apps without valid justification will be rejected.,Use photo picker API when possible; justify READ_MEDIA permissions with declaration form; request only necessary media types; consider scoped storage,Request broad photo/video permissions without justification; no declaration form; collect all media files unnecessarily,// Recommended: Photo Picker (no permission): val pickMedia = registerForActivityResult(PickVisualMedia()) { uri -> processPhoto(uri) }; pickMedia.launch(PickVisualMediaRequest(ImageOnly)); // If READ_MEDIA needed justify in Play Console declaration: 'Photo editing app requires access to modify user photos'; // Or scoped storage: MediaStore for specific files,Requesting READ_MEDIA_IMAGES/VIDEO without declaration form or valid core functionality justification,High,https://support.google.com/googleplay/android-developer/answer/14115180
240
+ Security,Accessibility API Restrictions,Google Play 2026 explicitly prohibits using Accessibility API for autonomous actions that change settings or circumvent privacy controls without user knowledge. Apps using AccessibilityService must have clear disability-related purpose. Misuse leads to immediate removal. Valid uses: screen readers navigation aids switch access. Invalid: automation ad clicking data harvesting bypassing security.,Use Accessibility API only for legitimate disability assistance; clear user disclosure; avoid autonomous actions changing settings,Misuse Accessibility API for automation ad clicking or bypassing security; no disclosure; background autonomous actions,// Valid use: class ScreenReaderService : AccessibilityService() { override fun onAccessibilityEvent(event: AccessibilityEvent) { announceForAccessibility(event.contentDescription) } }; // Manifest disclosure: \u003cservice android:name='.ScreenReaderService' android:permission='android.permission.BIND_ACCESSIBILITY_SERVICE'\u003e\u003cmeta-data android:name='android.accessibilityservice' android:resource='@xml/accessibility_service_config' /\u003e\u003c/service\u003e,Using AccessibilityService to: auto-click ads; harvest data from other apps; change system settings without permission; bypass security prompts,Critical,https://support.google.com/googleplay/android-developer/answer/13820226
241
+ Security,In-App Update Security,Google Play in-app updates allow seamless updates without leaving the app. Implement with AppUpdateManager using flexible or immediate modes. Validate update availability with Play Core library. Security: updates are signed and verified by Play Store encrypted in transit user must approve. Handle update failures gracefully. Test with internal testing track. Priority: high priority for critical security patches.,Use AppUpdateManager for in-app updates; handle flexible and immediate modes; prioritize security patches; test thoroughly; handle failures gracefully,No in-app update mechanism; force users to manual updates; no update priority for security fixes; untested update flow,"// Add dependency: implementation 'com.google.android.play:app-update:2.1.0'; val appUpdateManager = AppUpdateManagerFactory.create(context); appUpdateManager.appUpdateInfo.addOnSuccessListener { info -> if (info.updateAvailability() == UpdateAvailability.UPDATE_AVAILABLE && info.isUpdateTypeAllowed(AppUpdateType.IMMEDIATE)) { appUpdateManager.startUpdateFlowForResult(info, AppUpdateType.IMMEDIATE, activity, REQUEST_CODE) } }",No update mechanism; users stay on vulnerable versions; manual Play Store navigation required; security patches delayed,High,https://developer.android.com/guide/playcore/in-app-updates
242
+ Security,OWASP M1: Improper Credential Usage,OWASP Mobile Top 10 2024 M1 covers insecure credential handling storage and transmission. Never hardcode credentials API keys tokens. Use Android Keystore for sensitive keys EncryptedSharedPreferences for auth tokens. Implement strong authentication (MFA biometrics) token rotation secure password policies. Validate credentials server-side. Clear credentials on logout.,Use Android Keystore for keys; EncryptedSharedPreferences for tokens; implement MFA biometrics; token rotation; never hardcode credentials,Hardcode API keys passwords; store tokens in plain SharedPreferences; weak authentication; no token rotation; credentials in git,"// Generate key in Keystore: val keyGenerator = KeyGenerator.getInstance(KeyProperties.KEY_ALGORITHM_AES, 'AndroidKeyStore'); keyGenerator.init(KeyGenParameterSpec.Builder('auth_key', PURPOSE_ENCRYPT or PURPOSE_DECRYPT).setBlockModes(BLOCK_MODE_GCM).setEncryptionPaddings(ENCRYPTION_PADDING_NONE).build()); // Store token: EncryptedSharedPreferences encrypted token; // MFA: BiometricPrompt for sensitive operations",const val API_KEY = 'sk_live_abc123'; // Hardcoded exposed in APK; SharedPreferences.edit().putString('token' authToken) // Plaintext storage,Critical,https://owasp.org/www-project-mobile-top-10/
243
+ Security,OWASP M4: Insufficient Input Validation,OWASP M4 addresses injection attacks from unvalidated input. Validate all user input deep link parameters intent extras API responses with regex whitelisting length checks type validation. Sanitize output to prevent XSS. Use parameterized queries for SQL Room @Query. Validate file uploads. Check data types bounds special characters.,Validate all inputs with regex whitelist length type checks; sanitize outputs; parameterized queries; validate intents deep links API data,Trust all user input; no validation; string concatenation for SQL; accept any file upload; no sanitization,"// Input validation: fun validateEmail(input: String): String? = input.takeIf { it.matches(Regex('^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\\\.[a-zA-Z]{2,}$')) && it.length \u003c= 254 }; // Deep link validation: val userId = uri.getQueryParameter('id')?.takeIf { it.matches(Regex('^[0-9]{1,10}$')) } ?: return; // SQL: @Query('SELECT * WHERE id = :id') parameterized",val userId = intent.getStringExtra('user_id'); deleteUser(userId) // No validation - injection; db.rawQuery('SELECT * WHERE name = $input') // SQL injection,Critical,https://owasp.org/www-project-mobile-top-10/
244
+ Security,OWASP M8: Security Misconfiguration,OWASP M8 covers misconfigurations: android:debuggable=true in production insecure defaults excessive permissions weak encryption cleartext traffic debug code shipped. Review: remove debug features use android:debuggable=false minimize permissions enforce HTTPS disable unused features secure default configurations production-ready builds.,Remove all debug code; android:debuggable=false; minimize permissions; HTTPS only; secure defaults; review before release,Ship with android:debuggable=true; debug features enabled; excessive permissions; HTTP allowed; insecure defaults; no configuration review,// Release build (build.gradle.kts): buildTypes { release { isDebuggable = false; isMinifyEnabled = true; proguardFiles(getDefaultProguardFile('proguard-android-optimize.txt')) } }; // AndroidManifest: \u003capplication android:debuggable='false' android:allowBackup='false'\u003e; // Network: \u003cnetwork-security-config\u003e\u003cbase-config cleartextTrafficPermitted='false'\u003e; // Minimal permissions,\u003capplication android:debuggable='true'\u003e in production; debug logs enabled; \u003cuses-permission android:name='android.permission.READ_CONTACTS' /\u003e unused; cleartextTrafficPermitted='true',High,https://owasp.org/www-project-mobile-top-10/
245
+ Security,OWASP M9: Insecure Data Storage,OWASP M9 addresses storing sensitive data insecurely. Encrypt all sensitive data at rest using EncryptedSharedPreferences EncryptedFile Room with SQLCipher. Use Android Keystore for key management. Don't store in: plain SharedPreferences external storage logs temporary files. Implement data retention policies secure deletion. Exclude sensitive data from backups.,Encrypt data at rest with EncryptedSharedPreferences EncryptedFile Room+SQLCipher; use Keystore; exclude from backups; secure deletion,Store sensitive data in plain SharedPreferences; external storage; no encryption; included in backups; no deletion policy,"// Encrypted storage: val masterKey = MasterKey.Builder(context).setKeyScheme(AES256_GCM).build(); val encryptedPrefs = EncryptedSharedPreferences.create(context, 'secure_prefs', masterKey, AES256_SIV, AES256_GCM); encryptedPrefs.edit().putString('token', authToken).apply(); // Backup exclusion: \u003cfull-backup-content\u003e\u003cexclude domain='sharedpref' path='secure_prefs.xml'/\u003e\u003c/full-backup-content\u003e","val prefs = getSharedPreferences('prefs', MODE_PRIVATE); prefs.edit().putString('password', userPassword).apply() // Plaintext readable via ADB backup; File(externalStorageDirectory, 'sensitive.txt').writeText(data) // Public storage",Critical,https://owasp.org/www-project-mobile-top-10/
246
+ Security,Third-Party SDK Compliance,Google Play 2026 increases scrutiny on third-party SDKs requiring developers to ensure SDK compliance with policies especially data collection background behavior. Review SDK privacy policies update regularly vet before integration use dependency scanning declare SDK data in Data Safety. Non-compliant SDKs lead to app rejection. Check SDK permissions network access data harvesting.,Vet all SDKs before integration; review privacy policies; dependency scanning; keep updated; declare in Data Safety; remove non-compliant SDKs,Use SDKs without vetting; outdated versions; no privacy review; undeclared in Data Safety; unknown data collection,// SDK vetting checklist: 1. Review privacy policy; 2. Check permissions SDK requests; 3. Scan with: ./gradlew dependencyCheckAnalyze; 4. Monitor SDK behavior with network proxy; 5. Declare in Data Safety; 6. Update regularly; // Use trusted SDKs: dependencies { implementation('androidx.security:security-crypto:1.1.0-alpha06') // Google official }; // Remove suspicious SDKs with excessive permissions or unknown data collection,dependencies { implementation('com.unknown:analytics:1.0.0') } // Unvetted outdated unknown; SDK requests CONTACTS LOCATION undisclosed; Not declared in Data Safety; Never updated,High,https://support.google.com/googleplay/android-developer/answer/9888076
247
+ Security,Runtime Permission Best Practices,Android 6+ requires runtime permissions for dangerous permissions (location camera contacts etc). Request permissions contextually with clear rationale using shouldShowRequestPermissionRationale. Handle denials gracefully. Request minimum permissions. Use permission groups. Check permissions before use. Consider alternatives: photo picker (no permission) approximate location. Alert users why permission needed.,Request permissions at runtime with rationale; minimal permissions; handle denials; explain to users; use alternatives when possible,Request all permissions at install; no rationale; don't handle denials; excessive permissions; no explanation,"// Request with rationale: if (ContextCompat.checkSelfPermission(this, ACCESS_FINE_LOCATION) != PERMISSION_GRANTED) { if (shouldShowRequestPermissionRationale(ACCESS_FINE_LOCATION)) { showRationale('Location needed for nearby stores') } else { requestPermissionLauncher.launch(ACCESS_FINE_LOCATION) } }; // Handle denial: registerForActivityResult(RequestPermission()) { granted -> if (!granted) offerAlternative() }","// Bad: Request all upfront: requestPermissions(arrayOf(CAMERA, CONTACTS, LOCATION, READ_SMS, CALL_PHONE), 0) // No explanation; if (checkSelfPermission(CAMERA) != GRANTED) { finish() } // Don't handle denial",High,https://developer.android.com/training/permissions/requesting