Architecture layers
Layered architecture for Compose apps
UI Layer
Composable screens, stateless components, ViewModels holding immutable UI state, and adaptive scaffolds.
Domain Layer
Reusable use cases encapsulating complex business logic and state operations.
Data Layer
Repositories acting as the single source of truth, backed by local (DataStore, Room) or remote (Ktor, Retrofit) data sources.
Core Design System
Centralized MaterialTheme, color palettes, typography scales, shape tokens, and spring motion definitions.
State management
Unidirectional Data Flow (UDF) with StateFlow
Model UI state as immutable data classes and expose them via StateFlow:
data class ProfileUiState(
val isLoading: Boolean = false,
val user: User? = null,
val errorMessage: String? = null
)
class ProfileViewModel(private val repository: UserRepository) : ViewModel() {
private val _uiState = MutableStateFlow(ProfileUiState(isLoading = true))
val uiState: StateFlow<ProfileUiState> = _uiState.asStateFlow()
fun loadProfile() {
viewModelScope.launch {
_uiState.update { it.copy(isLoading = true) }
try {
val user = repository.getUser()
_uiState.update { it.copy(isLoading = false, user = user) }
} catch (e: Exception) {
_uiState.update { it.copy(isLoading = false, errorMessage = e.message) }
}
}
}
}
Consume state in composables with collectAsStateWithLifecycle() for lifecycle awareness.
Theming
MaterialTheme wrapper with dynamic color
@Composable
fun AppTheme(
darkTheme: Boolean = isSystemInDarkTheme(),
dynamicColor: Boolean = true,
content: @Composable () -> Unit
) {
val colorScheme = when {
dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> {
val context = LocalContext.current
if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context)
}
darkTheme -> DarkColorScheme
else -> LightColorScheme
}
MaterialTheme(
colorScheme = colorScheme,
typography = AppTypography,
shapes = AppShapes,
content = content
)
}
Component design
State hoisting and reusability
- Separate Stateful & Stateless: Keep reusable UI components stateless (receiving data and emitting event lambdas).
- Modifier Conventions: Accept a
modifier: Modifier = Modifieras the first optional parameter. - No Controller Leaks: Never pass
NavControllerorViewModelinstances down to child composables.
Accessibility & UX
Inclusive design standards
- Touch Targets: Ensure a minimum interactive touch target of 48x48 dp.
- Semantics: Provide
contentDescriptionfor icon buttons and images; merge compound item semantics. - Font Scaling: Design layouts that support up to 200% system font enlargement without clipping.
- Edge-to-Edge: Enable
enableEdgeToEdge()and handle window insets gracefully.
Testing & Quality
Automated testing and linting
.\gradlew.bat testDebugUnitTest
.\gradlew.bat lintDebug
.\gradlew.bat :app:assembleDebug