CPS251 Android Development by Scott Shaper

Video Playback

Imagine you're building a video streaming app like YouTube or Netflix. You need to play videos smoothly, let users pause and resume, and handle different video formats. Just like a TV remote that lets you control playback, video playback in Android apps requires special components that can handle video files, manage playback controls, and work with different video sources. Video playback is more complex than images because videos are larger files that need to stream over time, and users expect controls like play, pause, and seek.

In this lesson, we'll learn how to play videos in your Android app using Media3 (AndroidX Media3), which is Google's modern media player library built on ExoPlayer. Media3 is the recommended library for video playback in Android apps. We'll create a simple video player with basic controls, display video thumbnails using Coil, and learn how to handle different video sources.

When to Use Video Playback

  • When displaying video content from the internet or local storage
  • When creating video streaming applications
  • When building video tutorials or educational content
  • When showing video previews or trailers
  • When implementing video recording and playback features
  • When creating media gallery apps with video support
  • When building social media apps with video posts
  • When implementing video advertisements

Key Video Playback Concepts

Concept What It Does When to Use It
Media3 / ExoPlayer Google's modern media player library (AndroidX Media3) for playing videos and audio. Built on ExoPlayer but with updated APIs and better integration with modern Android features. When you need to play video or audio files. Media3 is the current recommended library (ExoPlayer 2.x is deprecated).
PlayerView A view component from Media3 that displays video with built-in playback controls (play, pause, seek, etc.) When you need a simple video player with controls. This is embedded in Compose using AndroidView.
MediaItem Represents a media source (video URL or file). Contains metadata about the media to be played. When you need to specify what video to play. Created from URIs, files, or other media sources.
Coil An image loading library for Android that efficiently loads and displays images from URLs or other sources When you need to display video thumbnails or images from the internet in your Compose UI
Player The main interface for controlling playback When you need to control play, pause, seek, etc.
prepare() Prepares the player to play a media item When you want to load a video before playing
play() / pause() Controls video playback When you need to start or stop playback
seekTo() Jumps to a specific position in the video When you need to skip to a different part

Project Setup: Video Player App

Let's start by setting up our VideoPlayer project. This will be a simple app that demonstrates how to play videos using ExoPlayer.

Step 1: Adding Dependencies

First, we need to add the Media3 (AndroidX Media3) and Coil dependencies. Media3 is the modern replacement for ExoPlayer 2.x, which is now deprecated. Coil is used for loading and displaying video thumbnails. We'll update the gradle/libs.versions.toml file:

[versions]
...
media3 = "1.2.1"  # Media3 version - this is the modern replacement for ExoPlayer 2.x
coil = "2.5.0"    # Coil version for image loading

[libraries]
...
# Media3 dependencies (replaces deprecated ExoPlayer 2.x)
media3-exoplayer = { group = "androidx.media3", name = "media3-exoplayer", version.ref = "media3" }
media3-ui = { group = "androidx.media3", name = "media3-ui", version.ref = "media3" }

# Coil for loading video thumbnails
coil-compose = { group = "io.coil-kt", name = "coil-compose", version.ref = "coil" }

Explanation: Media3 is AndroidX's modern media library that replaces the deprecated ExoPlayer 2.x. It provides the same functionality with updated APIs and better integration. Coil is a lightweight image loading library that works seamlessly with Jetpack Compose.

Then update the app/build.gradle.kts file to include these dependencies:

dependencies {
    ...
    // Media3 for video playback (replaces deprecated ExoPlayer 2.x)
    implementation(libs.media3.exoplayer)
    implementation(libs.media3.ui)
    
    // Coil for loading video thumbnails
    implementation(libs.coil.compose)
    
}

Why these dependencies:

  • media3-exoplayer - Provides the ExoPlayer engine for playing videos and audio
  • media3-ui - Provides the PlayerView component with built-in controls
  • coil-compose - Provides AsyncImage composable for loading images from URLs, which we use for video thumbnails

Step 2: Adding Internet Permission

Since our app will play videos from the internet, we need to add the internet permission to AndroidManifest.xml:

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools">

    <!-- Permission for internet access to stream videos -->
    <uses-permission android:name="android.permission.INTERNET" />

    <application
        <!--manifest code here-->
    </application>
</manifest>

Step 3: Understanding the App Structure

Our VideoPlayer app will have a simple structure with:

  • Video List - A list of available videos to play
  • Video Player - A screen that plays the selected video
  • Playback Controls - Play, pause, and seek controls
  • Video Information - Title and description of the video

