Skip to content
AgileLogics
7 min read

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 in viewModelScope.
  • 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 ArticlesUiState makes impossible combinations (loading and showing an error) impossible, and the when is exhaustive.
  • collectAsStateWithLifecycle stops collecting while the app is in the background, saving battery.
  • The ViewModel depends on the ArticleRepository interface, 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.