Android App Architecture
Prerequisites: Android with Jetpack Compose, StateFlow, SharedFlow, and Channels
Google's recommended Android architecture separates an app into layers with a clear, one-way flow of data:
UI (Compose) ──events──▶ ViewModel ──▶ Repository ──▶ data sources
▲ │ (Room, network)
└──────── UI state ───────┘
- UI layer — composables display state and send user events up.
- ViewModel — holds a screen's state as a
StateFlow, survives configuration changes such as rotation, and runs work inviewModelScope. - Data layer — repositories decide where data comes from (local database, network, cache) and expose it as suspend functions or flows.
This is often summarized as unidirectional data flow: state flows down, events flow up.
A feature, end to end
// Data layer
data class Article(val id: Int, val title: String)
interface ArticleRepository {
suspend fun latest(): List<Article>
}
// UI state: exactly one of these at a time
sealed interface ArticlesUiState {
data object Loading : ArticlesUiState
data class Loaded(val articles: List<Article>) : ArticlesUiState
data class Error(val message: String) : ArticlesUiState
}
// ViewModel
class ArticlesViewModel(private val repository: ArticleRepository) : ViewModel() {
private val _state = MutableStateFlow<ArticlesUiState>(ArticlesUiState.Loading)
val state: StateFlow<ArticlesUiState> = _state.asStateFlow()
init { refresh() }
fun refresh() {
viewModelScope.launch {
_state.value = ArticlesUiState.Loading
_state.value = try {
ArticlesUiState.Loaded(repository.latest())
} catch (e: IOException) {
ArticlesUiState.Error("Couldn't load articles.")
}
}
}
}
// UI
@Composable
fun ArticlesScreen(viewModel: ArticlesViewModel) {
val state by viewModel.state.collectAsStateWithLifecycle()
when (val s = state) {
ArticlesUiState.Loading -> CircularProgressIndicator()
is ArticlesUiState.Loaded -> LazyColumn {
items(s.articles) { Text(it.title) }
}
is ArticlesUiState.Error -> Text(s.message)
}
}
- The screen contains no networking and no decisions beyond showing the current state.
- The sealed
ArticlesUiStatemakes impossible combinations (loading and showing an error) impossible, and thewhenis exhaustive. collectAsStateWithLifecyclestops collecting while the app is in the background, saving battery.- The ViewModel depends on the
ArticleRepositoryinterface, so tests can pass a fake.
Supporting libraries
- Room — a SQLite database with compile-time-checked queries that can
return
Flows. - Retrofit or Ktor Client — networking.
- Hilt (or Koin) — dependency injection, which creates repositories and ViewModels and wires them together.
- Navigation Compose — moving between screens.
- WorkManager — reliable background work, such as syncing.
Keep it proportional
A small app doesn't need every layer on day one. Start with a ViewModel and a repository; add use cases, modules, and dependency injection as the app and team grow.