Step 4: Creating the Data Model

First, let's create our data model. Create a new file called VideoItem.kt:

package com.example.videoplayer

// Data class representing a video in our player
data class VideoItem(
    val id: String,
    val title: String,
    val description: String,
    val videoUrl: String,
    val thumbnailUrl: String
)

What this does:

  • id - Unique identifier for each video. Used to distinguish between different videos in the list.
  • title - The title of the video displayed to users in the video list and player screen.
  • description - Description of the video content, shown below the title to give users more information.
  • videoUrl - URL for the video file. This can be a remote URL (like from the internet) or a local file path. Media3 supports various video formats and sources.
  • thumbnailUrl - URL for a thumbnail image. This is displayed in the video list to give users a preview of the video content before they play it.

Why a data class: Using a data class provides several benefits: automatic generation of equals(), hashCode(), toString(), and copy() methods. This makes it easy to compare videos, create copies, and debug. The val keyword makes all properties immutable, which is a best practice in Kotlin for data models.

Step 5: Creating Sample Data

Now let's create sample data for our app. Create SampleVideoData.kt:

package com.example.videoplayer

// Sample video data using public test videos
object SampleVideoData {

    fun getSampleVideos(): List {
        return listOf(
            VideoItem(
                id = "1",
                title = "Sample Video 1",
                description = "A sample video for testing video playback",
                videoUrl = "https://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4",
                thumbnailUrl = "https://picsum.photos/400/300?random=1"
            ),
            VideoItem(
                id = "2",
                title = "Sample Video 2",
                description = "Another sample video for testing",
                videoUrl = "https://commondatastorage.googleapis.com/gtv-videos-bucket/sample/ElephantsDream.mp4",
                thumbnailUrl = "https://picsum.photos/400/300?random=2"
            ),
            VideoItem(
                id = "3",
                title = "Sample Video 3",
                description = "A third sample video",
                videoUrl = "https://commondatastorage.googleapis.com/gtv-videos-bucket/sample/ForBiggerBlazes.mp4",
                thumbnailUrl = "https://picsum.photos/400/300?random=3"
            )
        )
    }
}

What this does:

  • object SampleVideoData - Creates a singleton object (only one instance exists). This is perfect for sample data that doesn't need to be instantiated multiple times.
  • fun getSampleVideos(): List<VideoItem> - Returns a list of VideoItem objects. The return type is explicitly specified as List<VideoItem> to ensure type safety.
  • listOf() - Creates an immutable list containing our sample videos. These are public test videos from Google's test video bucket, perfect for testing without needing your own video files.
  • Video URLs: We're using Google's public test video bucket, which provides sample videos in various formats. These URLs are publicly accessible and don't require authentication.
  • Thumbnail URLs: We're using Picsum Photos, a service that provides placeholder images. The ?random=X parameter ensures each video gets a different random image.

Why use an object: Using an object instead of a class means we don't need to create an instance - we can call SampleVideoData.getSampleVideos() directly. This is ideal for utility functions and sample data that don't need state.

Step 6: Creating a Video Player Composable

Now let's create a composable that plays videos. This is the core component that handles video playback. Create VideoPlayerComposable.kt:

package com.example.videoplayer

import android.view.ViewGroup
import android.widget.FrameLayout
import androidx.compose.foundation.layout.*
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.viewinterop.AndroidView
import androidx.media3.exoplayer.ExoPlayer
import androidx.media3.common.MediaItem
import androidx.media3.ui.PlayerView

@Composable
fun VideoPlayerComposable(
    videoUrl: String,
    modifier: Modifier = Modifier
) {
    val context = LocalContext.current

    // Create and remember the ExoPlayer instance
    val exoPlayer = remember {
        ExoPlayer.Builder(context).build().apply {
            val mediaItem = MediaItem.fromUri(videoUrl)
            setMediaItem(mediaItem)
            prepare()
        }
    }

    // Clean up when the composable is removed
    DisposableEffect(Unit) {
        onDispose {
            exoPlayer.release()
        }
    }

    // Use AndroidView to embed the PlayerView
    AndroidView(
        factory = { ctx ->
            PlayerView(ctx).apply {
                player = exoPlayer
                layoutParams = FrameLayout.LayoutParams(
                    ViewGroup.LayoutParams.MATCH_PARENT,
                    ViewGroup.LayoutParams.MATCH_PARENT
                )
                useController = true // Show built-in controls
            }
        },
        modifier = modifier
    )
}

