Android App Architecture Layers: A Complete Practical Guide for Beginners 2026
One of the most confusing parts of learning modern Android development is understanding how an app is supposed to be organized. You open a professional project on GitHub and see folders named data, domain, presentation — and if no one explained what those mean, it feels like reading a book in a foreign language.
This guide is that explanation. We’re going to walk through each of the Android app architecture layers, what each one actually contains, what its job is, and why it exists — using examples that feel like real Android apps, not textbook theory. By the end, understanding Android app architecture layers will feel natural rather than overwhelming.
Why Layers Exist at All
Think of any Android app you use daily. A banking app, a food delivery app, a music player. Each one has to do at least three things: get data from somewhere, apply some logic to that data, and show it to the user.
When those three responsibilities are mixed together in the same file — or even the same class — the app becomes fragile. One change breaks something unexpected. Adding a new feature requires understanding the entire codebase before touching anything.
Android app architecture layers are the solution. Separate the concerns, give each layer one clear responsibility, and changes become predictable and manageable. That’s the entire reason this structure exists.
Layer 1 — The Data Layer
What It Does
The Data layer is responsible for one thing: providing data. It doesn’t care how the UI looks. It doesn’t apply business rules. It just knows how to fetch, store, and return data when asked.
This is where you’ll find Retrofit service interfaces for API calls, Room database DAOs for local storage, SharedPreferences or DataStore helpers for simple key-value data, and Repository implementations. Among all Android app architecture layers, the Data layer is the one closest to your actual data sources.
The Repository Pattern
The Repository is the most important piece of the Data layer. It acts as a single source of truth for a specific type of data in your app.
Say you’re building a news app. Your NewsRepository is responsible for news articles. When the app needs articles, it asks the repository. The repository decides: are there fresh articles in the local database? Return those. Are they outdated? Fetch from the API, save them locally, then return them.
kotlin
class NewsRepositoryImpl(
private val newsApiService: NewsApiService,
private val newsDao: NewsDao
) : NewsRepository {
override suspend fun getLatestNews(): List<Article> {
val cached = newsDao.getCachedArticles()
return if (cached.isNotEmpty() && !isCacheExpired()) {
cached.map { it.toDomainModel() }
} else {
val remote = newsApiService.fetchLatestNews()
newsDao.insertAll(remote.map { it.toEntity() })
remote.map { it.toDomainModel() }
}
}
}
The layers above the Data layer never know about this cache-versus-network decision. They just ask for articles and receive them. That clean separation is what makes Android app architecture layers so powerful.
What Doesn’t Belong Here
Business logic doesn’t belong in the Data layer. If you find yourself writing “if the user is a premium subscriber, return the full article, otherwise return only the first paragraph” — that’s business logic. It belongs in the Domain layer, not here. Repositories should stay focused on data retrieval and storage decisions, nothing more.
Layer 2 — The Domain Layer
What It Does
The Domain layer is the brain of your application. It contains the rules that define how your app actually behaves — independently of any UI or data source. Among all Android app architecture layers, this one is the most important to keep clean and dependency-free.
This layer holds two main things: entities and Use Cases.
Entities are plain Kotlin data classes that represent the core objects in your app. A User, an Article, a Product. These classes have zero Android imports. They’re pure Kotlin, nothing else.
Use Cases are single-purpose classes that represent one specific operation. GetLatestNewsUseCase. SearchProductsUseCase. SubmitOrderUseCase. Each one does exactly one thing and nothing more.
kotlin
// Pure Kotlin entity — no Android imports
data class Article(
val id: String,
val title: String,
val content: String,
val publishedAt: Long,
val isPremium: Boolean
)
// Repository interface — defined here, implemented in Data layer
interface NewsRepository {
suspend fun getLatestNews(): List<Article>
}
// Use Case — one job only
class GetLatestNewsUseCase(
private val newsRepository: NewsRepository
) {
suspend operator fun invoke(): List<Article> {
return newsRepository.getLatestNews()
}
}
Why Use Cases Matter
Use Cases prevent ViewModels from becoming bloated. Without them, your ViewModel ends up being the place where all logic lives — creating exactly the same problem we were trying to solve.
With Use Cases, your ViewModel simply calls the appropriate Use Case and observes the result. A SearchProductsUseCase can be used from a search screen ViewModel and a voice search ViewModel. Write it once, use it anywhere across your Android app architecture layers.
The Most Important Rule of This Layer
The Domain layer must have zero dependencies on Android framework classes or on the Data layer’s implementations. It can define interfaces — like NewsRepository — but the actual implementation lives in the Data layer.
This is what makes the Domain layer independently testable. Pure Kotlin, pure JVM, pure unit tests. No emulators, no mocking of Android systems. The Android recommended app architecture page describes this separation in detail.
Layer 3 — The UI Layer
What It Does
The UI layer is what users actually see. In 2025, for most new Android projects, this means Jetpack Compose screens and Jetpack ViewModels working together.
The UI layer — the outermost of all Android app architecture layers — has two responsibilities: displaying the current app state to the user, and forwarding user actions to the ViewModel. That’s it. No business logic. No direct API calls. Just observe state and react to it.
The ViewModel’s Role
The ViewModel sits between the UI layer’s visual components and the Domain layer’s Use Cases. It holds and manages UI state in a lifecycle-aware way — meaning it survives screen rotation and other configuration changes.
kotlin
@HiltViewModel
class NewsViewModel @Inject constructor(
private val getLatestNewsUseCase: GetLatestNewsUseCase
) : ViewModel() {
private val _uiState = MutableStateFlow<NewsUiState>(NewsUiState.Loading)
val uiState: StateFlow<NewsUiState> = _uiState.asStateFlow()
init {
loadNews()
}
private fun loadNews() {
viewModelScope.launch {
try {
val articles = getLatestNewsUseCase()
_uiState.value = NewsUiState.Success(articles)
} catch (e: Exception) {
_uiState.value = NewsUiState.Error(e.message ?: "Unknown error")
}
}
}
}
When a user taps “Refresh,” the Composable tells the ViewModel. The ViewModel calls GetLatestNewsUseCase. The Use Case calls the repository. The repository fetches from the API. The result flows back up. The ViewModel updates its state. The Composable re-renders. Each of the Android app architecture layers did exactly its job — nothing more.
UI State as a Single Object
Modern practice — especially with Compose — is to represent the entire screen’s state as a single sealed class. A NewsUiState might have Loading, Success, and Error variants.
kotlin
sealed class NewsUiState {
object Loading : NewsUiState()
data class Success(val articles: List<Article>) : NewsUiState()
data class Error(val message: String) : NewsUiState()
}
@Composable
fun NewsScreen(viewModel: NewsViewModel = hiltViewModel()) {
val uiState by viewModel.uiState.collectAsState()
when (uiState) {
is NewsUiState.Loading -> CircularProgressIndicator()
is NewsUiState.Success -> NewsList(articles = (uiState as NewsUiState.Success).articles)
is NewsUiState.Error -> ErrorMessage(message = (uiState as NewsUiState.Error).message)
}
}
No checking multiple boolean flags, no conflicting conditions. One state object, one render. Clean and predictable across all Android app architecture layers.
How the Three Android App Architecture Layers Talk to Each Other
There’s a specific direction to the communication between layers, and it matters enormously.
The UI layer calls into the Domain layer through ViewModels calling Use Cases. The Domain layer calls into the Data layer through Repository interfaces. But the reverse never happens.
The Data layer doesn’t call Use Cases. The Domain layer doesn’t update the UI directly. Information flows in one direction — and results flow back through callbacks, flows, or suspend functions. This one-directional flow is what makes the entire Android app architecture layers system predictable and debuggable.
For deeper reading on how these layers connect in production apps, the official Android architecture samples on GitHub show real, working implementations of all three layers together.
If you’re also working on dependency injection to wire your layers together, our Hilt dependency injection Android guide explains how Hilt connects each of the Android app architecture layers at runtime. And for teams adopting Clean Architecture on top of this structure, our Clean Architecture Android beginners guide goes deeper into the dependency rule and layer boundaries.
Tracing a Real Feature Through All Three Layers
Let’s trace the “Load User Profile” feature through all three Android app architecture layers so the flow becomes completely concrete.
The Data layer — UserRepositoryImpl fetches user data from a REST API using Retrofit. It maps the API response model to the Domain entity User. It also caches the result in a Room database for offline support.
The Domain layer — GetUserProfileUseCase holds a reference to the UserRepository interface. It calls repository.getUser(userId) and returns the User entity. It might also apply a rule — like checking if the profile is complete and flagging it if not.
The UI layer — ProfileViewModel calls GetUserProfileUseCase inside a coroutine. It collects the result and updates profileUiState. ProfileScreen Composable observes profileUiState and renders the user’s name, photo, and details — or a loading spinner, or an error message.
Three layers, each doing exactly their part. No overlap, no confusion.
Mistakes That Break Android App Architecture Layers
Importing Retrofit models into your ViewModel. Your ViewModel should only know about Domain entities, not API response objects. Map API responses to Domain entities inside the repository — never let them leak upward through your Android app architecture layers.
Writing database queries inside Use Cases. Use Cases should call Repository methods, not database operations directly. The moment a Use Case imports a DAO, the Domain layer has acquired a Data layer dependency and the separation collapses.
Calling Use Cases from Composables directly. The Composable talks to the ViewModel. The ViewModel talks to Use Cases. Skipping that chain breaks the separation between Android app architecture layers and makes testing significantly harder.
Final Conclusion
The three Android app architecture layers — Data, Domain, and UI — aren’t complexity for its own sake. They’re a practical way of organizing Android apps so that each part of the code has one clear job and can be changed or tested without breaking everything else.
The Data layer provides data. The Domain layer applies logic. The UI layer shows the result. Keep the communication flowing in one direction, keep dependencies pointing inward toward the Domain layer, and your Android codebase will be genuinely maintainable — not just theoretically clean on paper.
Once these Android app architecture layers truly click, reading professional Android projects stops feeling like guesswork and starts feeling like reading a familiar, well-organized map. Every folder makes sense. Every class has an obvious place. And every change you make stays exactly where it belongs.



Post Comment