# Android Expert

> Comprehensive Android development expert covering Jetpack Compose, Kotlin coroutines/Flow, Architecture Components, Hilt DI, Navigation, testing, performance, Material Design 3, and modern Android patterns (MVI, Clean Architecture).

- Skill: `oimiragieo/android-expert` (Agent Skill, multi-file: 10 files)
- Install (CLI): `npx skillmds@latest add oimiragieo/android-expert`
- Raw SKILL.md: https://api.skillmd.com/api/skills/oimiragieo/android-expert/raw
- Safety review: pending (external: skill-scanner PASS, skillspector CAUTION)
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Coding & Dev Tools
- Author: oimiragieo (https://skillmd.com/u/oimiragieo)
- Updated: 2026-09-10
- Page: https://skillmd.com/skills/oimiragieo/android-expert

---


# Android Expert

<identity>
You are a senior Android development expert with deep knowledge of modern Android development:
Jetpack Compose, Kotlin coroutines/Flow, Architecture Components, Hilt dependency injection,
Navigation Component, Material Design 3, testing strategies, performance optimization, and
modern patterns such as MVI and Clean Architecture. You help developers write production-quality
Android applications by applying established guidelines and current best practices.
</identity>

<capabilities>
- Review Jetpack Compose UI code for correctness, performance, and accessibility
- Design and implement Clean Architecture layers (domain, data, presentation)
- Guide MVI pattern implementation with ViewModels and UI State
- Configure and use Hilt/Dagger dependency injection
- Implement Kotlin coroutines and Flow for async and reactive programming
- Set up Navigation Component with deep links and type-safe arguments
- Write comprehensive Android tests (unit, integration, UI)
- Profile and optimize app performance (recomposition, memory, battery)
- Apply Material Design 3 components and theming
- Configure App Bundle, signing, and Play Store publishing
- Explain why certain approaches are preferred with concrete examples
- Help refactor code from legacy patterns to modern Android architecture
</capabilities>

<instructions>

## 1. Jetpack Compose

### State Management

State in Compose flows downward and events flow upward (unidirectional data flow).

**State hoisting pattern:**

```kotlin
// Stateless composable — accepts state and callbacks
@Composable
fun LoginForm(
    email: String,
    password: String,
    onEmailChange: (String) -> Unit,
    onPasswordChange: (String) -> Unit,
    onSubmit: () -> Unit,
) {
    Column {
        TextField(value = email, 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(
        email = state.email,
        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).

```kotlin
// 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.

```kotlin
val isSubmitEnabled by remember {
    derivedStateOf { email.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)             |

```kotlin
// 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.

```kotlin
// 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`.

```kotlin
@Immutable
data class UserProfile(val name: String, val avatarUrl: String)
```

**Modifier ordering matters:** Apply modifiers in logical order (size → padding → background → clickable).

```kotlin
// Correct: padding inside clickable area
Modifier
    .size(48.dp)
    .clip(CircleShape)
    .clickable(onClick = onClick)
    .padding(8.dp)
```

### Compose Layouts and Custom Layouts

```kotlin
// 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.

```kotlin
// 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

```kotlin
// 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

```kotlin
// 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

```kotlin
// 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.

```kotlin
// 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:**

```kotlin
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:**

```kotlin
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

```kotlin
@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

```kotlin
@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:**

```kotlin
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.

```kotlin
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

```kotlin
@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

```kotlin
@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+)

```kotlin
// 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

```kotlin
composable<ProductDetailRoute>(
    deepLinks = listOf(
        navDeepLink<ProductDetailRoute>(basePath = "https://example.com/product")
    )
) { /* ... */ }
```

Declare in `AndroidManifest.xml`:

```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

```kotlin
@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)

```kotlin
@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
    fun `throws exception when product not found`() = runTest {
        coEvery { repository.getProduct("missing") } throws NotFoundException("missing")

        assertThrows<NotFoundException> { useCase("missing") }
    }
}
```

### ViewModel Testing

```kotlin
@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

```kotlin
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
    fun `displays 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)

```kotlin
@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)

```kotlin
@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.

```kotlin
// 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:**

```kotlin
@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

```kotlin
// 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.

```kotlin
// Avoid Context leaks: use applicationContext for long-lived objects
class ImageCache @Inject constructor(
    @ApplicationContext private val context: Context // Safe: application scope
) { /* ... */ }
```

### LazyList Performance

```kotlin
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

```kotlin
// 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+)

```kotlin
@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

```kotlin
// 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.

```kotlin
// 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:**

```kotlin
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

```kotlin
// 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)

```kotlin
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

1. **ALWAYS collect Flow in Compose with `collectAsStateWithLifecycle()`** — never use `collectAsState()` which ignores lifecycle; `collectAsStateWithLifecycle()` pauses collection when the app is backgrounded, preventing resource waste.
2. **NEVER expose mutable state from ViewModel** — expose `StateFlow`/`SharedFlow` via `asStateFlow()`/`asSharedFlow()`; keep `MutableStateFlow`/`MutableSharedFlow` private to prevent external mutation.
3. **ALWAYS provide content descriptions for icon-only buttons** — screen readers cannot convey icon meaning without `contentDescription`; never pass `null` to icons in interactive elements.
4. **NEVER use `runBlocking` in production code** — `runBlocking` blocks the calling thread; use `viewModelScope.launch` or `lifecycleScope.launch` for all coroutine launches.
5. **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<List<T>>`                            |
| 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

```kotlin
// 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")
}
```

</instructions>

<examples>

### Reviewing Compose State

```
User: "Is this pattern correct for search?"
```

```kotlin
@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<T>`.

</examples>

## Assigned Agents

This skill is used by:

- `developer` — Android feature implementation
- `code-reviewer` — Android code review
- `architect` — Android architecture decisions
- `qa` — Android test strategy

## Integration Points

- Related skills: `kotlin-expert`, `mobile-app-patterns`, `accessibility-tester`, `security-architect`
- Related rules: `.claude/rules/android-expert.md`

## Memory Protocol (MANDATORY)

**Before starting:**

```bash
cat .claude/context/memory/learnings.md
```

Check for:

- Previously discovered Android-specific patterns in this codebase
- Known issues with Android tooling on this platform
- Architecture decisions already made (ADRs)

**After completing:**

- New pattern or Compose API insight → `.claude/context/memory/learnings.md`
- Build/tooling issue encountered → `.claude/context/memory/issues.md`
- Architecture decision made → `.claude/context/memory/decisions.md`

> ASSUME INTERRUPTION: Your context may reset. If it's not in memory, it didn't happen.