Detailed Explanation:

Imports:

  • androidx.media3.exoplayer.ExoPlayer - The Media3 ExoPlayer class (note: this is from Media3, not the deprecated ExoPlayer 2.x package). This is the core player that handles video decoding and playback.
  • androidx.media3.common.MediaItem - Represents a media source (video URL, file, etc.). This is Media3's way of specifying what to play.
  • androidx.media3.ui.PlayerView - The UI component that displays the video and provides built-in controls. This is a traditional Android View, not a Compose composable.
  • androidx.compose.ui.viewinterop.AndroidView - Allows us to embed traditional Android Views (like PlayerView) into our Compose UI.

Function Parameters:

  • videoUrl: String - The URL of the video to play. This can be a remote URL (http/https) or a local file path.
  • modifier: Modifier = Modifier - Allows the caller to customize the size, padding, and other properties of the video player.

Creating the Player:

  • val context = LocalContext.current - Gets the Android Context, which is required to create an ExoPlayer instance. LocalContext is a Compose utility that provides access to the current Android context.
  • val exoPlayer = remember { ... } - Creates and remembers the ExoPlayer instance. The remember function ensures the player is only created once and reused across recompositions, which is crucial for performance.
  • ExoPlayer.Builder(context).build() - Creates a new ExoPlayer instance using the builder pattern. The builder allows for configuration of the player (buffering, codecs, etc.), but we're using the default configuration here.
  • .apply { ... } - Applies configuration to the player immediately after creation. This is a Kotlin scope function that allows us to configure the object concisely.
  • MediaItem.fromUri(videoUrl) - Creates a MediaItem from the video URL. Media3 supports various URI schemes (http, https, file, content, etc.).
  • setMediaItem(mediaItem) - Tells the player what media to play. You can set multiple items to create a playlist, but here we're just playing one video.
  • prepare() - Prepares the player to play the media. This starts loading the video metadata and buffering. The video won't start playing automatically - the user needs to press play (or you can set playWhenReady = true).

Lifecycle Management:

  • DisposableEffect(Unit) - A Compose effect that runs cleanup code when the composable is removed from the composition. The Unit parameter means this effect runs once when the composable is first composed.
  • onDispose { exoPlayer.release() } - Releases the player's resources when the composable is removed. This is critical - failing to release the player causes memory leaks and can crash your app. The release() method frees all resources (decoders, buffers, network connections, etc.).

Embedding the PlayerView:

  • AndroidView - This composable allows us to embed traditional Android Views into Compose. Since PlayerView is a traditional View (not a Compose composable), we need this bridge.
  • factory = { ctx -> ... } - The factory lambda creates the PlayerView. It receives a Context parameter that we use to create the view.
  • PlayerView(ctx) - Creates the PlayerView instance. This view handles displaying the video and showing the controls.
  • player = exoPlayer - Connects the PlayerView to our ExoPlayer instance. The view will now display the video from this player.
  • layoutParams = FrameLayout.LayoutParams(...) - Sets the layout parameters to fill the available space. MATCH_PARENT means the view will take up all available width and height.
  • useController = true - Enables the built-in playback controls (play, pause, seek bar, etc.). Setting this to false would hide the controls, requiring you to build custom controls.

Why this approach: Media3's PlayerView is a mature, well-tested component that provides excellent video playback with minimal code. While you could build a custom video player from scratch, using PlayerView saves significant development time and ensures compatibility with various video formats and Android versions.

Step 7: Creating the Video Player Screen

Now let's create a screen that displays the video player with a top app bar and video information. Create VideoPlayerScreen.kt:

package com.example.videoplayer

import androidx.compose.foundation.layout.*
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material3.*
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp

@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun VideoPlayerScreen(
    videoItem: VideoItem,
    onBackClick: () -> Unit,
    modifier: Modifier = Modifier
) {
    Scaffold(
        topBar = {
            TopAppBar(
                title = { Text(videoItem.title) },
                navigationIcon = {
                    IconButton(onClick = onBackClick) {
                        Icon(
                            imageVector = Icons.AutoMirrored.Filled.ArrowBack,
                            contentDescription = "Back"
                        )
                    }
                }
            )
        }
    ) { paddingValues ->
        Column(
            modifier = modifier
                .fillMaxSize()
                .padding(paddingValues)
        ) {
            // Video player
            VideoPlayerComposable(
                videoUrl = videoItem.videoUrl,
                modifier = Modifier
                    .fillMaxWidth()
                    .aspectRatio(16f / 9f) // Standard video aspect ratio
            )

            // Video information
            Column(
                modifier = Modifier
                    .fillMaxWidth()
                    .padding(16.dp)
            ) {
                Text(
                    text = videoItem.title,
                    style = MaterialTheme.typography.titleLarge
                )
                Spacer(modifier = Modifier.height(8.dp))
                Text(
                    text = videoItem.description,
                    style = MaterialTheme.typography.bodyMedium
                )
            }
        }
    }
}

