Complete production-ready Android development workflow with Jetpack Compose, MVVM, Clean Architecture, Material Design 3, and Hilt DI
Install
npx skillscat add wj071842154/android-production-workflow-skill Install via the SkillsCat registry.
SKILL.md
Android Production Workflow - Jetpack Compose + MVVM + Material Design 3
Version: 1.0.0
Author: wj071842154
Last Updated: 2026-02-13
๐ Overview
This skill provides a complete, production-ready Android development workflow using:
- Jetpack Compose for declarative UI
- MVVM + Clean Architecture for scalable structure
- Material Design 3 for modern UI/UX
- Hilt for dependency injection
- Kotlin Coroutines + Flow for async operations
- Retrofit for networking
- Room for local database
๐๏ธ Architecture Principles
Clean Architecture Layers
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ Presentation Layer โ
โ (UI, ViewModel, Compose Screens) โ
โโโโโโโโโโโโโโโฌโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ
โโโโโโโโโโโโโโโผโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ Domain Layer โ
โ (Use Cases, Domain Models, Repo โ
โ Interfaces - Pure Kotlin) โ
โโโโโโโโโโโโโโโฌโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ
โโโโโโโโโโโโโโโผโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ Data Layer โ
โ (Repository Impl, API, Database, โ
โ Data Models, Mappers) โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโMVVM Pattern
ViewModel holds UI state and business logic
View (Composable) observes state and renders UI
Model (Domain/Data) provides data through repositories
User Action โ Composable โ ViewModel โ Use Case โ Repository โ API/Database
โ โ
โโโโโโโโโโโโโโ StateFlow โโโโโโโโโโโโโโโโโโโโโโโโโโโโ๐ Project Structure
app/
โโโ src/main/java/com/yourapp/
โ โโโ MyApplication.kt # Hilt Application
โ โ
โ โโโ di/ # Dependency Injection
โ โ โโโ AppModule.kt
โ โ โโโ NetworkModule.kt
โ โ โโโ DatabaseModule.kt
โ โ
โ โโโ domain/ # Domain Layer (Pure Kotlin)
โ โ โโโ model/ # Domain Models
โ โ โโโ repository/ # Repository Interfaces
โ โ โโโ usecase/ # Business Logic Use Cases
โ โ
โ โโโ data/ # Data Layer
โ โ โโโ repository/ # Repository Implementations
โ โ โโโ remote/ # Remote Data Source
โ โ โ โโโ api/ # Retrofit API interfaces
โ โ โ โโโ dto/ # Data Transfer Objects
โ โ โโโ local/ # Local Data Source
โ โ โ โโโ database/ # Room Database
โ โ โ โโโ dao/ # Data Access Objects
โ โ โ โโโ entity/ # Database Entities
โ โ โโโ mapper/ # Data Mappers (DTO โ Domain)
โ โ
โ โโโ presentation/ # Presentation Layer
โ โโโ theme/ # Material 3 Theme
โ โ โโโ Color.kt
โ โ โโโ Theme.kt
โ โ โโโ Type.kt
โ โโโ navigation/
โ โ โโโ NavGraph.kt
โ โโโ components/ # Reusable Composables
โ โโโ screens/ # Feature Screens
โ โโโ [feature]/
โ โโโ [Feature]Screen.kt
โ โโโ [Feature]ViewModel.kt
โ โโโ [Feature]UiState.kt๐ฏ Core Patterns
1. StateFlow + UI State Pattern
Always use immutable data classes for UI state:
data class HomeUiState(
val isLoading: Boolean = false,
val data: List<Item> = emptyList(),
val error: String? = null
)
@HiltViewModel
class HomeViewModel @Inject constructor(
private val useCase: GetItemsUseCase
) : ViewModel() {
private val _uiState = MutableStateFlow(HomeUiState())
val uiState: StateFlow<HomeUiState> = _uiState.asStateFlow()
init {
loadData()
}
fun loadData() {
viewModelScope.launch {
_uiState.update { it.copy(isLoading = true) }
useCase().collect { result ->
result.fold(
onSuccess = { data ->
_uiState.update {
it.copy(isLoading = false, data = data, error = null)
}
},
onFailure = { exception ->
_uiState.update {
it.copy(isLoading = false, error = exception.message)
}
}
)
}
}
}
}2. Repository Pattern with Flow
// Domain Layer - Interface
interface UserRepository {
fun getUsers(): Flow<Result<List<User>>>
suspend fun getUserById(id: Int): Result<User>
}
// Data Layer - Implementation
class UserRepositoryImpl @Inject constructor(
private val apiService: ApiService,
private val userDao: UserDao
) : UserRepository {
override fun getUsers(): Flow<Result<List<User>>> = flow {
try {
// Try remote first
val response = apiService.getUsers()
if (response.isSuccessful) {
val users = response.body()?.map { it.toDomain() } ?: emptyList()
// Cache locally
userDao.insertAll(users.map { it.toEntity() })
emit(Result.success(users))
} else {
// Fallback to local cache
val cachedUsers = userDao.getAll().map { it.toDomain() }
emit(Result.success(cachedUsers))
}
} catch (e: Exception) {
// Return cached data on error
val cachedUsers = userDao.getAll().map { it.toDomain() }
if (cachedUsers.isNotEmpty()) {
emit(Result.success(cachedUsers))
} else {
emit(Result.failure(e))
}
}
}
}3. Use Case Pattern
Single Responsibility - One use case per business action:
class GetUsersUseCase @Inject constructor(
private val repository: UserRepository
) {
operator fun invoke(): Flow<Result<List<User>>> {
return repository.getUsers()
}
}
class LoginUseCase @Inject constructor(
private val repository: AuthRepository
) {
suspend operator fun invoke(email: String, password: String): Result<User> {
// Add business logic validation here
if (email.isBlank() || password.isBlank()) {
return Result.failure(Exception("Email and password required"))
}
return repository.login(email, password)
}
}4. Hilt Dependency Injection
// Application
@HiltAndroidApp
class MyApplication : Application()
// Network Module
@Module
@InstallIn(SingletonComponent::class)
object NetworkModule {
@Provides
@Singleton
fun provideRetrofit(): Retrofit {
return Retrofit.Builder()
.baseUrl("https://api.example.com/")
.addConverterFactory(GsonConverterFactory.create())
.build()
}
@Provides
@Singleton
fun provideApiService(retrofit: Retrofit): ApiService {
return retrofit.create(ApiService::class.java)
}
}
// Repository Binding
@Module
@InstallIn(SingletonComponent::class)
abstract class RepositoryModule {
@Binds
@Singleton
abstract fun bindUserRepository(
impl: UserRepositoryImpl
): UserRepository
}5. Compose UI with State Collection
@Composable
fun HomeScreen(
viewModel: HomeViewModel = hiltViewModel()
) {
val uiState by viewModel.uiState.collectAsStateWithLifecycle()
Scaffold(
topBar = { TopBar() }
) { paddingValues ->
when {
uiState.isLoading -> LoadingIndicator()
uiState.error != null -> ErrorView(uiState.error!!)
else -> ContentView(uiState.data)
}
}
}๐จ Material Design 3 Guidelines
Color Scheme Structure
// Use Material 3 color roles
val LightColorScheme = lightColorScheme(
primary = Color(0xFF6750A4),
onPrimary = Color(0xFFFFFFFF),
primaryContainer = Color(0xFFEADDFF),
onPrimaryContainer = Color(0xFF21005D),
secondary = Color(0xFF625B71),
// ... complete color scheme
)Typography System
val Typography = Typography(
displayLarge = TextStyle(fontSize = 57.sp, lineHeight = 64.sp),
headlineMedium = TextStyle(fontSize = 28.sp, lineHeight = 36.sp),
titleLarge = TextStyle(fontSize = 22.sp, lineHeight = 28.sp),
bodyLarge = TextStyle(fontSize = 16.sp, lineHeight = 24.sp),
labelMedium = TextStyle(fontSize = 12.sp, lineHeight = 16.sp)
)Component Usage
// Cards
Card(
onClick = { /* action */ },
elevation = CardDefaults.cardElevation(defaultElevation = 2.dp)
) { Content() }
// Buttons
Button(
onClick = { /* action */ },
colors = ButtonDefaults.buttonColors(
containerColor = MaterialTheme.colorScheme.primary
)
) { Text("Action") }
// Top App Bar
TopAppBar(
title = { Text("Title") },
colors = TopAppBarDefaults.topAppBarColors(
containerColor = MaterialTheme.colorScheme.primaryContainer
)
)โก Performance Best Practices
1. Recomposition Optimization
// โ
GOOD: Use keys for lists
LazyColumn {
items(items = users, key = { it.id }) { user ->
UserItem(user)
}
}
// โ
GOOD: Use derivedStateOf for expensive calculations
val sortedUsers by remember(users) {
derivedStateOf { users.sortedBy { it.name } }
}
// โ BAD: Don't perform heavy work during composition
@Composable
fun MyScreen(users: List<User>) {
val sorted = users.sortedBy { it.name } // Runs on every recomposition!
}2. State Hoisting
// โ
GOOD: Stateless composables
@Composable
fun SearchBar(
query: String,
onQueryChange: (String) -> Unit
) {
TextField(value = query, onValueChange = onQueryChange)
}
// Usage in parent
@Composable
fun HomeScreen(viewModel: HomeViewModel = hiltViewModel()) {
val searchQuery by viewModel.searchQuery.collectAsState()
SearchBar(
query = searchQuery,
onQueryChange = { viewModel.updateSearchQuery(it) }
)
}3. Side Effects Management
// LaunchedEffect - For coroutine-based side effects
LaunchedEffect(userId) {
viewModel.loadUser(userId)
}
// DisposableEffect - For cleanup
DisposableEffect(Unit) {
val listener = SomeListener()
registerListener(listener)
onDispose {
unregisterListener(listener)
}
}
// SideEffect - For non-suspend side effects
SideEffect {
analytics.logScreenView("HomeScreen")
}๐ Code Quality Checklist
ViewModel
- Uses
StateFlowfor UI state, notLiveData - Exposes immutable state (
StateFlow, notMutableStateFlow) - Uses
viewModelScopefor coroutines - Handles all error cases
- Uses
update {}for state modifications - No UI logic (View references, Context)
Repository
- Returns
Flow<Result<T>>orResult<T> - Implements caching strategy (remote + local)
- Maps DTOs to domain models
- Handles network errors gracefully
- Uses Kotlin coroutines, not callbacks
Composables
- Uses
collectAsStateWithLifecycle()for Flow collection - Keys are provided for
LazyColumn/LazyRowitems - State hoisting applied for reusability
- No side effects in composition (use
LaunchedEffect) - Previews provided with
@Preview
Dependency Injection
-
@HiltViewModelfor ViewModels -
@Singletonfor app-level dependencies - Interface binding with
@Binds - Proper scoping (
SingletonComponent,ActivityComponent)
๐ Development Workflow
Step 1: Define Feature Requirements
- Identify screens needed
- Define user actions and navigation
- List data requirements (API, database)
Step 2: Domain Layer First
- Create domain models (pure Kotlin data classes)
- Define repository interfaces
- Write use cases with business logic
Step 3: Data Layer
- Create API service with Retrofit
- Define DTOs and mappers
- Implement repository with error handling
- Set up Room database if needed
Step 4: Dependency Injection
- Create Hilt modules for network/database
- Bind repository implementations
- Verify dependency graph
Step 5: Presentation Layer
- Define UI state data class
- Create ViewModel with StateFlow
- Build Composable screens
- Set up navigation
Step 6: Testing
- Unit test ViewModels (verify state transitions)
- Unit test Use Cases (verify business logic)
- Test repositories with mock data sources
- Compose UI tests for critical flows
๐งช Testing Patterns
ViewModel Testing
@Test
fun `loadUsers should update state with success`() = runTest {
// Given
val mockUsers = listOf(User(1, "Test"))
coEvery { useCase() } returns flowOf(Result.success(mockUsers))
val viewModel = HomeViewModel(useCase)
// When
viewModel.loadUsers()
// Then
val state = viewModel.uiState.value
assertEquals(false, state.isLoading)
assertEquals(mockUsers, state.users)
assertNull(state.error)
}Compose UI Testing
@Test
fun homeScreen_displaysUserList() {
composeTestRule.setContent {
HomeScreen()
}
composeTestRule
.onNodeWithText("User Name")
.assertIsDisplayed()
}๐ Dependencies Template
// Compose BOM
val composeBom = platform("androidx.compose:compose-bom:2024.01.00")
implementation(composeBom)
// Core Compose
implementation("androidx.compose.ui:ui")
implementation("androidx.compose.material3:material3")
implementation("androidx.activity:activity-compose:1.8.2")
// ViewModel + Lifecycle
implementation("androidx.lifecycle:lifecycle-viewmodel-compose:2.7.0")
implementation("androidx.lifecycle:lifecycle-runtime-compose:2.7.0")
// Navigation
implementation("androidx.navigation:navigation-compose:2.7.6")
// Hilt
implementation("com.google.dagger:hilt-android:2.48")
kapt("com.google.dagger:hilt-compiler:2.48")
implementation("androidx.hilt:hilt-navigation-compose:1.1.0")
// Coroutines
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3")
// Retrofit
implementation("com.squareup.retrofit2:retrofit:2.9.0")
implementation("com.squareup.retrofit2:converter-gson:2.9.0")
// Room
val roomVersion = "2.6.1"
implementation("androidx.room:room-runtime:$roomVersion")
implementation("androidx.room:room-ktx:$roomVersion")
kapt("androidx.room:room-compiler:$roomVersion")
// Testing
testImplementation("junit:junit:4.13.2")
testImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:1.7.3")
testImplementation("io.mockk:mockk:1.13.8")
androidTestImplementation("androidx.compose.ui:ui-test-junit4")โ ๏ธ Common Pitfalls to Avoid
โ DON'T
// Don't expose MutableStateFlow
val uiState: MutableStateFlow<UiState> = MutableStateFlow(UiState())
// Don't use LiveData in new projects
val data: LiveData<List<Item>> = liveData { }
// Don't perform heavy work during composition
@Composable
fun MyScreen(items: List<Item>) {
val sorted = items.sortedBy { it.name } // RECOMPUTES EVERY TIME!
}
// Don't use viewModelScope without error handling
viewModelScope.launch {
val result = repository.getData() // Can crash on exception!
}
// Don't mix concerns - ViewModels shouldn't know about Views
class MyViewModel : ViewModel() {
fun updateUI(context: Context) { /* NO! */ }
}โ DO
// Expose immutable StateFlow
private val _uiState = MutableStateFlow(UiState())
val uiState: StateFlow<UiState> = _uiState.asStateFlow()
// Use StateFlow with Kotlin Flow
val data: Flow<List<Item>> = repository.getItems()
// Use derivedStateOf for expensive calculations
val sorted by remember(items) {
derivedStateOf { items.sortedBy { it.name } }
}
// Handle errors gracefully
viewModelScope.launch {
try {
val result = repository.getData()
_uiState.update { it.copy(data = result) }
} catch (e: Exception) {
_uiState.update { it.copy(error = e.message) }
}
}
// Keep ViewModels pure - no Android dependencies
class MyViewModel @Inject constructor(
private val useCase: GetDataUseCase
) : ViewModel()๐ References
- Android Architecture Guide
- Jetpack Compose Documentation
- Material Design 3
- Hilt Dependency Injection
- Kotlin Coroutines
๐ License
MIT License - Free to use and modify for your projects.