Chapter10: Architecture Patterns
Architecture Patterns! This is the chapter that turns your app from “working code” into professional, maintainable, testable code that can grow to thousands of lines without becoming a nightmare. In January 2026, the Android community has settled on a very clear recommended stack for most apps:
- MVVM (Model-View-ViewModel) as the primary UI pattern (especially with Jetpack Compose).
- Clean Architecture (or layered architecture) for separation of concerns.
- Repository pattern to abstract data sources.
- Hilt for dependency injection (latest stable is 2.59 released Jan 21, 2026 — fully supports AGP 9.0+ and KSP).
We’ll go deep like we’re refactoring your Tip Calculator / To-Do / Posts app together in Airoli — explaining why each pattern exists, common mistakes, and a full hands-on refactor using clean architecture + MVVM + Hilt + Repository.
1. MVVM (Model-View-ViewModel) – The UI Layer Pattern
Why MVVM in 2026?
- View (Compose UI) only displays and collects user events.
- ViewModel holds UI state & business logic, survives config changes.
- Model = data layer (repository, use cases).
- Unidirectional data flow: View → events → ViewModel → state → View.
- Testable: ViewModel has no Android context → easy unit tests.
Core components:
- View → Composable functions.
- ViewModel → @HiltViewModel class with StateFlow / LiveData for UI state.
- UiState → Sealed/data class holding loading/error/success states.
Example UiState:
|
0 1 2 3 4 5 6 7 8 9 10 |
data class PostsUiState( val isLoading: Boolean = false, val posts: List<Post> = emptyList(), val error: String? = null ) |
ViewModel:
|
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
@HiltViewModel class PostsViewModel @Inject constructor( private val getPostsUseCase: GetPostsUseCase ) : ViewModel() { private val _uiState = MutableStateFlow(PostsUiState(isLoading = true)) val uiState: StateFlow<PostsUiState> = _uiState.asStateFlow() init { loadPosts() } private fun loadPosts() { viewModelScope.launch { getPostsUseCase().collect { result -> _uiState.value = when (result) { is Result.Loading -> PostsUiState(isLoading = true) is Result.Success -> PostsUiState(posts = result.data) is Result.Error -> PostsUiState(error = result.message) } } } } } |
Composable observes: val state by viewModel.uiState.collectAsStateWithLifecycle()
2. Dependency Injection with Hilt Basics
Why Hilt?
- Reduces boilerplate vs manual DI.
- Provides @HiltViewModel, @Inject constructors.
- Scopes: @Singleton, @ActivityScoped, etc.
- Integrates with Navigation, WorkManager, etc.
Setup (latest 2026 style – using KSP instead of KAPT)
In root build.gradle.kts:
|
0 1 2 3 4 5 6 7 8 |
plugins { id("com.google.dagger.hilt.android") version "2.59" apply false } |
In app/build.gradle.kts:
|
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
plugins { id("com.google.dagger.hilt.android") } dependencies { implementation("com.google.dagger:hilt-android:2.59") ksp("com.google.dagger:hilt-compiler:2.59") // For Navigation Compose integration implementation("androidx.hilt:hilt-navigation-compose:1.2.0") } |
Application class:
|
0 1 2 3 4 5 6 7 |
@HiltAndroidApp class MyApplication : Application() |
In AndroidManifest.xml:
|
0 1 2 3 4 5 6 7 8 |
<application android:name=".MyApplication" ...> |
Now you can @Inject almost anything!
3. Repository Pattern + Clean Architecture Layers
Clean Architecture layers (from inner to outer):
- Domain (core business logic – pure Kotlin, no Android deps)
- Entities (data classes like Post)
- Use Cases / Interactors (e.g., GetPostsUseCase)
- Repositories (interfaces)
- Data (implementation of data sources)
- Remote (Retrofit)
- Local (Room/DataStore)
- Repository impl
- Presentation (UI + ViewModel)
- Composables
- ViewModels
Repository → Single source of truth, abstracts where data comes from (API, DB, cache).
Example:
|
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
// domain/repository/PostRepository.kt interface PostRepository { fun getPosts(): Flow<Result<List<Post>>> } // data/repository/PostRepositoryImpl.kt @Singleton class PostRepositoryImpl @Inject constructor( private val apiService: ApiService, private val postDao: PostDao // if Room caching ) : PostRepository { override fun getPosts(): Flow<Result<List<Post>>> = flow { emit(Result.Loading) try { val posts = apiService.getPosts() // Optional: cache to Room emit(Result.Success(posts)) } catch (e: Exception) { emit(Result.Error(e.message ?: "Network error")) } }.flowOn(Dispatchers.IO) } |
Use Case (domain layer):
|
0 1 2 3 4 5 6 7 8 9 10 |
class GetPostsUseCase @Inject constructor( private val repository: PostRepository ) { operator fun invoke(): Flow<Result<List<Post>>> = repository.getPosts() } |
4. Hands-on: Refactor Your Posts App with Clean Architecture
Let’s refactor the JSONPlaceholder Posts app from Chapter 9 into clean MVVM + Hilt + Repository.
Project Structure (recommended for 2026):
|
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
app ├── di // Hilt modules ├── domain │ ├── model │ ├── repository │ └── usecase ├── data │ ├── remote │ ├── local │ └── repository ├── presentation │ ├── screen │ └── viewmodel └── MyApplication.kt |
Step-by-step refactor:
- Create Hilt Module (di/AppModule.kt)
|
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
@Module @InstallIn(SingletonComponent::class) object AppModule { @Provides @Singleton fun provideRetrofit(): Retrofit = Retrofit.Builder() .baseUrl("https://jsonplaceholder.typicode.com/") .addConverterFactory(Json { ignoreUnknownKeys = true }.asConverterFactory("application/json".toMediaType())) .build() @Provides @Singleton fun provideApiService(retrofit: Retrofit): ApiService = retrofit.create(ApiService::class.java) } |
- Move to clean layers (create folders as above)
- domain/model/Post.kt (same data class)
- domain/repository/PostRepository.kt (interface)
- domain/usecase/GetPostsUseCase.kt (as above)
- data/repository/PostRepositoryImpl.kt (impl with Retrofit)
- presentation/viewmodel/PostsViewModel.kt (as MVVM example above, @HiltViewModel)
- presentation/screen/PostsScreen.kt (Composable with state collection)
- Update PostsScreen to use injected ViewModel:
|
0 1 2 3 4 5 6 7 8 9 10 11 12 |
@Composable fun PostsScreen( viewModel: PostsViewModel = hiltViewModel() ) { val state by viewModel.uiState.collectAsStateWithLifecycle() // ... same UI with loading/error/posts } |
- Add to NavHost (if multi-screen):
|
0 1 2 3 4 5 6 7 8 |
composable<Posts> { PostsScreen() } |
Benefits you get after refactor:
- ViewModel has no direct Retrofit – easy to mock for tests.
- Change API to Room/offline → only change RepositoryImpl.
- Add caching, error handling, mapping → in UseCase/Repo.
- Scalable: Add feature modules later.
Test tip: Write unit test for UseCase/ViewModel (no Android deps):
|
0 1 2 3 4 5 6 7 8 9 |
@Test fun `getPosts success`() = runTest { // Mock repository } |
You’ve now refactored to clean, modern Android architecture! This structure is what most serious 2026 apps (news, e-commerce, social) use.
Questions? Want to add Room caching to the repo? Feature module? Testing setup? Or refactor your Tip Calculator instead? Tell me — next chapter: Advanced topics (permissions, background, testing, publishing). You’re at pro level now — incredible journey! Keep it up! 🚀🏗️