Detailed Explanation:

Imports:

  • androidx.compose.material.icons.automirrored.filled.ArrowBack - Uses the AutoMirrored version of the ArrowBack icon. This is the recommended version because it automatically mirrors in right-to-left (RTL) languages. The old Icons.Default.ArrowBack is deprecated.
  • androidx.compose.material3.* - Imports Material Design 3 components, including TopAppBar, Scaffold, and Text.

Annotation:

  • @OptIn(ExperimentalMaterial3Api::class) - This annotation is required because TopAppBar is currently an experimental Material3 API. This tells the compiler that we're intentionally using an experimental API. Without this, you'll get a compilation error.

Function Parameters:

  • videoItem: VideoItem - The video data to display, containing the URL, title, description, etc.
  • onBackClick: () -> Unit - A callback function that's called when the user taps the back button. This allows the parent composable to handle navigation.
  • modifier: Modifier = Modifier - Allows customization of the screen's layout properties.

Scaffold Structure:

  • Scaffold - A Material Design component that provides a basic screen structure with slots for a top app bar, bottom bar, floating action button, etc. It handles system window insets (like the status bar) automatically.
  • topBar = { TopAppBar(...) } - Defines the top app bar (toolbar) that appears at the top of the screen. This shows the video title and a back button.
  • TopAppBar - Material3's top app bar component. It provides a consistent look and feel with Material Design guidelines.
  • title = { Text(videoItem.title) } - Displays the video title in the center of the app bar. The Text composable is wrapped in a lambda so it can access the current theme.
  • navigationIcon - The icon shown on the left side of the app bar, typically used for navigation (like going back).
  • IconButton - A clickable button that contains an icon. It provides proper touch targets and ripple effects.
  • Icons.AutoMirrored.Filled.ArrowBack - The back arrow icon. The AutoMirrored version automatically flips in RTL languages, which is important for internationalization.
  • contentDescription = "Back" - Provides an accessibility description for screen readers. This is required for accessibility compliance.

Content Layout:

  • { paddingValues -> ... } - The Scaffold's content lambda receives paddingValues that account for the top app bar and system insets. We apply this padding to ensure content isn't hidden behind the app bar.
  • Column - Arranges children vertically. The video player is on top, and the video information is below.
  • modifier.fillMaxSize().padding(paddingValues) - Makes the column fill the entire screen and applies the padding from Scaffold to avoid overlap with the app bar.
  • VideoPlayerComposable - Our custom composable that handles video playback. We pass the video URL from the videoItem.
  • .fillMaxWidth().aspectRatio(16f / 9f) - Makes the video player fill the width and maintain a 16:9 aspect ratio (standard widescreen format). This ensures the video doesn't look stretched or squished.
  • Column (for video info) - A second column that displays the video title and description below the player.
  • Text with titleLarge style - Displays the video title in a large, prominent font.
  • Spacer - Adds vertical spacing between the title and description for better readability.
  • Text with bodyMedium style - Displays the description in a smaller, secondary font.

Why this structure: The Scaffold provides a standard Android app layout pattern that users are familiar with. The top app bar gives clear navigation, and the aspect ratio constraint ensures videos display correctly regardless of screen size.

Step 8: Creating a Video List Screen

Let's create a screen that shows a scrollable list of available videos with thumbnails. This screen uses Coil to load images from URLs. Create VideoListScreen.kt:

package com.example.videoplayer

import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.*
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.unit.dp
import coil.compose.AsyncImage

@Composable
fun VideoListScreen(
    videos: List,
    onVideoClick: (VideoItem) -> Unit,
    modifier: Modifier = Modifier
) {
    LazyColumn(
        modifier = modifier
            .fillMaxSize()
            .padding(top = 50.dp),
        verticalArrangement = Arrangement.spacedBy(8.dp),
        contentPadding = PaddingValues(horizontal = 16.dp, vertical = 8.dp)
    ) {
        items(videos) { video ->
            VideoListItem(
                videoItem = video,
                onClick = { onVideoClick(video) }
            )
        }
    }
}

