StreamFinder is a single-module Android app, built with Jetpack Compose and Material 3, that browses popular movies and TV shows via the TMDB (The Movie Database) API. It supports searching, viewing details with trailers, and maintaining an offline list of favorites backed by a local database.
- Home — browses popular movies and popular TV shows in horizontally scrollable rows.
- Search — full-text search across movies and TV shows (TMDB
/search/multi), debounced to avoid firing a request on every keystroke. - Detail — backdrop image, title, rating, release year, runtime/season count, genres, overview, trailer playback (opens YouTube), and share action.
- Favorites — persisted locally with Room, toggle from the detail screen, view as a grid, survives app restarts without any network calls.
- Navigation — Search and Favorites open as modal dialogs over Home, Detail is a full destination reachable from Home, Search, or Favorites.
- Theming — Material 3 with dynamic color on Android 12+, a static purple/pink palette on older versions, and RTL layout support.
MVVM + Clean Architecture, split into three layers:
ui/— Compose screens,ViewModels, and UI state. Each screen exposes state viaStateFlow<UiState<T>>and reacts to it withwhenon a sealedUiState(Loading/Success/Error).domain/— Plain Kotlin models (MediaItem,MediaDetail,MediaType) and repository interfaces. No Android or networking dependencies.data/— Repository implementations that mediate between the remote API (Retrofit) and local storage (Room), mapping DTOs to domain models.
com.lev.streamfinder
├── data
│ ├── local
│ │ ├── AppDatabase.kt
│ │ ├── FavoritesDao.kt
│ │ └── FavoriteEntity.kt
│ ├── remote
│ │ ├── TmdbApiService.kt
│ │ ├── AuthInterceptor.kt
│ │ └── dto/
│ └── repository/
│ ├── MovieRepository.kt / MovieRepositoryImpl.kt
│ └── TvRepository.kt / TvRepositoryImpl.kt
├── domain
│ ├── model/ (MediaItem, MediaDetail, MediaType)
│ └── usecase/
├── ui
│ ├── home/
│ ├── search/
│ ├── favorites/
│ ├── detail/
│ ├── navigation/
│ └── theme/
└── di/ (Hilt modules: Network, Database, Repository, Coil)
Dependency injection (Hilt) wires repositories into ViewModels and provides singletons for the Retrofit client, Room database, and Coil ImageLoader, keeping each layer unaware of how its dependencies are constructed.
| Library | Purpose |
|---|---|
| Jetpack Compose + Material 3 | Declarative UI, no XML layouts |
| Hilt | Dependency injection across ViewModels, repositories, and data sources |
| Retrofit + Gson | Type-safe HTTP client for the TMDB REST API |
| OkHttp | Underlying HTTP client, used for the API key auth interceptor and image cache-control interceptor |
| Coil | Async image loading with disk/memory caching for posters and backdrops |
| Room | Local persistence for favorites, exposed as Flow for reactive UI updates |
| Navigation Compose | Screen and dialog navigation (NavHost, dialog() destinations) |
| Media3 (ExoPlayer) | Media playback support |
| Kotlin Coroutines / Flow | Async data loading, debouncing, and reactive state |
| MockK | Mocking in unit tests |
| Turbine | Testing Flow/StateFlow emissions |
| Robolectric | Running Room DAO tests against a real in-memory SQLite database under JVM unit tests |
- Get a TMDB API key:
- Create an account at https://www.themoviedb.org/signup
- Go to https://www.themoviedb.org/settings/api and request an API key (choose "Developer" access)
- Copy the example properties file:
copy local.properties.example local.properties
- Open
local.propertiesand set:sdk.dir=/path/to/your/Android/Sdk TMDB_API_KEY=your_tmdb_api_key_here local.propertiesis git-ignored, the key is injected intoBuildConfig.TMDB_API_KEYat build time and attached to every TMDB request byAuthInterceptor.- Build the debug APK:
.\gradlew assembleDebug
Run the unit test suite:
.\gradlew testRun a single test class:
.\gradlew test --tests "com.lev.streamfinder.ui.search.SearchViewModelTest"Coverage:
SearchViewModelTest— verifies the 300ms search debounce: no request fires before the window elapses, exactly one request fires once it does, and rapid keystrokes collapse into a single search for the final query.FavoritesDaoTest— exercisesinsert,deleteById,isFavorite, andgetAllFavorites(including sort order) against a real in-memory Room database via Robolectric.MovieRepositoryImplTest— verifies DTO-to-domain mapping (including dropping unrecognizedmedia_typevalues from multi-search results) and that thrown exceptions are wrapped asResult.failurerather than propagating.
Tests live under app/src/test/java/com/lev/streamfinder/, mirroring the main source package structure.
| Icon | Home | Search | Detail | Favorites |
|---|---|---|---|---|