A production-shaped Android sample that plays local videos with AndroidX Media3 (ExoPlayer) and a Compose-first UI.
Pick files from device storage, build a playlist, and play them with Material 3 controls β without wrapping PlayerView in AndroidView.
- Compose-native playback via
media3-ui-compose-material3(Playercomposable, shutter, progress, play/pause) - ExoPlayer provided with Hilt at ViewModel scope and released with the ViewModel
- SAF picker (
OpenDocument) with persistable read permission so URIs survive process death - Material 3 layout: edge-to-edge, dynamic color on Android 12+, playlist empty state, now-playing row
- Modern toolchain: AGP 9.4, Gradle 9.6, built-in Kotlin, Compose Compiler plugin, KSP instead of kapt
- Version catalog (
gradle/libs.versions.toml) for a single source of truth
flowchart LR
A[MainActivity] --> B[VideoPlayerScreen]
B --> C[MainViewModel]
C --> D[ExoPlayer]
C --> E[MetaDataReader]
C --> F[SavedStateHandle]
G[Hilt VideoPlayerModule] --> D
G --> E
| Layer | Responsibility |
|---|---|
MainActivity |
Edge-to-edge host, Compose theme |
VideoPlayerScreen |
Player surface, playlist, SAF launcher |
MainViewModel |
Playlist state, playback, URI persistence |
VideoPlayerModule |
@ViewModelScoped ExoPlayer + metadata reader |
MetaDataReader |
Display name from MediaStore |
Playback pauses on ON_STOP so audio does not continue in the background. This sample is a foreground local player, not a MediaSession / notification service.
| Component | Version |
|---|---|
| Android Gradle Plugin | 9.4.0 |
| Gradle | 9.6.0 |
| Kotlin | 2.3.21 (built-in Kotlin + Compose compiler plugin) |
| KSP | 2.3.11 |
| Compose BOM | 2026.08.00 |
| Media3 | 1.11.1 |
| Hilt | 2.60.1 |
| compileSdk | 37 |
| targetSdk | 36 |
| minSdk | 26 (Android 8.0) |
| JVM | 17 |
app/src/main/java/com/halil/ozel/videoplayercomposesample/
βββ MainActivity.kt
βββ VideoPlayerApp.kt
βββ data/
β βββ MetaDataReader.kt
β βββ VideoItem.kt
βββ di/
β βββ VideoPlayerModule.kt
βββ ui/
βββ MainViewModel.kt
βββ VideoPlayerScreen.kt
βββ theme/
- Android Studio with AGP 9.4 support (for example Android Studio Quail)
- JDK 17 or newer for the Gradle daemon
- Android SDK Platform 37 (compile) β devices still target API 36
git clone https://github.com/halilozel1903/VideoPlayerComposeSample.git
cd VideoPlayerComposeSample
./gradlew :app:assembleDebugOpen the project in Android Studio, connect a device or emulator (API 26+), and run the app configuration.
- Tap the library FAB.
- Choose any video from storage (not limited to MP4).
- Tap a row in the playlist to start playback.
- Use the on-player Material 3 controls for play, pause, and seek.
@Provides
@ViewModelScoped
fun provideVideoPlayer(app: Application): Player {
return ExoPlayer.Builder(app).build()
}The ViewModel owns the Player, restores the URI list from SavedStateHandle, and maps URIs to MediaItem plus a display name. The screen collects that list with collectAsStateWithLifecycle and renders Media3βs Compose Player.
- kapt is gone. Hilt runs through KSP.
- Do not apply
org.jetbrains.kotlin.android. AGP 9 supplies built-in Kotlin. - Keep applying
org.jetbrains.kotlin.plugin.composefor the Compose compiler. - Dependency versions live in
gradle/libs.versions.toml. Bump them there first.
./gradlew help
./gradlew build --dry-run
./gradlew :app:assembleDebugIf you are using this repo as a tutorial, a useful order is:
VideoPlayerModuleβ how ExoPlayer is created and scopedMainViewModelβ playlist +MediaItemVideoPlayerScreenβ Compose UI and lifecycle pauseMetaDataReaderβ reading a display name from a content URI
Official docs:
Issues and pull requests are welcome. Please keep changes focused (toolchain, player, UI, or docs) and match the existing Kotlin style.
MIT License
Copyright (c) 2026 Halil Ozel
See LICENSE for the full text.
Halil Ozel β GitHub


