A Kotlin Multiplatform app for browsing and exploring movies, powered by the TMDB API. Targets Android and iOS with a shared Compose Multiplatform UI.
Browse popular movies and TV shows, search the TMDB catalog, drill into details (cast, trailers, reviews, seasons, where to watch), and bookmark titles to a local watchlist. Region-aware results are driven by a Settings picker that defaults to your system locale.
The app is a single Gradle module (:composeApp) with one package per feature under
composeApp/src/commonMain/kotlin/dev/odaridavid/smoovie/. Each feature package is self-contained
(screen + ViewModel + UI models + data/ + domain/); platform-native pieces live in
androidMain/ and iosMain/. The Firebase Cloud Function in functions/ proxies TMDB.
The TMDB API token never ships in the mobile app. Requests flow:
App ──(X-Firebase-AppCheck)──▶ Cloud Function (Firebase) ──(Bearer token)──▶ TMDB
The Cloud Function (functions/src/index.ts) verifies the Firebase App Check token (proving the
request came from an authentic build), attaches the TMDB bearer token from Google Secret Manager,
and forwards GET /3/* to api.themoviedb.org. The mobile app's TMDB_BASE_URL points at the
deployed function URL.
Running the app from source requires your own Firebase project. The committed
google-services.json / GoogleService-Info.plist and the TMDB_BASE_URL in AppConfig.kt all
point at the production Smoovie project, and only its owner can register debug App Check tokens
against it — so cloning and running as-is will fail with "Failed to load" on every TMDB call.
If you just want to try the app, install it from Google Play. To build from source, work through all three steps below.
Create a Firebase project (Blaze plan — 2nd-gen functions require it; app-scale usage is $0),
grab a TMDB API token, and deploy the Cloud Function.
Full walkthrough in functions/README.md. You'll end up with a function
URL like https://tmdb-proxy-<hash>-<region>.a.run.app.
- Add Android + iOS apps to your Firebase project, then download the generated
google-services.json(Android) andGoogleService-Info.plist(iOS) and replace the committed files atcomposeApp/google-services.jsonandiosApp/iosApp/GoogleService-Info.plist. - Update
TMDB_BASE_URLinAppConfig.ktto point at your function URL.
App Check rejects requests from non-authentic builds. For your local debug build to be allowed, register its debug token once per device/simulator install:
- Android: run the debug build on your device, then in Logcat filter for
Enter this debug secretand copy the UUID. - iOS: run the debug build, then in the Xcode Debug Area look for the line printed under
===== Firebase App Check Debug Token =====and copy the UUID.
Paste the UUID into Firebase Console → App Check → Apps → ⋮ → Manage debug tokens (in your project from step 1), give it a name, save. The token persists in the app's local storage.
| Library | Purpose |
|---|---|
| Ktor | Multiplatform HTTP client |
| kotlinx.serialization | JSON parsing, typed nav route args |
| kotlinx.coroutines | Async / StateFlow |
| Compose Multiplatform | Shared UI |
| AndroidX Lifecycle | ViewModel + viewModelScope (KMP) |
| AndroidX Navigation Compose | Typed KMP navigation + shared transitions |
| Room KMP | Local persistence for the watchlist |
| androidx.sqlite (bundled) | Bundled SQLite driver for KMP |
| Koin | Multiplatform dependency injection |
| Coil 3 | Image loading (backdrops, posters, profiles) |
| KSP | Room annotation processing |
| multiplatform-settings | Cross-platform KV persistence (filter + region + consent prefs) |
| Firebase App Check | Verifies requests come from authentic builds (Play Integrity / App Attest) |
| Firebase Crashlytics | Android crash reporting (opt-in via settings consent prompt) |
| Napier | Multiplatform logging behind the observability.Logger interface |
| Play In-App Review | Android in-app review prompt (paired with SKStoreReviewController on iOS) |
| ktlint-gradle | Code style enforcement |
| Kover | Code coverage reports (HTML + XML, used by CI) |
| Develocity | Build scans — build/test insights published to scans.gradle.com |
Run from the IDE toolbar or from the terminal:
# macOS / Linux
./gradlew :composeApp:assembleDebug
# Windows
.\gradlew.bat :composeApp:assembleDebugOpen /iosApp in Xcode and run, or use the run configuration in Android Studio / Fleet.
./gradlew :composeApp:allTests./gradlew :composeApp:ktlintCheck # check (what CI runs)
./gradlew :composeApp:ktlintFormat # auto-fixThe Develocity Gradle plugin publishes
build scans — shareable reports of what a build did (task timings,
cache hits, test results, environment). Add --scan to any build to publish one:
./gradlew :composeApp:assembleDebug --scanThe scan URL is printed at the end of the build. Scans publish automatically in CI; locally they
only publish when you pass --scan. The terms of use
are pre-accepted in settings.gradle.kts, so publishing is unattended.
A hooks/pre-commit script runs ktlintCheck before every commit and blocks it if there are
violations. Activate it once after cloning:
git config core.hooksPath hooksIf the hook blocks a commit, run ./gradlew :composeApp:ktlintFormat to auto-fix, re-stage the
changes, then commit again.
Both pages live under docs/ and are served via Firebase Hosting.
Privacy policy (docs/privacy-policy.html) — linked from the Google
Play and App Store listings:
Support (docs/support.html) — linked from the App Store listing (the
Play Console uses the developer support email instead):
To redeploy after editing:
firebase deploy --only hosting