Modern Android Architecture in 2026: The Complete Picture

Android architecture has converged. After years of competing patterns — MVP, MVC, MVVM, MVI — the community has settled on a clear, well-supported approach. This article presents the full picture: what to use at each layer, and why.

The Three Layers

The official Android architecture guide defines three layers with clear responsibilities:

  • UI layer — displays state to the user and handles user events. ViewModel + Compose.
  • Domain layer (optional) — encapsulates business logic in use-cases. Especially valuable in larger apps.
  • Data layer — owns data sources (network, database, sensors) and exposes them as Flows via Repositories.

UI Layer: UDF with ViewModel

Unidirectional Data Flow: state flows down to the UI, events flow up to the ViewModel.

// State
data class FeedUiState(
    val articles: List<Article> = emptyList(),
    val isLoading: Boolean = false,
    val error: String? = null
)

// ViewModel
class FeedViewModel(private val getArticles: GetArticlesUseCase) : ViewModel() {
    val uiState: StateFlow<FeedUiState> = getArticles()
        .map { FeedUiState(articles = it) }
        .catch { e -> emit(FeedUiState(error = e.message)) }
        .stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), FeedUiState(isLoading = true))
}

// UI
@Composable
fun FeedScreen(viewModel: FeedViewModel = hiltViewModel()) {
    val state by viewModel.uiState.collectAsStateWithLifecycle()
    FeedContent(state)
}

Domain Layer: Use-Cases

Use-cases (or interactors) contain application-specific business logic. Each use-case does one thing.

class GetArticlesUseCase(private val repo: ArticleRepository) {
    operator fun invoke(): Flow<List<Article>> =
        repo.getArticles()
            .map { articles -> articles.filter { it.isPublished }.sortedByDescending { it.date } }
}

Data Layer: Repository Pattern

class ArticleRepository(
    private val remoteDs: ArticleRemoteDataSource,
    private val localDs: ArticleLocalDataSource
) {
    fun getArticles(): Flow<List<Article>> = localDs.getAll()

    suspend fun sync() {
        val remote = remoteDs.fetchAll()
        localDs.upsertAll(remote)
    }
}

Dependency Injection with Hilt

Hilt is the standard DI framework for Android. It generates the DI graph at compile time, providing compile-time safety and minimal runtime overhead.

@HiltViewModel
class FeedViewModel @Inject constructor(
    private val getArticles: GetArticlesUseCase
) : ViewModel()

@Module
@InstallIn(SingletonComponent::class)
object DataModule {
    @Provides @Singleton
    fun provideArticleRepository(
        remote: ArticleRemoteDataSource,
        local: ArticleLocalDataSource
    ): ArticleRepository = ArticleRepository(remote, local)
}

Modularization

For apps beyond a certain size, modularization pays dividends in build speed and team scalability. The recommended module structure in 2026:

  • :app — application entry point only. Minimal code.
  • :feature:feed, :feature:profile — one module per feature, owns UI and ViewModel.
  • :domain — use-cases and domain models. No Android dependencies.
  • :data:articles, :data:users — one module per data domain.
  • :core:ui, :core:network, :core:database — shared infrastructure.

What to Avoid

  • God ViewModels — a ViewModel that owns all state for a complex screen is hard to test and reason about. Split by responsibility.
  • Skipping the domain layer in complex apps — when ViewModels directly call repository methods with complex logic, the logic is untestable without Android.
  • Circular dependencies between modules — use a dependency graph linter or build time checks to enforce layer rules.