Android Native
Purpose
Implement Android native applications using Kotlin, Jetpack Compose, MVVM+Clean Architecture, Coroutines+Flow, Hilt DI, Room, and comprehensive testing.
Agent Protocol
Trigger
User request includes: android, kotlin, jetpack, compose, android architecture, android testing, room, hilt.
Input Context
- Android SDK version (compileSdk / minSdk / targetSdk)
- Kotlin version
- Build system (Gradle KTS)
- Architecture pattern (MVVM, Clean, MVI)
Output Artifact
A markdown document containing:
- Project structure
- Jetpack Compose setup
- MVVM+Clean Architecture
- Coroutines+Flow concurrency
- Hilt dependency injection
- Room database setup
- JUnit + Espresso test plan
Response Format
Produce the artifact directly. No preamble, no postamble, no explanations. No filler, no hedging, no transitions. Strip articles a/an/the where unambiguous. Compress output — why use many token when few do trick.
Max Response Length
4096 tokens
Workflow
Step 1: Set Up Project Structure
Organize code with Clean Architecture layers: data, domain, ui, and di with feature-based packaging.
Step 2: Implement Domain Layer
Define domain models, repository interfaces, and use cases independent of framework dependencies.
Step 3: Implement Data Layer
Set up Room database with DAOs and entities, Retrofit API service, and repository implementations bridging local and remote sources.
Step 4: Configure Dependency Injection
Set up Hilt modules for database, network, and repository bindings with appropriate scopes.
Step 5: Build UI with Jetpack Compose
Create ViewModels with StateFlow, Compose screens with state collection, and handle loading/error/success states.
Step 6: Write Tests
Cover domain use cases with JUnit + MockK unit tests and UI screens with Compose + Espresso UI tests.
Architecture Decision Trees
Architecture Selection
App complexity?
├── Simple CRUD (<10 screens, no complex state)
│ → MVVM + Repository — ViewModel → Repository → API/DB
│ → No UseCase layer needed
├── Complex state (real-time, multi-step forms)
│ → MVI with sealed Intent/State classes, reducer pattern
│ → Single state object per screen
└── Large codebase (5+ teams, shared domain)
→ Clean Architecture with strict layer separation
→ Domain module shared across features
DI Strategy
Team preference?
├── Annotation-based → Hilt
│ Pros: compile-time verification, @HiltViewModel, less boilerplate
│ Cons: slower build, kapt/ksp overhead
├── Manual DI → Koin
│ Pros: no annotation processing, fast builds, easy to learn
│ Cons: runtime errors for missing bindings
└── Compiler-assisted → Dagger (raw)
Pros: most control, mature
Cons: steep learning curve, verbose
Navigation Approach
Single-activity?
├── Yes → Jetpack Navigation Compose
│ NavHost with sealed Route class, type-safe arguments
├── Multi-activity → Navigation component with XML
└── Feature-modules → Dynamic feature navigation with NavDeepLink
Image Loading Strategy
Network images needed?
├── Yes, Coil — Kotlin coroutines, Compose integration, memory-efficient
└── Yes, Glide — Mature, animated GIF/WebP, disk cache, OK
Coil preferred for new projects — lighter (150KB vs ~500KB)
Background Work Strategy
WorkManager vs Foreground Service vs AlarmManager
├── Deferrable, reliable → WorkManager (constraints, chaining, battery-optimized)
├── User-visible must continue → Foreground Service (notification required)
└── Exact timing needed → AlarmManager (limited, consider ScheduleTaskExecutor)
Project Structure
app/
├── src/main/java/com/example/app/
│ ├── App.kt
│ ├── MainActivity.kt
│ ├── data/
│ │ ├── local/
│ │ │ ├── AppDatabase.kt
│ │ │ ├── OrderDao.kt
│ │ │ └── OrderEntity.kt
│ │ ├── remote/
│ │ │ ├── ApiService.kt
│ │ │ └── OrderResponse.kt
│ │ └── repository/
│ │ └── OrderRepositoryImpl.kt
│ ├── di/
│ │ ├── AppModule.kt
│ │ ├── DatabaseModule.kt
│ │ └── NetworkModule.kt
│ ├── domain/
│ │ ├── model/
│ │ │ └── Order.kt
│ │ ├── repository/
│ │ │ └── OrderRepository.kt
│ │ └── usecase/
│ │ └── GetOrdersUseCase.kt
│ └── ui/
│ ├── orders/
│ │ ├── OrderListScreen.kt
│ │ ├── OrderDetailScreen.kt
│ │ └── OrderViewModel.kt
│ └── theme/
└── src/test/
└── ...
MVVM + Clean Architecture
// domain/model/Order.kt
data class Order(
val id: String,
val customerName: String,
val total: Double,
val status: OrderStatus
)
enum class OrderStatus { PENDING, SHIPPED, DELIVERED }
// domain/repository/OrderRepository.kt
interface OrderRepository {
suspend fun getOrders(): Result<List<Order>>
suspend fun getOrder(id: String): Result<Order>
}
// domain/usecase/GetOrdersUseCase.kt
class GetOrdersUseCase(private val repo: OrderRepository) {
suspend operator fun invoke(): Result<List<Order>> = repo.getOrders()
}
Jetpack Compose UI
// ui/orders/OrderViewModel.kt
@HiltViewModel
class OrderViewModel @Inject constructor(
private val getOrders: GetOrdersUseCase
) : ViewModel() {
private val _state = MutableStateFlow<OrderUiState>(OrderUiState.Loading)
val state: StateFlow<OrderUiState> = _state.asStateFlow()
fun loadOrders() {
viewModelScope.launch {
_state.value = OrderUiState.Loading
getOrders()
.onSuccess { _state.value = OrderUiState.Success(it) }
.onFailure { _state.value = OrderUiState.Error(it.message) }
}
}
}
sealed interface OrderUiState {
data object Loading : OrderUiState
data class Success(val orders: List<Order>) : OrderUiState
data class Error(val message: String?) : OrderUiState
}
// ui/orders/OrderListScreen.kt
@Composable
fun OrderListScreen(
viewModel: OrderViewModel = hiltViewModel(),
onOrderClick: (String) -> Unit
) {
val state by viewModel.state.collectAsStateWithLifecycle()
LaunchedEffect(Unit) { viewModel.loadOrders() }
when (val s = state) {
is OrderUiState.Loading -> CircularProgressIndicator()
is OrderUiState.Error -> ErrorMessage(s.message,
is OrderUiState.Success -> LazyColumn {
items(s.orders, key = { it.id }) { order ->
OrderCard(order = order, onOrderClick(order.id) })
}
}
}
}
@Composable
fun OrderCard(order: Order, onClick: () -> Unit) {
Card(modifier = Modifier.clickable(onClick = onClick)) {
Column(modifier = Modifier.padding(16.dp)) {
Text(text = order.customerName, style = MaterialTheme.typography.titleMedium)
Text(text = "$${order.total}", style = MaterialTheme.typography.bodyLarge)
}
}
}
Room Database
// data/local/OrderEntity.kt
@Entity(tableName = "orders")
data class OrderEntity(
@PrimaryKey val id: String,
@ColumnInfo(name = "customer_name") val customerName: String,
val total: Double,
val status: String
)
// data/local/OrderDao.kt
@Dao
interface OrderDao {
@Query("SELECT * FROM orders")
fun getAll(): Flow<List<OrderEntity>>
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insertAll(orders: List<OrderEntity>)
@Query("DELETE FROM orders WHERE id = :id")
suspend fun deleteById(id: String)
}
// data/local/AppDatabase.kt
@Database(entities = [OrderEntity::class], version = 1, exportSchema = true)
abstract class AppDatabase : RoomDatabase() {
abstract fun orderDao(): OrderDao
}
Hilt DI
// di/AppModule.kt
@Module
@InstallIn(SingletonComponent::class)
object AppModule {
@Provides @Singleton
fun provideOrderRepository(
api: ApiService,
dao: OrderDao
): OrderRepository = OrderRepositoryImpl(api, dao)
}
// di/NetworkModule.kt
@Module
@InstallIn(SingletonComponent::class)
object NetworkModule {
@Provides @Singleton
fun provideApiService(): ApiService {
return Retrofit.Builder()
.baseUrl("https://api.example.com/")
.addConverterFactory(MoshiConverterFactory.create())
.addCallAdapterFactory(CoroutineCallAdapterFactory())
.client(OkHttpClient.Builder()
.connectTimeout(30, TimeUnit.SECONDS)
.readTimeout(30, TimeUnit.SECONDS)
.addInterceptor(HttpLoggingInterceptor().apply {
level = if (BuildConfig.DEBUG) BODY else NONE
})
.build())
.build()
.create(ApiService::class.java)
}
}
// di/DatabaseModule.kt
@Module
@InstallIn(SingletonComponent::class)
object DatabaseModule {
@Provides @Singleton
fun provideDatabase(@ApplicationContext ctx: Context): AppDatabase {
return Room.databaseBuilder(ctx, AppDatabase::class.java, "app-db")
.fallbackToDestructiveMigration()
.build()
}
@Provides fun provideOrderDao(db: AppDatabase): OrderDao = db.orderDao()
}
Testing
Unit Test (JUnit + MockK)
class GetOrdersUseCaseTest {
private val repo = mockk<OrderRepository>()
private val useCase = GetOrdersUseCase(repo)
@Test
fun `returns orders successfully`() = runTest {
val orders = listOf(Order("1", "Test", 100.0, OrderStatus.PENDING))
coEvery { repo.getOrders() } returns Result.success(orders)
val result = useCase()
assertTrue(result.isSuccess)
assertEquals(orders, result.getOrThrow())
}
@Test
fun `returns failure when repository fails`() = runTest {
coEvery { repo.getOrders() } returns Result.failure(IOException("Network error"))
val result = useCase()
assertTrue(result.isFailure)
assertTrue(result.exceptionOrNull() is IOException)
}
}
UI Test (Compose + Espresso)
@RunWith(AndroidJUnit4::class)
class OrderListScreenTest {
@get:Rule
val composeTestRule = createComposeRule()
@Test
fun displaysOrders() {
val viewModel = mockk<OrderViewModel>()
every { viewModel.state } returns MutableStateFlow(
OrderUiState.Success(listOf(
Order("1", "Alice", 50.0, OrderStatus.PENDING)
))
).asStateFlow()
composeTestRule.setContent {
OrderListScreen(viewModel = viewModel,
}
composeTestRule.onNodeWithText("Alice").assertIsDisplayed()
}
@Test
fun showsErrorState() {
val viewModel = mockk<OrderViewModel>()
every { viewModel.state } returns MutableStateFlow(
OrderUiState.Error("Failed to load")
).asStateFlow()
composeTestRule.setContent {
OrderListScreen(viewModel = viewModel,
}
composeTestRule.onNodeWithText("Failed to load").assertIsDisplayed()
}
}
Compose Performance Patterns
// Stable data classes — Compose skips recomposition when inputs are stable
@Stable
data class OrderUiModel(val id: String, val name: String, val total: Double)
// derivedStateOf for expensive computations
val totalPrice by remember(orders) {
derivedStateOf { orders.sumOf { it.total } }
}
// LazyColumn keys for stable identity
items(orders, key = { it.id }) { order -> OrderCard(order) }
// Remember lambda to avoid allocation on recomposition
val { { id: String -> onOrderClick(id) } }
// ContentType for list-item differentiation
items(orders, key = { it.id }, contentType = { "order" }) { ... }
Compose Modifier Best Practices
- Modifier order matters:
size→padding→background→clickable(outer to inner) - Klibs
Modifier.composedfor reusable modifier chains - Avoid
Modifier.absoluteOffset— preferModifier.offsetfor RTL support Modifier.clipToBounds()to prevent content overflowModifier.alpha(0f)vsModifier.visible(false)— invisible keeps layout space, gone releases it
Production Considerations
- ProGuard/R8 rules: keep
@Keepannotations, Moshi adapters, Retrofit interfaces - Network security config for certificate pinning
- Baseline Profiles for startup optimization (30%+ faster cold start)
- LeakCanary for memory leak detection in debug builds
- Chucker for network inspection in debug builds
- StrictMode for detecting disk/network on main thread during development
- App Startup library for ordered, lazy SDK initialization
Kotlin Coroutines & Flow Best Practices
viewModelScope.launchfor fire-and-forget operations (logging, analytics)viewModelScope.asyncfor concurrent decomposition (parallel API calls)flatMapLatestfor search-as-you-type debounced queriesstateIn(WhileSubscribed(5000))to keep upstream active for 5s after last subscribercatch { emit(UiState.Error(it)) }for error recovery in Flow chainsRetrofit suspend functionsare main-safe — no need to wrap withwithContext(IO)- Room DAO
Flowautomatically runs on background thread Dispatchers.IOfor blocking operations,Dispatchers.Defaultfor CPU-heavy work
Package Structure Options
Feature-first (recommended for medium+ apps):
com.example.app/
├── feature/orders/
│ ├── data/ local/ remote/ repository/
│ ├── domain/ model/ repository/ usecase/
│ └── ui/ OrderListScreen OrderViewModel
├── core/ network/ database/ di/ testing/
Layer-first (simple apps):
com.example.app/
├── data/
├── domain/
├── ui/
└── di/
Anti-Patterns
- God ViewModels: ViewModel handling logic for multiple screens — split per screen
- Repository exposing raw data sources: Never return
ApiResultorDbModel— map to domain models - Hilt unscoped bindings: Always declare scope — unscoped = new instance every injection
- CollectAsState instead of collectAsStateWithLifecycle: Without lifecycle awareness, collection leaks when screen is in background
- MutableStateFlow exposure: Expose as
StateFlow, keepMutableStateFlowprivate - Using GlobalScope: Never use
GlobalScope.launch— creates leaked coroutines tied to app process - Main thread DB access: Room on main thread throws — use
suspendfunctions orFlow - Ignoring configuration changes: State in ViewModel survives rotation; state in
rememberdoes not - Deep layout hierarchy: Compose recomposes deeply nested layouts inefficiently — keep composable depth <10 levels
Rules
- Repository interfaces belong in domain layer; implementations in data layer
- ViewModels expose state via StateFlow, never expose mutable state
- Hilt modules must declare explicit scopes — no unscoped bindings for singletons
- Room operations on IO thread — use coroutine dispatchers or Room's built-in threading
- Compose screens collect StateFlow with collectAsStateWithLifecycle, not collectAsState
- Use sealed interfaces for UI state to represent loading, success, and error
- DI modules are in a separate package — never scatter provides across feature packages
- Baseline Profiles for all release builds targeting startup performance
- Compose Modifier order: size → padding → background → clickable (outside-in)
- Navigation routes defined in a single sealed class — never stringly-typed routes
- Image loading via Coil in Compose with
AsyncImageorrememberAsyncImagePainter - ProGuard rules must keep all Moshi/Retrofit/Kotlin serialization classes
References
- references/android-advanced.md — Android Advanced Topics
- references/android-architecture.md — Android Architecture — MVVM + Clean Architecture
- references/android-fundamentals.md — Android Fundamentals
- references/compose-performance.md — Jetpack Compose Performance Optimization
- references/hilt-di.md — Hilt Dependency Injection
- references/jetpack-compose.md — Jetpack Compose
- references/testing.md — Android Testing
Handoff
Hand off to mobile/universal/deployment/SKILL.md for deployment.