@Composable
fun VideoListItem(
    videoItem: VideoItem,
    onClick: () -> Unit,
    modifier: Modifier = Modifier
) {
    Card(
        modifier = modifier
            .fillMaxWidth()
            .clickable(onClick = onClick),
        shape = RoundedCornerShape(8.dp)
    ) {
        Column {
            // Thumbnail
            AsyncImage(
                model = videoItem.thumbnailUrl,
                contentDescription = videoItem.title,
                modifier = Modifier
                    .fillMaxWidth()
                    .height(200.dp),
                contentScale = ContentScale.Crop
            )

            // Video info
            Column(
                modifier = Modifier.padding(16.dp)
            ) {
                Text(
                    text = videoItem.title,
                    style = MaterialTheme.typography.titleMedium
                )
                Spacer(modifier = Modifier.height(4.dp))
                Text(
                    text = videoItem.description,
                    style = MaterialTheme.typography.bodySmall,
                    color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.7f)
                )
            }
        }
    }
}

Detailed Explanation:

Imports:

  • coil.compose.AsyncImage - Coil's Compose image loading composable. This efficiently loads images from URLs with automatic caching, memory management, and placeholder support.
  • androidx.compose.foundation.lazy.LazyColumn - A vertically scrollable list that only composes visible items. This is much more efficient than a regular Column for long lists because it reuses views and only creates items that are visible.
  • androidx.compose.foundation.lazy.items - Extension function that makes it easy to create list items from a collection.
  • androidx.compose.foundation.clickable - Makes a composable respond to click/tap events with proper touch feedback.

VideoListScreen Function:

  • videos: List<VideoItem> - The list of videos to display. Note the explicit type parameter List<VideoItem> - this is required in Kotlin for type safety.
  • onVideoClick: (VideoItem) -> Unit - Callback function called when a user taps a video. It receives the clicked VideoItem so the parent can navigate to the player screen.
  • LazyColumn - Creates a scrollable vertical list. Unlike a regular Column, LazyColumn only creates composables for items that are currently visible, making it efficient for long lists.
  • .fillMaxSize() - Makes the list fill the entire available space.
  • .padding(top = 50.dp) - Adds top padding. This might be adjusted based on your app's design (e.g., if you have a status bar or other UI elements).
  • verticalArrangement = Arrangement.spacedBy(8.dp) - Adds 8dp of space between each video item for visual separation.
  • contentPadding - Adds padding around the entire list content (horizontal and vertical). This ensures items don't touch the screen edges.
  • items(videos) { video -> ... } - Creates a list item for each video in the list. The lambda receives each VideoItem and creates a VideoListItem composable for it.

VideoListItem Function:

  • Card - Material Design card component that provides elevation and rounded corners. Cards are perfect for displaying content in lists.
  • .fillMaxWidth() - Makes the card fill the available width.
  • .clickable(onClick = onClick) - Makes the entire card clickable. When tapped, it calls the onClick callback.
  • shape = RoundedCornerShape(8.dp) - Gives the card rounded corners (8dp radius) for a modern, polished look.
  • Column - Arranges the thumbnail and video info vertically within the card.
  • AsyncImage - Coil's image loading composable. This is the key component for displaying thumbnails:
    • model = videoItem.thumbnailUrl - The URL of the image to load. Coil supports URLs, URIs, files, resources, and more.
    • contentDescription - Accessibility description for screen readers. Important for users with visual impairments.
    • .fillMaxWidth().height(200.dp) - Makes the image fill the width and sets a fixed height of 200dp. This ensures consistent card sizes.
    • contentScale = ContentScale.Crop - Scales the image to fill the space while maintaining aspect ratio, cropping if necessary. This ensures thumbnails look good even if source images have different aspect ratios.
  • Column (for video info) - Contains the title and description text.
  • Text with titleMedium - Displays the video title in a medium-sized, prominent font.
  • Spacer - Adds 4dp of space between title and description.
  • Text with bodySmall - Displays the description in a smaller, secondary font.
  • color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.7f) - Makes the description text 70% opaque, giving it a subtle, secondary appearance that doesn't compete with the title.

