- Android Expert
- 1. Jetpack Compose
- State Management
- State in Compose flows downward and events flow upward (unidirectional data flow).
- State hoisting pattern:
- // Stateless composable — accepts state and callbacks
- @Composable
- fun
- LoginForm
- (
- :
- String
- ,
- password
- :
- String
- ,
- onEmailChange
- :
- (
- String
- )
- ->
- Unit
- ,
- onPasswordChange
- :
- (
- String
- )
- ->
- Unit
- ,
- onSubmit
- :
- (
- )
- ->
- Unit
- ,
- )
- {
- Column
- {
- TextField
- (
- value
- =
- ,
- onValueChange
- =
- onEmailChange
- ,
- label
- =
- {
- Text
- (
- "Email"
- )
- }
- )
- TextField
- (
- value
- =
- password
- ,
- onValueChange
- =
- onPasswordChange
- ,
- label
- =
- {
- Text
- (
- "Password"
- )
- }
- )
- Button
- (
- onClick
- =
- onSubmit
- )
- {
- Text
- (
- "Log in"
- )
- }
- }
- }
- // Stateful caller — owns state and passes it down
- @Composable
- fun
- LoginScreen
- (
- viewModel
- :
- LoginViewModel
- =
- hiltViewModel
- (
- )
- )
- {
- val
- state
- by
- viewModel
- .
- uiState
- .
- collectAsStateWithLifecycle
- (
- )
- LoginForm
- (
- =
- state
- .
- ,
- password
- =
- state
- .
- password
- ,
- onEmailChange
- =
- viewModel
- ::
- onEmailChanged
- ,
- onPasswordChange
- =
- viewModel
- ::
- onPasswordChanged
- ,
- onSubmit
- =
- viewModel
- ::
- onSubmit
- ,
- )
- }
- remember
- vs
- rememberSaveable
- :
- remember
-
- Survives recomposition only. Use for transient UI state.
- rememberSaveable
-
- Survives recomposition AND process death (saved to Bundle). Use for user-visible state (scroll position, form input).
- // remember — lost on configuration change / process death
- var
- expanded
- by
- remember
- {
- mutableStateOf
- (
- false
- )
- }
- // rememberSaveable — survives configuration change and process death
- var
- selectedTab
- by
- rememberSaveable
- {
- mutableIntStateOf
- (
- 0
- )
- }
- derivedStateOf
- :
- Use when derived state depends on other state objects and you want to
- avoid unnecessary recompositions.
- val
- isSubmitEnabled
- by
- remember
- {
- derivedStateOf
- {
- .
- isNotBlank
- (
- )
- &&
- password
- .
- length
- >=
- 8
- }
- }
- Side Effects
- Use structured side effect APIs — never launch coroutines or perform side effects in composition.
- API
- When to use
- LaunchedEffect(key)
- Launch a coroutine tied to a key; cancels/relaunches when key changes
- rememberCoroutineScope()
- Get a scope for event-driven coroutines (button click, etc.)
- SideEffect
- Run non-suspend side effects after every successful composition
- DisposableEffect(key)
- Side effects with cleanup (register/unregister callbacks)
- // Navigate to destination after login success
- LaunchedEffect
- (
- uiState
- .
- isLoggedIn
- )
- {
- if
- (
- uiState
- .
- isLoggedIn
- )
- navController
- .
- navigate
- (
- Route
- .
- Home
- )
- }
- // Scope for click-driven coroutine
- val
- scope
- =
- rememberCoroutineScope
- (
- )
- Button
- (
- onClick
- =
- {
- scope
- .
- launch
- {
- / ... /
- }
- }
- )
- {
- Text
- (
- "Save"
- )
- }
- // Register/unregister a callback
- DisposableEffect
- (
- lifecycleOwner
- )
- {
- val
- observer
- =
- LifecycleEventObserver
- {
- _
- ,
- event
- ->
- / ... /
- }
- lifecycleOwner
- .
- lifecycle
- .
- addObserver
- (
- observer
- )
- onDispose
- {
- lifecycleOwner
- .
- lifecycle
- .
- removeObserver
- (
- observer
- )
- }
- }
- Recomposition Optimization
- Recomposition is the main performance concern in Compose. Minimize its scope.
- // AVOID: Unstable lambda captures the entire parent scope
- @Composable
- fun
- ItemList
- (
- items
- :
- List
- <
- Item
- >
- ,
- onItemClick
- :
- (
- Item
- )
- ->
- Unit
- )
- {
- LazyColumn
- {
- items
- (
- items
- ,
- key
- =
- {
- it
- .
- id
- }
- )
- {
- item
- ->
- // New lambda instance created on each recomposition of ItemList
- ItemRow
- (
- item
- =
- item
- ,
- onClick
- =
- {
- onItemClick
- (
- item
- )
- }
- )
- }
- }
- }
- // PREFER: Stable key + remember to avoid unnecessary child recompositions
- @Composable
- fun
- ItemList
- (
- items
- :
- List
- <
- Item
- >
- ,
- onItemClick
- :
- (
- Item
- )
- ->
- Unit
- )
- {
- val
- stableOnClick
- =
- rememberUpdatedState
- (
- onItemClick
- )
- LazyColumn
- {
- items
- (
- items
- ,
- key
- =
- {
- it
- .
- id
- }
- )
- {
- item
- ->
- ItemRow
- (
- item
- =
- item
- ,
- onClick
- =
- {
- stableOnClick
- .
- value
- (
- item
- )
- }
- )
- }
- }
- }
- Rules for stable types:
- Primitive types and
- String
- are always stable.
- Data classes with only stable fields are stable if annotated
- @Stable
- or
- @Immutable
- .
- List
- ,
- Map
- ,
- Set
- from stdlib are
- unstable
- — prefer
- kotlinx.collections.immutable
- .
- @Immutable
- data
- class
- UserProfile
- (
- val
- name
- :
- String
- ,
- val
- avatarUrl
- :
- String
- )
- Modifier ordering matters:
- Apply modifiers in logical order (size → padding → background → clickable).
- // Correct: padding inside clickable area
- Modifier
- .
- size
- (
- 48
- .
- dp
- )
- .
- clip
- (
- CircleShape
- )
- .
- clickable
- (
- onClick
- =
- onClick
- )
- .
- padding
- (
- 8
- .
- dp
- )
- Compose Layouts and Custom Layouts
- // Custom layout example: badge overlay
- @Composable
- fun
- BadgeBox
- (
- badgeCount
- :
- Int
- ,
- content
- :
- @Composable
- (
- )
- ->
- Unit
- )
- {
- Layout
- (
- content
- =
- {
- content
- (
- )
- if
- (
- badgeCount
- >
- 0
- )
- {
- Box
- (
- Modifier
- .
- background
- (
- Color
- .
- Red
- ,
- CircleShape
- )
- )
- {
- Text
- (
- "
- $
- badgeCount
- "
- ,
- color
- =
- Color
- .
- White
- ,
- fontSize
- =
- 10
- .
- sp
- )
- }
- }
- }
- )
- {
- measurables
- ,
- constraints
- ->
- val
- contentPlaceable
- =
- measurables
- [
- 0
- ]
- .
- measure
- (
- constraints
- )
- val
- badgePlaceable
- =
- measurables
- .
- getOrNull
- (
- 1
- )
- ?
- .
- measure
- (
- Constraints
- (
- )
- )
- layout
- (
- contentPlaceable
- .
- width
- ,
- contentPlaceable
- .
- height
- )
- {
- contentPlaceable
- .
- placeRelative
- (
- 0
- ,
- 0
- )
- badgePlaceable
- ?
- .
- placeRelative
- (
- contentPlaceable
- .
- width
- -
- badgePlaceable
- .
- width
- /
- 2
- ,
- -
- badgePlaceable
- .
- height
- /
- 2
- )
- }
- }
- }
- CompositionLocal
- Use
- CompositionLocal
- to propagate ambient data through the composition tree without
- threading it explicitly through every composable.
- // Define
- val
- LocalSnackbarHostState
- =
- compositionLocalOf
- <
- SnackbarHostState
- >
- {
- error
- (
- "No SnackbarHostState provided"
- )
- }
- // Provide at a high level
- CompositionLocalProvider
- (
- LocalSnackbarHostState provides snackbarHostState
- )
- {
- MyAppContent
- (
- )
- }
- // Consume anywhere below
- val
- snackbarHostState
- =
- LocalSnackbarHostState
- .
- current
- When to use:
- User preferences (theme, locale), shared services (analytics, navigation).
- When to avoid:
- Data that changes frequently or should be passed explicitly.
- Animations
- // Simple animated visibility
- AnimatedVisibility
- (
- visible
- =
- showDetails
- )
- {
- DetailsPanel
- (
- )
- }
- // Animated value
- val
- alpha
- by
- animateFloatAsState
- (
- targetValue
- =
- if
- (
- isEnabled
- )
- 1f
- else
- 0.4f
- ,
- animationSpec
- =
- tween
- (
- durationMillis
- =
- 300
- )
- ,
- label
- =
- "alpha"
- ,
- )
- // Shared element transition (Compose 1.7+)
- SharedTransitionLayout
- {
- AnimatedContent
- (
- targetState
- =
- selectedItem
- )
- {
- item
- ->
- if
- (
- item
- ==
- null
- )
- {
- ListScreen
- (
- onItemClick
- =
- {
- selectedItem
- =
- it
- }
- ,
- animatedVisibilityScope
- =
- this
- ,
- sharedTransitionScope
- =
- this
- @SharedTransitionLayout
- ,
- )
- }
- else
- {
- DetailScreen
- (
- item
- =
- item
- ,
- animatedVisibilityScope
- =
- this
- ,
- sharedTransitionScope
- =
- this
- @SharedTransitionLayout
- ,
- )
- }
- }
- }
- 2. Kotlin Coroutines and Flow
- Coroutines Fundamentals
- // ViewModel: use viewModelScope (auto-cancelled on VM cleared)
- class
- OrderViewModel
- @Inject
- constructor
- (
- private
- val
- orderRepository
- :
- OrderRepository
- ,
- )
- :
- ViewModel
- (
- )
- {
- fun
- placeOrder
- (
- order
- :
- Order
- )
- {
- viewModelScope
- .
- launch
- {
- try
- {
- orderRepository
- .
- placeOrder
- (
- order
- )
- }
- catch
- (
- e
- :
- HttpException
- )
- {
- // handle error
- }
- }
- }
- }
- // Repository: return suspend fun or Flow, never launch internally
- class
- OrderRepositoryImpl
- @Inject
- constructor
- (
- private
- val
- api
- :
- OrderApi
- ,
- private
- val
- dao
- :
- OrderDao
- ,
- )
- :
- OrderRepository
- {
- override
- suspend
- fun
- placeOrder
- (
- order
- :
- Order
- )
- {
- api
- .
- placeOrder
- (
- order
- .
- toRequest
- (
- )
- )
- dao
- .
- insert
- (
- order
- .
- toEntity
- (
- )
- )
- }
- }
- Dispatcher guidelines:
- Dispatchers.Main
-
- UI interactions, state updates
- Dispatchers.IO
-
- Network calls, file/database I/O
- Dispatchers.Default
- CPU-intensive computations
// withContext switches dispatcher for a block
suspend
fun
loadImage
(
url
:
String
)
:
Bitmap
=
withContext
(
Dispatchers
.
IO
)
{
URL
(
url
)
.
readBytes
(
)
.
let
{
BitmapFactory
.
decodeByteArray
(
it
,
0
,
it
.
size
)
}
}
Flow
Use
Flow
for reactive streams. Prefer
StateFlow
/
SharedFlow
in ViewModels.
// Repository: expose cold Flow
fun
observeOrders
(
)
:
Flow
<
List
<
Order
= dao . observeAll ( ) . map { entities -> entities . map { it . toModel ( ) } } // ViewModel: convert to StateFlow for UI class OrderListViewModel @Inject constructor ( repo : OrderRepository ) : ViewModel ( ) { val orders : StateFlow < List < Order
= repo . observeOrders ( ) . stateIn ( scope = viewModelScope , started = SharingStarted . WhileSubscribed ( 5_000 ) , initialValue = emptyList ( ) , ) } // Compose: collect safely with lifecycle awareness val orders by viewModel . orders . collectAsStateWithLifecycle ( ) Flow operators to know: flow . filter { it . isActive } . map { it . toUiModel ( ) } . debounce ( 300 ) // search input debounce . distinctUntilChanged ( ) . catch { e -> emit ( emptyList ( ) ) } // handle errors inline . flowOn ( Dispatchers . IO ) // run upstream on IO dispatcher SharedFlow for one-shot events: private val _events = MutableSharedFlow < UiEvent
( ) val events : SharedFlow < UiEvent
= _events . asSharedFlow ( ) // Emit from ViewModel fun onSubmit ( ) { viewModelScope . launch { _events . emit ( UiEvent . NavigateToHome ) } } // Collect in Composable LaunchedEffect ( Unit ) { viewModel . events . collect { event -> when ( event ) { is UiEvent . NavigateToHome -> navController . navigate ( Route . Home ) is UiEvent . ShowError -> snackbar . showSnackbar ( event . message ) } } } 3. Android Architecture Components ViewModel @HiltViewModel class ProductDetailViewModel @Inject constructor ( savedStateHandle : SavedStateHandle , private val getProductUseCase : GetProductUseCase , ) : ViewModel ( ) { private val productId : String = checkNotNull ( savedStateHandle [ "productId" ] ) private val _uiState = MutableStateFlow < ProductDetailUiState
( ProductDetailUiState . Loading ) val uiState : StateFlow < ProductDetailUiState
= _uiState . asStateFlow ( ) init { loadProduct ( ) } private fun loadProduct ( ) { viewModelScope . launch { _uiState . value = try { val product = getProductUseCase ( productId ) ProductDetailUiState . Success ( product ) } catch ( e : Exception ) { ProductDetailUiState . Error ( e . message ?: "Unknown error" ) } } } } sealed interface ProductDetailUiState { data object Loading : ProductDetailUiState data class Success ( val product : Product ) : ProductDetailUiState data class Error ( val message : String ) : ProductDetailUiState } Room @Entity ( tableName = "orders" ) data class OrderEntity ( @PrimaryKey val id : String , val customerId : String , val totalCents : Long , val status : String , val createdAt : Long , ) @Dao interface OrderDao { @Query ( "SELECT * FROM orders ORDER BY createdAt DESC" ) fun observeAll ( ) : Flow < List < OrderEntity
@Query ( "SELECT * FROM orders WHERE id = :id" ) suspend fun getById ( id : String ) : OrderEntity ? @Insert ( onConflict = OnConflictStrategy . REPLACE ) suspend fun insert ( order : OrderEntity ) @Delete suspend fun delete ( order : OrderEntity ) } @Database ( entities = [ OrderEntity :: class ] , version = 2 ) @TypeConverters ( Converters :: class ) abstract class AppDatabase : RoomDatabase ( ) { abstract fun orderDao ( ) : OrderDao } Migration example: val MIGRATION_1_2 = object : Migration ( 1 , 2 ) { override fun migrate ( db : SupportSQLiteDatabase ) { db . execSQL ( "ALTER TABLE orders ADD COLUMN notes TEXT NOT NULL DEFAULT ''" ) } } WorkManager Use for deferrable, guaranteed background work. class SyncWorker @AssistedInject constructor ( @Assisted context : Context , @Assisted params : WorkerParameters , private val syncRepository : SyncRepository , ) : CoroutineWorker ( context , params ) { override suspend fun doWork ( ) : Result = try { syncRepository . sync ( ) Result . success ( ) } catch ( e : Exception ) { if ( runAttemptCount < 3 ) Result . retry ( ) else Result . failure ( ) } @AssistedFactory interface Factory : ChildWorkerFactory < SyncWorker
} // Schedule periodic sync val syncRequest = PeriodicWorkRequestBuilder < SyncWorker
( 1 , TimeUnit . HOURS ) . setConstraints ( Constraints ( requiredNetworkType = NetworkType . CONNECTED ) ) . setBackoffCriteria ( BackoffPolicy . EXPONENTIAL , 10 , TimeUnit . MINUTES ) . build ( ) WorkManager . getInstance ( context ) . enqueueUniquePeriodicWork ( "sync" , ExistingPeriodicWorkPolicy . KEEP , syncRequest , ) 4. Dependency Injection with Hilt Module Setup @HiltAndroidApp class MyApplication : Application ( ) // Activity/Fragment @AndroidEntryPoint class MainActivity : ComponentActivity ( ) { / ... / } // Network module @Module @InstallIn ( SingletonComponent :: class ) object NetworkModule { @Provides @Singleton fun provideOkHttpClient ( ) : OkHttpClient = OkHttpClient . Builder ( ) . addInterceptor ( AuthInterceptor ( ) ) . build ( ) @Provides @Singleton fun provideRetrofit ( okHttpClient : OkHttpClient ) : Retrofit = Retrofit . Builder ( ) . baseUrl ( "https://api.example.com/" ) . client ( okHttpClient ) . addConverterFactory ( GsonConverterFactory . create ( ) ) . build ( ) @Provides @Singleton fun provideOrderApi ( retrofit : Retrofit ) : OrderApi = retrofit . create ( OrderApi :: class . java ) } // Repository binding @Module @InstallIn ( SingletonComponent :: class ) abstract class RepositoryModule { @Binds @Singleton abstract fun bindOrderRepository ( impl : OrderRepositoryImpl ) : OrderRepository } Scopes Scope Component Lifetime @Singleton SingletonComponent Application lifetime @ActivityRetainedScoped ActivityRetainedComponent ViewModel lifetime @ActivityScoped ActivityComponent Activity lifetime @ViewModelScoped ViewModelComponent ViewModel lifetime @FragmentScoped FragmentComponent Fragment lifetime Hilt with WorkManager @HiltWorker class UploadWorker @AssistedInject constructor ( @Assisted context : Context , @Assisted params : WorkerParameters , private val uploadService : UploadService , ) : CoroutineWorker ( context , params ) { / ... / } 5. Navigation Component NavGraph with Type-Safe Arguments (Navigation 2.8+) // Define destinations as serializable objects/classes @Serializable object HomeRoute @Serializable object ProfileRoute @Serializable data class ProductDetailRoute ( val productId : String ) // Build NavGraph @Composable fun AppNavGraph ( navController : NavHostController ) { NavHost ( navController , startDestination = HomeRoute ) { composable < HomeRoute
{ HomeScreen ( onProductClick = { id -> navController . navigate ( ProductDetailRoute ( id ) ) } ) } composable < ProductDetailRoute
{ backStackEntry -> val args = backStackEntry . toRoute < ProductDetailRoute
( ) ProductDetailScreen ( productId = args . productId ) } composable < ProfileRoute
{ ProfileScreen ( ) } } } Deep Links composable < ProductDetailRoute
( deepLinks = listOf ( navDeepLink < ProductDetailRoute
( basePath = "https://example.com/product" ) ) ) { / ... / } Declare in AndroidManifest.xml : < activity android: name = " .MainActivity "
< 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 = " example.com " /> </ intent-filter
</ activity
Bottom Navigation @Composable fun MainScreen ( ) { val navController = rememberNavController ( ) val currentBackStack by navController . currentBackStackEntryAsState ( ) val currentDestination = currentBackStack ? . destination Scaffold ( bottomBar = { NavigationBar { TopLevelDestination . entries . forEach { dest -> NavigationBarItem ( icon = { Icon ( dest . icon , contentDescription = dest . label ) } , label = { Text ( dest . label ) } , selected = currentDestination ? . hasRoute ( dest . route :: class ) == true , onClick = { navController . navigate ( dest . route ) { popUpTo ( navController . graph . findStartDestination ( ) . id ) { saveState = true } launchSingleTop = true restoreState = true } } , ) } } } ) { padding -> AppNavGraph ( navController = navController , modifier = Modifier . padding ( padding ) ) } } 6. Android Testing Unit Testing (JUnit5 + MockK) @ExtendWith ( MockKExtension :: class ) class GetProductUseCaseTest { @MockK lateinit var repository : ProductRepository private lateinit var useCase : GetProductUseCase @BeforeEach fun setUp ( ) { useCase = GetProductUseCase ( repository ) } @Test fun
returns product when repository succeeds( ) = runTest { val product = Product ( id = "1" , name = "Widget" , priceCents = 999 ) coEvery { repository . getProduct ( "1" ) } returns product val result = useCase ( "1" ) assertThat ( result ) . isEqualTo ( product ) } @Test funthrows exception when product not found( ) = runTest { coEvery { repository . getProduct ( "missing" ) } throws NotFoundException ( "missing" ) assertThrows < NotFoundException{ useCase ( "missing" ) } } } ViewModel Testing @OptIn ( ExperimentalCoroutinesApi :: class ) class ProductDetailViewModelTest { @get:Rule val mainDispatcherRule = MainDispatcherRule ( ) private val repository = mockk < ProductRepository
( ) private lateinit var viewModel : ProductDetailViewModel @Before fun setUp ( ) { viewModel = ProductDetailViewModel ( savedStateHandle = SavedStateHandle ( mapOf ( "productId" to "abc" ) ) , getProductUseCase = GetProductUseCase ( repository ) , ) } @Test fun
uiState is Loading initially then Success( ) = runTest { val product = Product ( "abc" , "Gizmo" , 1299 ) coEvery { repository . getProduct ( "abc" ) } returns product val states = mutableListOf < ProductDetailUiState( ) val job = launch { viewModel . uiState . toList ( states ) } advanceUntilIdle ( ) job . cancel ( ) assertThat ( states ) . contains ( ProductDetailUiState . Loading ) assertThat ( states . last ( ) ) . isEqualTo ( ProductDetailUiState . Success ( product ) ) } } class MainDispatcherRule ( private val dispatcher : TestCoroutineDispatcher = TestCoroutineDispatcher ( ) , ) : TestWatcher ( ) { override fun starting ( description : Description ) { Dispatchers . setMain ( dispatcher ) } override fun finished ( description : Description ) { Dispatchers . resetMain ( ) dispatcher . cleanupTestCoroutines ( ) } } Compose UI Testing class LoginScreenTest { @get:Rule val composeTestRule = createComposeRule ( ) @Test fun
submit button disabled when fields are empty( ) { composeTestRule . setContent { LoginScreen ( onLoginSuccess = { } ) } composeTestRule . onNodeWithText ( "Log in" ) . assertIsNotEnabled ( ) } @Test fundisplays error message on invalid credentials( ) { composeTestRule . setContent { LoginScreen ( onLoginSuccess = { } ) } composeTestRule . onNodeWithText ( "Email" ) . performTextInput ( "bad@example.com" ) composeTestRule . onNodeWithText ( "Password" ) . performTextInput ( "wrongpass" ) composeTestRule . onNodeWithText ( "Log in" ) . performClick ( ) composeTestRule . onNodeWithText ( "Invalid credentials" ) . assertIsDisplayed ( ) } } Espresso (Legacy / Hybrid Apps) @RunWith ( AndroidJUnit4 :: class ) class MainActivityTest { @get:Rule val activityRule = ActivityScenarioRule ( MainActivity :: class . java ) @Test fun navigatesToDetailScreen ( ) { onView ( withId ( R . id . product_list ) ) . perform ( RecyclerViewActions . actionOnItemAtPosition < RecyclerView . ViewHolder( 0 , click ( ) ) ) onView ( withId ( R . id . product_title ) ) . check ( matches ( isDisplayed ( ) ) ) } } Robolectric (Fast JVM Tests) @RunWith ( RobolectricTestRunner :: class ) @Config ( sdk = [ 34 ] ) class NotificationHelperTest { @Test fun
creates notification with correct channel( ) { val context = ApplicationProvider . getApplicationContext < Context( ) val helper = NotificationHelper ( context ) helper . showOrderNotification ( orderId = "42" , message = "Your order shipped!" ) val nm = context . getSystemService ( Context . NOTIFICATION_SERVICE ) as NotificationManager assertThat ( nm . activeNotifications ) . hasSize ( 1 ) } } 7. Performance Baseline Profiles Baseline Profiles pre-compile hot paths during app installation, reducing JIT overhead. // app/src/main/baseline-prof.txt (auto-generated by Macrobenchmark) // Or use the Baseline Profile Gradle Plugin: // build.gradle.kts (app) plugins { id ( "androidx.baselineprofile" ) } // Generate: ./gradlew :app:generateBaselineProfile Macrobenchmark for generation: @RunWith ( AndroidJUnit4 :: class ) class BaselineProfileGenerator { @get:Rule val rule = BaselineProfileRule ( ) @Test fun generate ( ) = rule . collect ( packageName = "com.example.myapp" ) { pressHome ( ) startActivityAndWait ( ) // Interact with critical user journeys device . findObject ( By . text ( "Products" ) ) . click ( ) device . waitForIdle ( ) } } Tracing / Systrace // Add custom trace sections trace ( "MyExpensiveOperation" ) { performExpensiveWork ( ) } // Compose compiler metrics — add to build.gradle.kts tasks . withType < KotlinCompile
( ) . configureEach { compilerOptions . freeCompilerArgs . addAll ( "-P" , "plugin:androidx.compose.compiler.plugins.kotlin:reportsDestination= ${ layout . buildDirectory . get ( ) } /compose_metrics" ) } Memory Profiling Use Android Studio Memory Profiler to capture heap dumps. Look for Bitmap leaks, Context leaks in static fields, and unclosed Cursor objects. Use LeakCanary in debug builds for automatic leak detection. // Avoid Context leaks: use applicationContext for long-lived objects class ImageCache @Inject constructor ( @ApplicationContext private val context : Context // Safe: application scope ) { / ... / } LazyList Performance LazyColumn { items ( items = itemList , key = { item -> item . id } , // Stable key prevents unnecessary recompositions contentType = { item -> item . type } , // Enables item recycling by type ) { item -> ItemRow ( item = item ) } } 8. Material Design 3 Theming // Define color scheme val LightColorScheme = lightColorScheme ( primary = Color ( 0xFF6750A4 ) , onPrimary = Color ( 0xFFFFFFFF ) , secondary = Color ( 0xFF625B71 ) , // ... other tokens ) // Apply theme @Composable fun MyAppTheme ( darkTheme : Boolean = isSystemInDarkTheme ( ) , content : @Composable ( ) -> Unit , ) { val colorScheme = if ( darkTheme ) DarkColorScheme else LightColorScheme MaterialTheme ( colorScheme = colorScheme , typography = AppTypography , shapes = AppShapes , content = content , ) } Dynamic Color (Android 12+) @Composable fun MyAppTheme ( darkTheme : Boolean = isSystemInDarkTheme ( ) , content : @Composable ( ) -> Unit ) { val context = LocalContext . current val colorScheme = when { Build . VERSION . SDK_INT = Build . VERSION_CODES . S -> { if ( darkTheme ) dynamicDarkColorScheme ( context ) else dynamicLightColorScheme ( context ) } darkTheme -> DarkColorScheme else -> LightColorScheme } MaterialTheme ( colorScheme = colorScheme , content = content ) } Key M3 Components // Top App Bar TopAppBar ( title = { Text ( "Orders" ) } , navigationIcon = { IconButton ( onClick = onBack ) { Icon ( Icons . AutoMirrored . Filled . ArrowBack , "Back" ) } } , actions = { IconButton ( onClick = onSearch ) { Icon ( Icons . Default . Search , "Search" ) } } , ) // Card ElevatedCard ( modifier = Modifier . fillMaxWidth ( ) . clickable ( onClick = onClick ) , ) { Column ( Modifier . padding ( 16 . dp ) ) { Text ( text = title , style = MaterialTheme . typography . titleMedium ) Text ( text = subtitle , style = MaterialTheme . typography . bodyMedium ) } } // FAB FloatingActionButton ( onClick = onAdd ) { Icon ( Icons . Default . Add , contentDescription = "Add" ) } 9. Modern Android Patterns MVI (Model-View-Intent) MVI is the recommended pattern for Compose apps. State flows one direction; intents describe user actions. // Intent (user actions) sealed interface ProductListIntent { data object LoadProducts : ProductListIntent data class SearchQueryChanged ( val query : String ) : ProductListIntent data class ProductClicked ( val id : String ) : ProductListIntent } // UI State data class ProductListUiState ( val isLoading : Boolean = false , val products : List < Product
= emptyList ( ) , val error : String ? = null , val searchQuery : String = "" , ) // One-shot effects sealed interface ProductListEffect { data class NavigateToDetail ( val productId : String ) : ProductListEffect } @HiltViewModel class ProductListViewModel @Inject constructor ( private val getProductsUseCase : GetProductsUseCase , ) : ViewModel ( ) { private val _uiState = MutableStateFlow ( ProductListUiState ( ) ) val uiState : StateFlow < ProductListUiState
= _uiState . asStateFlow ( ) private val _effect = MutableSharedFlow < ProductListEffect
( ) val effect : SharedFlow < ProductListEffect
= _effect . asSharedFlow ( ) fun handleIntent ( intent : ProductListIntent ) { when ( intent ) { is ProductListIntent . LoadProducts -> loadProducts ( ) is ProductListIntent . SearchQueryChanged -> updateSearch ( intent . query ) is ProductListIntent . ProductClicked -> { viewModelScope . launch { _effect . emit ( ProductListEffect . NavigateToDetail ( intent . id ) ) } } } } private fun loadProducts ( ) { viewModelScope . launch { _uiState . update { it . copy ( isLoading = true , error = null ) } _uiState . update { try { it . copy ( isLoading = false , products = getProductsUseCase ( ) ) } catch ( e : Exception ) { it . copy ( isLoading = false , error = e . message ) } } } } } Clean Architecture Layers presentation/ ui/ — Composables, screens viewmodel/ — ViewModels, UI State, Intents domain/ model/ — Domain entities (pure Kotlin, no Android deps) repository/ — Repository interfaces usecase/ — Business logic (one use case per file) data/ repository/ — Repository implementations remote/ — API service interfaces, DTOs, mappers local/ — Room entities, DAOs, mappers di/ — Hilt modules Use Case example: class GetFilteredProductsUseCase @Inject constructor ( private val productRepository : ProductRepository , ) { suspend operator fun invoke ( query : String ) : List < Product
= productRepository . getProducts ( ) . filter { it . name . contains ( query , ignoreCase = true ) } . sortedBy { it . name } } 10. App Bundle and Publishing Build Configuration // build.gradle.kts (app) android { defaultConfig { applicationId = "com.example.myapp" minSdk = 26 targetSdk = 35 versionCode = 10 versionName = "1.2.0" } buildTypes { release { isMinifyEnabled = true isShrinkResources = true proguardFiles ( getDefaultProguardFile ( "proguard-android-optimize.txt" ) , "proguard-rules.pro" ) signingConfig = signingConfigs . getByName ( "release" ) } debug { applicationIdSuffix = ".debug" versionNameSuffix = "-DEBUG" } } bundle { language { enableSplit = true } density { enableSplit = true } abi { enableSplit = true } } } Signing Configuration (via environment variables — never commit keystores) signingConfigs { create ( "release" ) { storeFile = file ( System . getenv ( "KEYSTORE_PATH" ) ?: "release.jks" ) storePassword = System . getenv ( "KEYSTORE_PASSWORD" ) keyAlias = System . getenv ( "KEY_ALIAS" ) keyPassword = System . getenv ( "KEY_PASSWORD" ) } } ProGuard Rules
Keep data classes used for serialization
-keep class com.example.myapp.data.remote.dto.* { ; }
Keep Hilt-generated classes
-keepnames @dagger.hilt.android.lifecycle.HiltViewModel class * extends androidx.lifecycle.ViewModel
Retrofit
-keepattributes Signature, Exceptions
-keep class retrofit2.* { ; }
Iron Laws
ALWAYS collect Flow in Compose with
collectAsStateWithLifecycle()
— never use
collectAsState()
which ignores lifecycle;
collectAsStateWithLifecycle()
pauses collection when the app is backgrounded, preventing resource waste.
NEVER expose mutable state from ViewModel
— expose
StateFlow
/
SharedFlow
via
asStateFlow()
/
asSharedFlow()
; keep
MutableStateFlow
/
MutableSharedFlow
private to prevent external mutation.
ALWAYS provide content descriptions for icon-only buttons
— screen readers cannot convey icon meaning without
contentDescription
; never pass
null
to icons in interactive elements.
NEVER use
runBlocking
in production code
—
runBlocking
blocks the calling thread; use
viewModelScope.launch
or
lifecycleScope.launch
for all coroutine launches.
ALWAYS provide stable keys in
LazyColumn
/
LazyRow
— missing
key
lambda causes full list recomposition on any data change; always use
key = { item.id }
.
Anti-Patterns to Avoid
Anti-pattern
Preferred
StateFlow
in
init {}
without
WhileSubscribed
Use
SharingStarted.WhileSubscribed(5_000)
to avoid upstreams when no UI is present
Calling
collect
in
LaunchedEffect
without lifecycle awareness
Use
collectAsStateWithLifecycle()
Passing
Activity
/
Fragment
context to ViewModel
Use
@ApplicationContext
or
SavedStateHandle
Business logic in Composables
Put logic in ViewModel/UseCase
mutableListOf()
as Compose state
Use
mutableStateListOf()
or
MutableStateFlow>
Hardcoded strings in Composables
Use
stringResource(R.string.key)
runBlocking
in production code
Use coroutines properly;
runBlocking
blocks the thread
GlobalScope.launch
Use
viewModelScope
or
lifecycleScope
Mutable state exposed from ViewModel
Expose
StateFlow
/
SharedFlow
; keep mutable state private
Accessibility
// Provide content descriptions for icon-only buttons
IconButton
(
onClick
=
onFavorite
)
{
Icon
(
imageVector
=
if
(
isFavorite
)
Icons
.
Filled
.
Favorite
else
Icons
.
Outlined
.
FavoriteBorder
,
contentDescription
=
if
(
isFavorite
)
"Remove from favorites"
else
"Add to favorites"
,
)
}
// Use semantic roles for custom components
Box
(
modifier
=
Modifier
.
semantics
{
role
=
Role
.
Switch
stateDescription
=
if
(
isChecked
)
"On"
else
"Off"
}
.
clickable
(
onClick
=
onToggle
)
)
// Merge descendants to reduce TalkBack verbosity
Row
(
modifier
=
Modifier
.
semantics
(
mergeDescendants
=
true
)
{
}
)
{
Icon
(
Icons
.
Default
.
Star
,
contentDescription
=
null
)
// null = decorative
Text
(
"4.5 stars"
)
}
Reviewing Compose State
User: "Is this pattern correct for search?"
@Composable
fun
SearchBar
(
onQueryChange
:
(
String
)
->
Unit
)
{
var
query
by
remember
{
mutableStateOf
(
""
)
}
TextField
(
value
=
query
,
onValueChange
=
{
query
=
it
;
onQueryChange
(
it
)
}
,
label
=
{
Text
(
"Search"
)
}
)
}
Review:
remember
is appropriate for transient UI input.
Consider
rememberSaveable
if you want the query to survive configuration changes.
Add
debounce
in the ViewModel rather than calling
onQueryChange
on every keystroke; this avoids unnecessary searches.
Missing:
modifier
parameter for the caller to control layout.
Diagnosing Excessive Recomposition
User: "My list recomposes entirely when one item changes"
Root cause and fix:
Add
key = { item.id }
to
items()
in
LazyColumn
so Compose can track items by identity.
Ensure
Item
data class is
@Stable
or
@Immutable
with stable field types.
Use
kotlinx.collections.immutable.ImmutableList
instead of
List