Scalable News App Architecture on Android
Scalable News App Architecture on Android
Android applications that go beyond simple prototypes require a clear separation of concerns, predictable data flow, and a layered structure that scales with feature growth. Google's official architecture guidance codifies these ideas into a layered system built around the UI Layer and the Data Layer, with an optional Domain Layer for complex business logic. Understanding how these layers interact, how data flows through them, and how each piece remains independently testable is one of the most common topics in senior Android interviews. By the end of this lesson, you will be able to:
- Explain the responsibilities of the UI, Data, and optional Domain layers in an Android application.
- Describe the Repository pattern and the Single Source of Truth principle for offline first data management.
- Trace Unidirectional Data Flow from a user action through the ViewModel to the data layer and back to the UI.
- Identify when to introduce a Domain Layer with UseCase classes.
- Apply lifecycle aware state collection, dependency injection, and a multi layered testing strategy.
Layered Architecture and Separation of Concerns
Google's recommended architecture divides an Android application into two mandatory layers and one optional layer. The UI Layer contains Composables (or Views) and ViewModels. Composables render state and capture user input. The ViewModel acts as a state holder: it receives events from the UI, delegates work to lower layers, and exposes a single StateFlow<UiState> that the UI observes. The ViewModel never references Context or any Android lifecycle type directly. If a transient UI effect like a Toast is needed, the ViewModel models it as part of the UI state and lets the Composable perform the side effect.
The Data Layer manages all data operations and contains the application's business logic related to data sourcing. Its central abstraction is the Repository. A NewsRepository provides a unified API such as getArticlesStream(): Flow<List<Article>> and suspend fun toggleBookmark(articleId: String). Internally it coordinates between a remote data source (a Retrofit service) and a local data source (a Room DAO). The ViewModel never touches the Retrofit service or the Room DAO directly. This boundary means swapping Retrofit for Ktor or Room for SQLDelight requires changes only inside the Repository and its data source classes.
The Domain Layer is optional and contains UseCase (or Interactor) classes. A UseCase encapsulates a single piece of business logic that combines data from multiple repositories or applies complex transformation rules. For a news app, a GetPersonalizedFeedUseCase might combine NewsRepository and UserPreferencesRepository to filter, sort, and rank articles. The Domain Layer becomes worthwhile when the same logic is needed in multiple ViewModels or when a ViewModel's init block starts accumulating complex flow-combining logic that obscures its primary role as a state holder.
Unidirectional Data Flow and UI State Modeling
Unidirectional Data Flow (UDF) is the principle that state flows down from the ViewModel to the UI and events flow up from the UI to the ViewModel. Consider the bookmark action on a news feed screen:
- The user taps the bookmark icon. The Composable does not mutate any state. It calls a lambda like
onBookmarkClicked(articleId)that was passed down from the ViewModel. - The ViewModel receives the event and delegates to the Repository:
viewModelScope.launch { newsRepository.toggleBookmark(articleId) }. - The Repository updates the Room database. Because the ViewModel is already observing a
Flowfrom Room, the database change emits a new list of articles with the updated bookmark flag. - The ViewModel receives the new list, wraps it in a
Successstate, and updates itsStateFlow. Compose detects the state change and recomposes the affected item.
The UI state itself is modeled as a sealed interface to make every screen condition explicit and exhaustive:
sealed interface NewsFeedUiState {
data object Loading : NewsFeedUiState
data class Success(
val articles: List<Article>,
val userMessage: String? = null
) : NewsFeedUiState
data class Error(val message: String) : NewsFeedUiState
}
The ViewModel exposes a single StateFlow<NewsFeedUiState>. On the Compose side, the recommended collection API is collectAsStateWithLifecycle(), which automatically stops collection when the lifecycle drops below STARTED. The simpler collectAsState() continues collecting even when the app is backgrounded, wasting resources and potentially triggering unnecessary work in upstream flows.
This interview continues for subscribers
Subscribe to Dove Letter for full access to exclusive interviews about Android and Kotlin development.
Become a Sponsor