Why Coil for Images: Coil is a modern, efficient image loading library specifically designed for Kotlin and Compose. It provides:

  • Automatic memory and disk caching
  • Efficient memory usage
  • Automatic image decoding and resizing
  • Placeholder and error image support
  • Seamless integration with Jetpack Compose

Why LazyColumn: For lists with more than a few items, LazyColumn is essential for performance. It only creates composables for visible items, reuses them as you scroll, and handles large lists efficiently without causing memory issues or lag.

Step 9: Updating MainActivity

Finally, let's update MainActivity.kt to tie everything together. This is the entry point of our app and manages the navigation between the video list and player screens:

package com.example.videoplayer

import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Scaffold
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import com.example.videoplayer.ui.theme.VideoPlayerTheme

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        enableEdgeToEdge()
        setContent {
            VideoPlayerTheme {
                Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding ->
                    VideoPlayerApp(modifier = Modifier.padding(innerPadding))
                }
            }
        }
    }
}

@Composable
fun VideoPlayerApp(modifier: Modifier = Modifier) {
    var selectedVideo by remember { mutableStateOf(null) }
    val videos = remember { SampleVideoData.getSampleVideos() }

    if (selectedVideo == null) {
        VideoListScreen(
            videos = videos,
            onVideoClick = { video -> selectedVideo = video }
        )
    } else {
        VideoPlayerScreen(
            videoItem = selectedVideo!!,
            onBackClick = { selectedVideo = null }
        )
    }
}

Detailed Explanation:

MainActivity Class:

  • class MainActivity : ComponentActivity - The main activity of our app. ComponentActivity is the base class for activities that use Jetpack Compose.
  • override fun onCreate(savedInstanceState: Bundle?) - Called when the activity is created. This is where we set up our UI.
  • enableEdgeToEdge() - Enables edge-to-edge display, allowing content to extend behind system bars (status bar, navigation bar). This gives a modern, immersive look.
  • setContent { ... } - Sets the Compose content for this activity. Everything inside this lambda becomes the UI of our app.
  • VideoPlayerTheme - Applies our app's theme (colors, typography, etc.). This ensures consistent styling throughout the app.
  • Scaffold - Provides the basic screen structure. The innerPadding accounts for system bars when edge-to-edge is enabled.
  • VideoPlayerApp - Our root composable that manages the app's state and navigation.

VideoPlayerApp Composable:

  • var selectedVideo by remember { mutableStateOf(null) } - This is the key piece of state management:
    • var - A mutable variable that can change over time.
    • by remember - The remember function ensures this state persists across recompositions (when the UI updates). Without it, the state would reset every time the composable recomposes.
    • mutableStateOf(null) - Creates a state holder that can hold either a VideoItem or null. The type parameter <VideoItem?> is required - without it, Kotlin would infer the type as Nothing?, which can't hold any values.
    • null initially means no video is selected, so we show the list.
    • When a video is selected, this becomes a VideoItem, and we show the player screen.
  • val videos = remember { SampleVideoData.getSampleVideos() } - Loads the list of videos. The remember ensures we only call getSampleVideos() once, not on every recomposition. This is a performance optimization.
  • if (selectedVideo == null) { ... } else { ... } - Simple conditional navigation:
    • If selectedVideo is null, show the VideoListScreen.
    • Otherwise, show the VideoPlayerScreen with the selected video.
  • VideoListScreen - Displays the list of videos. When a video is clicked:
    • onVideoClick = { video -> selectedVideo = video } - Sets selectedVideo to the clicked video. This triggers a recomposition, and the if condition will now be false, so the player screen is shown.
  • VideoPlayerScreen - Displays the video player. When the back button is clicked:
    • videoItem = selectedVideo!! - Uses the non-null assertion operator (!!) because we know selectedVideo is not null in the else branch. This is safe here because of the if condition.
    • onBackClick = { selectedVideo = null } - Sets selectedVideo back to null. This triggers a recomposition, and we're back to showing the list.

State Management Pattern: This is a simple but effective state management pattern for navigation:

  • We use a single state variable (selectedVideo) to determine which screen to show.
  • When the state changes, Compose automatically recomposes and shows the appropriate screen.
  • This is a unidirectional data flow: state flows down (to child composables), and events flow up (callbacks change the state).
  • For more complex apps, you might use Navigation Compose or a state management library, but for simple two-screen navigation, this pattern works perfectly.

Understanding Each Part of the Implementation

