A hands-on learning project that teaches modern Android development by building a News Reader app from scratch in 10 incremental steps. Each step introduces one concept, keeps the app runnable, and is backed by detailed documentation.
By the end you'll have a fully working app with Jetpack Compose UI, ViewModel + StateFlow, Hilt DI, Retrofit networking, Room caching, Navigation, pull-to-refresh, and unit tests — all wired together following the recommended Android architecture.
- Android developers who want to learn Jetpack Compose and modern architecture (MVVM, Repository pattern, single source of truth).
- Developers moving from XML-based Android to Compose.
- Anyone who prefers learning by building a real app rather than reading docs in isolation.
| Layer | Library | Purpose |
|---|---|---|
| UI | Jetpack Compose, Material 3 | Declarative UI, theming, pull-to-refresh |
| State | ViewModel + StateFlow | Lifecycle-aware, reactive state management |
| DI | Hilt | Compile-time dependency injection |
| Network | Retrofit + OkHttp | REST API calls to NewsAPI.org |
| Local DB | Room | SQLite caching with Flow-based queries |
| Navigation | Navigation Compose + Hilt Navigation | Type-safe routes, argument passing |
| Async | Kotlin Coroutines + Flow | Structured concurrency |
| Testing | JUnit 4 + kotlinx-coroutines-test | ViewModel unit tests with fake repository |
- Android Studio (latest stable — Ladybug or newer recommended)
- Kotlin 2.0+ (this project uses 2.0.21)
- JDK 11+
- A free API key from NewsAPI.org (sign up takes 30 seconds)
git clone https://github.com/jackhoang2411/PracticeArchitecture.git
cd PracticeArchitectureCreate (or edit) local.properties in the project root and add:
NEWS_API_KEY=your_api_key_here
local.propertiesis gitignored — your key stays private.
Open the project folder in Android Studio. Gradle will sync automatically.
Select a device or emulator and hit Run (or ./gradlew installDebug).
./gradlew :app:testDebugUnitTestapp/src/main/java/com/example/learningarchitect/
├── domain/
│ ├── model/ Article (domain model)
│ └── repository/ NewsRepository (interface)
├── data/
│ ├── remote/
│ │ ├── dto/ ArticleDto, NewsApiResponse, SourceDto
│ │ ├── mapper/ ArticleDto → Article mapping
│ │ └── NewsApiService.kt (Retrofit interface)
│ ├── local/
│ │ ├── entity/ ArticleEntity (Room)
│ │ ├── dao/ ArticleDao
│ │ ├── mapper/ Entity ↔ Domain mapping
│ │ └── AppDatabase.kt
│ └── repository/
│ ├── DefaultNewsRepository.kt (real: API + Room)
│ └── FakeNewsRepository.kt (sample data)
├── di/
│ ├── AppModule.kt (binds NewsRepository)
│ ├── NetworkModule.kt (Retrofit, OkHttp)
│ ├── DatabaseModule.kt (Room)
│ └── Qualifiers.kt (@NewsApiKey)
├── ui/
│ ├── screens/
│ │ ├── NewsScreen.kt / NewsViewModel.kt / NewsUiState.kt
│ │ └── ArticleDetailScreen.kt / ArticleDetailViewModel.kt
│ ├── navigation/
│ │ ├── Routes.kt
│ │ └── NewsNavGraph.kt
│ └── theme/ Color, Type, Theme
├── MainActivity.kt
└── LearningArchitectApplication.kt (@HiltAndroidApp)
app/src/test/ (unit tests)
├── data/repository/ FakeNewsRepositoryForTest
└── ui/screens/ NewsViewModelTest
docs/
├── STEP_1.md … STEP_10.md (detailed write-ups for each step)
Each step builds on the previous one. Every step has a detailed guide in docs/STEP_<N>.md that explains what you're building, why, and the key concepts.
| Step | What you learn | Key files | Guide |
|---|---|---|---|
| 1 | Compose basics — data model, one screen, hardcoded list | Article.kt, NewsScreen.kt, MainActivity.kt |
STEP_1 |
| 2 | ViewModel + StateFlow — Loading / Success / Error states | NewsViewModel.kt, NewsUiState.kt |
STEP_2 |
| 3 | Hilt — Application class, modules, constructor injection | LearningArchitectApplication.kt, AppModule.kt |
STEP_3 |
| 4 | Retrofit — API service, DTOs, network module | NewsApiService.kt, NetworkModule.kt, DTOs |
STEP_4 |
| 5 | Repository pattern — wire API into ViewModel via interface | DefaultNewsRepository.kt, ArticleMapper.kt |
STEP_5 |
| 6 | Room — entity, DAO, database, cache API responses | ArticleEntity.kt, ArticleDao.kt, AppDatabase.kt |
STEP_6 |
| 7 | Cache-first strategy — observe Room Flow, refresh from API | getArticlesFlow(), refresh() in repository |
STEP_7 |
| 8 | Navigation Compose — detail screen, route arguments, hiltViewModel | Routes.kt, NewsNavGraph.kt, ArticleDetailScreen.kt |
STEP_8 |
| 9 | Polish — pull-to-refresh, empty state, error UX | PullToRefreshBox, isRefreshing in ViewModel |
STEP_9 |
| 10 | Testing — unit test ViewModel with fake repository | FakeNewsRepositoryForTest.kt, NewsViewModelTest.kt |
STEP_10 |
Check out the git tag for each step and read the matching guide:
git checkout step-1-compose-basics # start here
# read docs/STEP_1.md, explore the code, run the app
git checkout step-2-viewmodel-state # next step
# read docs/STEP_2.md ...At each step, the app compiles and runs. You can see exactly what changed by diffing tags:
git diff step-1-compose-basics..step-2-viewmodel-stateStay on main and browse the full codebase. Use docs/STEP_1.md through docs/STEP_10.md as a guided tour — each one explains the concepts and points to the relevant files.
Looking up how to set up Hilt? Check docs/STEP_3.md and di/. Need a Room caching example? See docs/STEP_6.md and data/local/. Each step is self-contained enough to be a reference for that specific topic.
┌─────────────────────────────────────────────────┐
│ UI Layer │
│ NewsScreen ← NewsViewModel ← NewsUiState │
│ ArticleDetailScreen ← ArticleDetailViewModel │
│ NewsNavGraph (navigation) │
└──────────────────────┬──────────────────────────┘
│ collects Flow / calls refresh()
┌──────────────────────▼──────────────────────────┐
│ Domain Layer │
│ NewsRepository (interface) │
│ Article (domain model) │
└──────────────────────┬──────────────────────────┘
│ implemented by
┌──────────────────────▼──────────────────────────┐
│ Data Layer │
│ DefaultNewsRepository │
│ ├── NewsApiService (Retrofit) ── remote/ │
│ └── ArticleDao (Room) ──────── local/ │
│ │
│ Cache-first: observe Room Flow, refresh via API │
└─────────────────────────────────────────────────┘
All steps are tagged so you can jump to any point:
step-1-compose-basics
step-2-viewmodel-state
step-3-hilt-setup
step-4-retrofit
step-5-repository
step-6-room-cache
step-7-cache-strategy
step-8-navigation
step-9-polish
step-10-testing
| Problem | Solution |
|---|---|
NEWS_API_KEY is empty at runtime |
Make sure local.properties has NEWS_API_KEY=your_key (no quotes). Rebuild the project. |
| "Cannot create an instance of ViewModel" | Use hiltViewModel() (from androidx.hilt:hilt-navigation-compose) instead of viewModel(). See STEP_8. |
UncompletedCoroutinesError in tests |
Don't pass the same dispatcher to both setMain() and runTest(). See STEP_10. |
| Room schema changed, app crashes | Uninstall the app (or bump the database version with a migration). |
| Compose preview not rendering | Make sure preview composables don't depend on Hilt-injected ViewModels. Use hardcoded sample data. |
The 10-step roadmap is complete. Ideas to keep going:
- Compose UI tests — use
composeTestRuleto test screen behavior. - Repository integration tests — use an in-memory Room database.
- Paging — load articles page by page with Paging 3.
- Image loading — add Coil or Glide for article thumbnails.
- Offline-first — show cached data immediately, sync in background.
- Dark theme — the Material 3 theme already supports it; add a toggle.
This is a personal learning project. Feel free to use it as a reference or starting point for your own learning.