A production-grade movie discovery application showcasing Clean Architecture, MVI pattern, hybrid caching strategies, and comprehensive testing with CI/CD automation.
Built as a portfolio project to demonstrate modern Android development practices for mid-level to senior engineering roles.
- 🎥 Browse Movies — Discover Now Playing, Popular, Top Rated, and Upcoming releases
- 💨 Instant Loading — Home screen loads from cache instantly, updates silently in background
- 🔍 Smart Search — Debounced search (300ms delay) with infinite scroll pagination
- 📖 Movie Details — Cast, genres, runtime, ratings, release dates, and full synopsis
- ⭐ Offline Watchlist — Save and manage bookmarks with zero network dependency
- 🔄 Pull-to-Refresh — Manual content refresh with Material 3 components
- 🏗️ Clean Architecture — Strict layer separation with dependency inversion
- 🔄 MVI Pattern — Unidirectional data flow for predictable state management
- 💾 Hybrid Caching — NetworkBoundResource for Home, API-first for Detail/Search
- 🗄️ Room Database — Offline-capable watchlist with reactive Flow updates
- ♾️ Paging 3 — Memory-efficient infinite scroll with built-in load states
- ⚡ Flow Operators — Debounce, distinctUntilChanged, flatMapLatest for search optimization
- 🧪 50+ Tests — Comprehensive unit and instrumentation test coverage
- 🚀 CI/CD — Automated testing and signed release builds via GitHub Actions
| Home Screen | Search Results | Movie Detail | Watchlist |
|---|---|---|---|
![]() |
![]() |
![]() |
![]() |
| Browse cached home content | Search with debounce | Manage fully-offline watchlist |
|---|---|---|
![]() |
![]() |
![]() |
- Go to GitHub Actions
- Click latest successful workflow run (green ✅)
- Download
MovieWise-release-vXXartifact - Unzip and install
app-release.apk
Signed release build with ProGuard enabled. May require "Install from Unknown Sources".
See Getting Started section.
| Category | Technologies | Why This Choice |
|---|---|---|
| Language | Kotlin 2.2.1 | Coroutines, Flow, null safety |
| UI | Jetpack Compose + Material 3 | Declarative UI, modern design |
| Architecture | Clean Architecture + MVI | Testability, separation of concerns |
| Dependency Injection | Hilt (Dagger) | Less boilerplate than manual Dagger |
| Database | Room | Type-safe SQL, Flow integration |
| Networking | Retrofit + Kotlinx Serialization | Standard HTTP client, compile-time safety |
| Async | Kotlin Coroutines + Flow | Native async/reactive programming |
| Pagination | Paging 3 | Memory-efficient infinite scroll |
| Image Loading | Coil | Native Compose integration |
| Testing | JUnit, MockK, Turbine | Kotlin-first testing tools |
| CI/CD | GitHub Actions | Free for public repos |
MovieWise follows Clean Architecture with strict layer boundaries and dependency inversion.
┌──────────────────────────────────────────────────────────────┐
│ PRESENTATION LAYER │
│ ┌──────────────┐ ┌──────────────┐ ┌───────────────────┐ │
│ │ Compose │ │ ViewModels │ │ MVI Contracts │ │
│ │ Screens │◄─┤ (State Mgmt)├─►│ State/Event/Effect│ │
│ └──────────────┘ └──────────────┘ └───────────────────┘ │
├──────────────────────────────────────────────────────────────┤
│ DOMAIN LAYER │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────────┐ │
│ │ Use Cases │ │ Models │ │ Repository │ │
│ │ (Business) │ │ (Domain) │ │ Interfaces │ │
│ └──────────────┘ └──────────────┘ └──────────────────┘ │
├──────────────────────────────────────────────────────────────┤
│ DATA LAYER │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────────┐ │
│ │ Repositories │ │ TMDB API │ │ Room Database │ │
│ │ (Impl) │◄─┤ (Retrofit) ├─►│ (Cache+Watch) │ │
│ └──────────────┘ └──────────────┘ └──────────────────┘ │
└──────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────┐
│ MVI PATTERN │
├─────────────────────────────────────────────────────────────┤
│ │
│ ┌──────────────┐ │
│ ┌─────────────►│ UI/SCREEN │─────────────┐ │
│ │ │ (Compose) │ │ │
│ │ └──────────────┘ │ │
│ │ │ │
│ STATE EVENT │
│ (Immutable) (Intent) │
│ │ │ │
│ │ ┌──────────────┐ │ │
│ │ │ │ │ │
│ └──────────────│ VIEWMODEL │◄────────────┘ │
│ │ (Process) │ │
│ └──────┬───────┘ │
│ │ │
│ ┌──────▼───────┐ │
│ │ USE CASE │ │
│ │ (Domain) │ │
│ └──────┬───────┘ │
│ │ │
│ ┌──────▼───────┐ │
│ │ REPOSITORY │ │
│ │ (API + Room) │ │
│ └──────────────┘ │
│ │
└─────────────────────────────────────────────────────────────┘
| Screen | Strategy | Rationale | Network Required |
|---|---|---|---|
| Home | Cache-first (NetworkBoundResource) | Users revisit frequently; instant UX | ❌ No (shows cache) |
| Detail | API-first (no cache) | Details rarely change; always-fresh data | ✅ Yes |
| Search | API-only (Paging 3) | Dynamic results; caching queries = storage explosion | ✅ Yes |
| Watchlist | Fully offline (Room only) | User's data should be locally owned | ❌ Never |
Cache-first approach for optimal UX:
1. Query Room database → Emit cached data (instant UI)
2. Fetch from TMDB API in parallel
3. Save fresh data to Room on success
4. Room update triggers new emission → UI updates silently
5. On API failure: Keep showing cache + display error Snackbar
Result: Users never see blank loading screens after first launch.MovieWise uses Room with two main entities.
┌─────────────────┐ ┌─────────────────┐
│ MovieEntity │ │ WatchlistEntity │
│ │ │ │
│ • id │ │ • id │
│ • title │ │ • title │
│ • overview │ │ • overview │
│ • posterPath │ │ • posterPath │
│ • voteAverage │ │ • voteAverage │
│ • releaseDate │ │ • releaseDate │
│ • category │ │ • addedAt │
│ • cachedAt │ │ │
└─────────────────┘ └─────────────────┘
│ │
│ │
Caches home User bookmarks
screen movies (fully offline)
| Feature | Implementation | Purpose |
|---|---|---|
| Caching | MovieEntity with category field |
Instant home screen loading |
| Offline Watchlist | WatchlistEntity separate table |
Zero network dependency |
| Flow Updates | Room @Query returns Flow<List<T>> |
Automatic UI updates on data changes |
| Cache Invalidation | Delete old + insert fresh in transaction | Atomic cache updates |
╱╲
╱ ╲ 8 UI Tests
╱────╲ (Navigation integration)
╱ ╲
╱ ╲ 45+ Unit Tests
╱──────────╲ (Business logic)
╱ ╲
╱──────────────╲
| Layer | What's Tested |
|---|---|
| ViewModels | State transitions, event handling, effects |
| Repository | NetworkBoundResource, caching, API failures |
| Mappers | DTO/Entity/Domain transformations |
| UseCases | Repository delegation, parameter passing |
| Utilities | Error message mapping, HTTP codes |
| Navigation | Bottom nav, screen transitions, back stack |
Every push triggers:
- ✅ 45+ automated tests on Ubuntu
- ✅ Android Lint checks
- ✅ Signed release APK build
- ✅ Test reports on failure
- Android Studio Hedgehog (2023.1.1) or newer
- JDK 17
- Android SDK 26+
- TMDB API key (Get free key)
- Clone the repository
git clone https://github.com/UsmanAnsari/MovieWise.git
cd MovieWise-
Get TMDB API Key
- Sign up at themoviedb.org
- Settings → API → Request API Key → Developer
- Copy API Key (v3 auth) (not Read Access Token)
-
Create
local.properties
In project root:
TMDB_API_KEY=your_api_key_hereFile is git-ignored for security
- Build and Run
./gradlew installDebug
# Or click Run ▶️ in Android StudioAPI Key Error:
- Verify
local.propertiesin project root (notapp/) - File → Sync Project with Gradle Files
Build Errors:
./gradlew clean buildapp/src/main/java/com/usman/moviewise/
│
├── 📂 data/ # Data Layer
│ ├── local/
│ │ ├── dao/ # Room DAOs
│ │ ├── entity/ # Room Entities
│ │ └── MovieDatabase.kt # Room Database
│ ├── remote/
│ │ ├── api/ # Retrofit API interfaces
│ │ └── dto/ # Network DTOs
│ ├── repository/ # Repository implementations
│ ├── mappers/ # Data transformations
│ └── util/ # NetworkBoundResource
│
├── 📂 domain/ # Domain Layer
│ ├── model/ # Domain models
│ ├── repository/ # Repository interfaces
│ └── usecase/ # Business logic
│ ├── GetPopularMoviesUseCase.kt
│ ├── GetMovieDetailUseCase.kt
│ ├── SearchMoviesUseCase.kt
│ └── ...
│
├── 📂 ui/ # Presentation/UI Layer
│ ├── home/ # Home screen (MVI)
│ │ ├── HomeContract.kt # State/Event/Effect
│ │ ├── HomeViewModel.kt # State management
│ │ └── HomeScreen.kt # Compose UI
│ ├── detail/ # Detail screen
│ ├── search/ # Search screen
│ ├── watchlist/ # Watchlist screen
│ ├── navigation/ # Nav graph & bottom nav
│ └── components/ # Shared UI components
│
├── 📂 di/ # Dependency Injection
│ ├── NetworkModule.kt # Retrofit, OkHttp
│ ├── DatabaseModule.kt # Room, DAOs
│ └── RepositoryModule.kt # Repository bindings
│
└── 📂 util/ # Utilities
├── Constants.kt
└── Extensions.kt
// HomeContract.kt - Clean separation of concerns
data class State(
val isLoading: Boolean = true,
val popular: List = emptyList(),
val nowPlaying: List = emptyList(),
val error: String? = null
) : UiState
sealed interface Event : UiEvent {
data object Refresh : Event
data class MovieClicked(val movieId: Int) : Event
}
sealed interface Effect : UiEffect {
data class NavigateToDetail(val movieId: Int) : Effect
}inline fun networkBoundResource(
crossinline query: () -> Flow,
crossinline fetch: suspend () -> RequestType,
crossinline saveFetchResult: suspend (RequestType) -> Unit,
crossinline shouldFetch: (ResultType) -> Boolean = { true }
): Flow<Resource> = flow {
// 1. Emit cached data first (instant UI)
val data = query().first()
emit(Resource.Loading(data))
if (shouldFetch(data)) {
try {
// 2. Fetch from API
val apiResponse = fetch()
// 3. Save to database
saveFetchResult(apiResponse)
// 4. Query database again (single source of truth)
emitAll(query().map { Resource.Success(it) })
} catch (e: Exception) {
// 5. On error, keep showing cached data
emit(Resource.Error(e.toUserFriendlyMessage(), data))
}
} else {
// Cache is fresh enough
emit(Resource.Success(data))
}
}// SearchViewModel.kt
init {
queryFlow
.debounce(300) // Wait 300ms after typing stops
.filter { it.length >= 2 } // Minimum 2 characters
.distinctUntilChanged() // Skip if same as previous
.flatMapLatest { query -> // Cancel old search when new query arrives
searchMovies(query)
.cachedIn(viewModelScope) // Preserve data across config changes
}
.collectLatest { pagingData ->
setState { copy(searchResults = pagingData) }
}
}MovieWise implements 9 Use Cases following Single Responsibility Principle:
| Use Case | Responsibility | Returns |
|---|---|---|
GetPopularMoviesUseCase |
Fetch popular movies (cached) | Flow<Resource<List<Movie>>> |
GetNowPlayingMoviesUseCase |
Fetch now playing (cached) | Flow<Resource<List<Movie>>> |
GetTopRatedMoviesUseCase |
Fetch top rated (cached) | Flow<Resource<List<Movie>>> |
GetUpcomingMoviesUseCase |
Fetch upcoming (cached) | Flow<Resource<List<Movie>>> |
GetMovieDetailUseCase |
Fetch movie details (API-only) | Flow<Resource<MovieDetail>> |
SearchMoviesUseCase |
Search with Paging 3 | Flow<PagingData<Movie>> |
GetWatchlistUseCase |
Fetch watchlist (Room) | Flow<List<Movie>> |
AddToWatchlistUseCase |
Add movie to watchlist | suspend fun |
RemoveFromWatchlistUseCase |
Remove from watchlist | suspend fun |
IsMovieInWatchlistUseCase |
Check watchlist status | Flow<Boolean> |
Architecture & Design Patterns
Clean Architecture isn't just folders — It's about dependency rules. The domain layer has zero Android dependencies, making business logic completely testable.
MVI eliminates state bugs — Unidirectional flow means state can only be modified in one place (ViewModel). No more "who changed this state?" debugging sessions.
NetworkBoundResource is production-critical — Users on slow networks see cached content instantly. If API fails, they still have a working app. This pattern is essential for mobile.
Testing Strategy
Test the pyramid, not the ice cream cone — 45+ unit tests (fast, stable) + 8 UI tests (slow, integration-only).
Turbine makes Flow testing elegant — test { awaitItem() } beats runBlocking { delay(100) } every time.
MockK's coEvery/coVerify — Perfect for suspend functions. Mockito requires workarounds.
Performance & UX
Debounce saved my API quota — Typing "inception" without debounce = 9 API calls. With 300ms debounce = 1 call.
Room Flow is magical — Add movie to watchlist in DetailScreen → WatchlistScreen updates automatically. Zero manual refresh code.
flatMapLatest cancels old searches — User types fast, old search requests are cancelled automatically. No race conditions.
CI/CD & DevOps
GitHub Actions caught bugs I missed locally — Lint violations, test failures from merge conflicts, build issues from dependency updates.
Signed APK automation — Base64-encode keystore → Store in GitHub Secrets → Decode in workflow → Sign APK. Took time to set up, saves hours long-term.
Branch name sanitization — feature/search → feature-search for artifact names. Small detail, avoids workflow failures.
| Feature | Status | Notes |
|---|---|---|
| Home Caching | ✅ Implemented | NetworkBoundResource pattern |
| Watchlist | ✅ Fully Offline | Room database with Flow |
| Search | ✅ API-only | Paging 3 with debounce |
| Detail | ✅ API-first | No caching (deliberate choice) |
| Testing | ✅ 50+ tests | Unit + instrumentation |
| CI/CD | ✅ Automated | GitHub Actions with signed APKs |
In a production app with millions of users, I would add:
| Enhancement | Why | Complexity |
|---|---|---|
| Detail Caching | Offline detail viewing | Medium (RemoteMediator) |
| Cloud Sync | Cross-device watchlist | High (backend + auth) |
| Analytics | Understand user behavior | Low (Firebase) |
| Crashlytics | Production error tracking | Low (Firebase) |
| Accessibility | TalkBack optimization | Medium (custom semantics) |
| Localization | Multi-language support | Medium (TMDB supports 30+) |
Why these aren't included: This is a portfolio project optimized for demonstrating Clean Architecture, MVI, and testing practices — the core skills employers evaluate. Adding every production feature would dilute focus and increase maintenance burden.
- Phase 1: Foundation (Clean Architecture + MVI)
- Phase 2: Core Features (Browse, Search, Detail, Watchlist)
- Phase 3: Comprehensive Testing (50+ tests)
- Phase 4: CI/CD Pipeline (GitHub Actions)
- Phase 5: Detail Screen Caching (RemoteMediator)
- Phase 6: Firebase Integration (Auth + Cloud Sync)
- Phase 7: Analytics & Crashlytics
Usman Ali Ansari
- 📧 Email: usman10ansari@gmail.com
- 💼 LinkedIn: usman1ansari
- 🐙 GitHub: @UsmanAnsari
- TMDB — Free movie API
- Material Design 3 — Design system
- Android Community — Excellent open-source libraries