Media3/ExoPlayer Setup (Modern API):

  • ExoPlayer.Builder - Creates a new player instance using the builder pattern. The builder allows you to configure various aspects of the player (codecs, buffering, etc.), but we're using default settings here.
  • MediaItem.fromUri() - Creates a MediaItem from a video URL. Media3's MediaItem is more flexible than ExoPlayer 2.x - it supports various URI schemes and can include metadata.
  • setMediaItem() - Sets the video to play. You can set multiple items to create a playlist, or replace the current item with a new one.
  • prepare() - Prepares the player to play the media. This starts loading metadata and buffering the video. The video won't auto-play unless you also set playWhenReady = true.
  • Note: We're using Media3 (AndroidX Media3), not the deprecated ExoPlayer 2.x. The imports are from androidx.media3 packages, not com.google.android.exoplayer2.

PlayerView (Media3 UI Component):

  • PlayerView - A traditional Android View (not a Compose composable) that displays the video and provides built-in controls. This is from Media3's UI library.
  • useController = true - Enables the built-in playback controls (play, pause, seek bar, fullscreen, etc.). Setting this to false would hide controls, requiring you to build custom controls.
  • AndroidView - A Compose composable that allows embedding traditional Android Views into Compose. This is necessary because PlayerView is a View, not a composable.
  • layoutParams - Sets the view's layout parameters to fill the available space (MATCH_PARENT for both width and height).

Lifecycle Management (Critical for Memory):

  • DisposableEffect - A Compose side effect that runs cleanup code when the composable is removed from the composition. This is essential for resource management.
  • exoPlayer.release() - Releases all player resources (decoders, buffers, network connections, etc.). This is absolutely critical - failing to release the player causes memory leaks, can crash your app, and wastes system resources.
  • Why it matters: Video players use significant system resources. Without proper cleanup, you'll quickly run out of memory, especially if users navigate between multiple videos.

Coil Image Loading:

  • AsyncImage - Coil's Compose composable for loading images asynchronously from URLs.
  • model - The image source (URL, URI, file, resource, etc.). Coil automatically handles the loading, caching, and decoding.
  • contentScale - How to scale the image. ContentScale.Crop fills the space while maintaining aspect ratio, cropping excess if needed.
  • Benefits: Automatic caching (memory and disk), efficient memory usage, placeholder support, and seamless Compose integration.

State Management:

  • remember - Persists state across recompositions. Essential for maintaining state in Compose.
  • mutableStateOf - Creates observable state. When the value changes, Compose automatically recomposes dependent UI.
  • Type safety: Always specify the type parameter (e.g., mutableStateOf<VideoItem?>(null)) to avoid type inference issues.

Tips for Success

  • Use Media3, not ExoPlayer 2.x - Media3 (AndroidX Media3) is the current recommended library. ExoPlayer 2.x is deprecated. Always use androidx.media3 packages, not com.google.android.exoplayer2.
  • Always release the player - Use DisposableEffect to call exoPlayer.release() when done. This is critical for preventing memory leaks and crashes.
  • Use remember for state - Always use remember for state that should persist across recompositions. Without it, state resets on every recomposition.
  • Specify type parameters explicitly - When using mutableStateOf or List, always specify the type parameter (e.g., mutableStateOf<VideoItem?>(null)) to avoid type inference issues.
  • Use LazyColumn for lists - For any list with more than a few items, use LazyColumn instead of Column. It only composes visible items, making it much more efficient.
  • Add @OptIn for experimental APIs - If you use experimental Material3 APIs like TopAppBar, add @OptIn(ExperimentalMaterial3Api::class) to avoid compilation errors.
  • Use AutoMirrored icons - Use Icons.AutoMirrored.Filled.ArrowBack instead of the deprecated Icons.Default.ArrowBack for better RTL language support.
  • Test with different video formats - Test your app with various video formats (MP4, WebM, etc.) and sizes to ensure compatibility.
  • Handle network errors gracefully - When streaming videos, handle network failures, timeouts, and other errors. Show user-friendly error messages.
  • Show loading indicators - Consider showing loading indicators while videos prepare. This improves perceived performance and user experience.
  • Use appropriate aspect ratios - Set aspect ratios (like 16:9) to prevent video distortion. Don't let videos stretch to fill arbitrary spaces.
  • Add internet permission - Don't forget to add <uses-permission android:name="android.permission.INTERNET" /> to AndroidManifest.xml when streaming videos.
  • Use Coil for images - Coil is the recommended image loading library for Compose. It handles caching, memory management, and provides excellent performance.
  • Test on different devices - Test on various devices (phones, tablets) and screen sizes to ensure your UI works well everywhere.
  • Consider thumbnails - Using thumbnails in video lists improves performance and user experience. Load full videos only when the user selects them.

Common Mistakes to Avoid

  • Using deprecated ExoPlayer 2.x - Don't use com.google.android.exoplayer2 packages. Always use androidx.media3 (Media3) instead.
  • Forgetting to release the player - Not calling exoPlayer.release() in DisposableEffect causes memory leaks and can crash your app after playing a few videos.
  • Missing type parameters - Forgetting to specify types like List<VideoItem> or mutableStateOf<VideoItem?> causes compilation errors or type inference issues.
  • Not using remember - Forgetting remember for state means it resets on every recomposition, making state management impossible.
  • Not adding @OptIn annotation - Using experimental Material3 APIs like TopAppBar without @OptIn(ExperimentalMaterial3Api::class) causes compilation errors.
  • Using deprecated icons - Using Icons.Default.ArrowBack instead of Icons.AutoMirrored.Filled.ArrowBack causes deprecation warnings and poor RTL support.
  • Not adding internet permission - Forgetting <uses-permission android:name="android.permission.INTERNET" /> means videos from URLs won't load.
  • Forgetting Coil dependency - If you use AsyncImage from Coil, you must add the Coil dependency. The import alone isn't enough.
  • Not handling errors - Not handling network failures, invalid URLs, or unsupported formats leads to crashes and poor user experience.
  • Playing multiple videos simultaneously - Creating multiple ExoPlayer instances without proper management wastes resources and can cause performance issues.
  • Using Column instead of LazyColumn - Using regular Column for long lists causes performance issues. Always use LazyColumn for scrollable lists.
  • Ignoring player lifecycle - Not properly managing when players are created and destroyed leads to resource leaks and crashes.
  • No error messages - Not providing user-friendly error messages when videos fail to load leaves users confused.
  • Wrong aspect ratios - Not setting proper aspect ratios makes videos look stretched or squished, creating a poor viewing experience.
  • Not testing edge cases - Not testing with slow networks, different video formats, or various device sizes leads to issues in production.

Best Practices

  • Use Media3 (AndroidX Media3) - Always use the modern Media3 library (androidx.media3) instead of deprecated ExoPlayer 2.x. This ensures you're using actively maintained, modern APIs.
  • Always release players - Use DisposableEffect to call exoPlayer.release() when composables are removed. This is non-negotiable for preventing memory leaks.
  • Use remember for state - Always wrap state in remember to persist it across recompositions. This is fundamental to Compose state management.
  • Specify types explicitly - Always specify type parameters for generics (e.g., List<VideoItem>, mutableStateOf<VideoItem?>) to avoid type inference issues and improve code clarity.
  • Use LazyColumn for lists - Always use LazyColumn (or LazyRow) for scrollable lists. It's much more efficient than Column for multiple items.
  • Handle errors gracefully - Implement proper error handling for network failures, invalid URLs, unsupported formats, and playback errors. Show user-friendly error messages.
  • Show loading states - Display loading indicators while videos prepare. This improves perceived performance and user experience.
  • Use proper aspect ratios - Always set appropriate aspect ratios (like 16:9) to prevent video distortion. Use aspectRatio() modifier in Compose.
  • Use Coil for images - Use Coil's AsyncImage for loading thumbnails. It provides automatic caching, efficient memory usage, and excellent Compose integration.
  • Test thoroughly - Test with various video formats, network conditions (fast, slow, offline), device sizes, and orientations to ensure robustness.
  • Use thumbnails - Load and display thumbnails in video lists. Only load full videos when users select them. This improves performance and reduces data usage.
  • Follow Material Design - Use Material3 components and follow Material Design guidelines for a consistent, modern UI that users expect.
  • Add proper permissions - Add internet permission for streaming, and consider storage permissions if playing local files.
  • Optimize for performance - Consider video quality selection based on network conditions, implement proper buffering strategies, and optimize memory usage.
  • Handle lifecycle properly - Ensure players are paused/resumed when the app goes to background/foreground. Consider using LifecycleObserver for this.
  • Use AutoMirrored icons - Use AutoMirrored versions of icons (like Icons.AutoMirrored.Filled.ArrowBack) for better internationalization and RTL language support.
  • Add @OptIn for experimental APIs - When using experimental Material3 APIs, always add the appropriate @OptIn annotation to acknowledge you're using experimental features.