Audio Handling
Imagine you're building a music app like Spotify or a podcast app. You need to play audio files, let users control playback, and show information about what's playing. Just like a music player that lets you play, pause, skip tracks, and adjust volume, audio handling in Android apps requires special components that can play audio files, manage playback state, and provide controls. Audio playback is similar to video but simpler because you only need to handle sound, not visuals.
In this lesson, we'll learn how to play audio in your Android app using ExoPlayer, which can handle both audio and video. We'll create a simple music player with play, pause, and seek controls, and learn how to manage audio playback state.
When to Use Audio Handling
- When creating music or podcast applications
- When playing sound effects or notifications
- When implementing audio books or educational content
- When building voice recording and playback features
- When creating audio streaming applications
- When implementing background audio playback
- When building radio or live audio streaming apps
- When adding sound effects to games or apps
Key Audio Handling Concepts
| Concept | What It Does | When to Use It |
|---|---|---|
ExoPlayer |
Media player library that can play audio files | When you need to play audio from URLs or files |
MediaItem |
Represents an audio source (URL or file) | When you need to specify what audio to play |
Player |
The main interface for controlling audio playback | When you need to control play, pause, seek, etc. |
prepare() |
Prepares the player to play an audio item | When you want to load audio before playing |
play() / pause() |
Controls audio playback | When you need to start or stop playback |
currentPosition |
Gets the current playback position | When you need to show progress or seek |
duration |
Gets the total duration of the audio | When you need to show total time or progress |
Project Setup: Music Player App
Let's start by setting up our MusicPlayer project. This will be a simple app that demonstrates how to play audio files using ExoPlayer.
Step 1: Adding Dependencies
We'll use the same ExoPlayer dependency we used for video. Update the gradle/libs.versions.toml file:
[versions]
...
exoplayer = "2.19.1" # Add this line
[libraries]
...
exoplayer-core = { group = "com.google.android.exoplayer", name = "exoplayer-core", version.ref = "exoplayer" } # Add this line
Then update the app/build.gradle.kts file:
dependencies {
...
implementation(libs.exoplayer.core)
}
Step 2: Adding Internet Permission
Since our app will play audio 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 audio -->
<uses-permission android:name="android.permission.INTERNET" />
<application
<!--manifest code here-->
</application>
</manifest>
Step 3: Understanding the App Structure
Our MusicPlayer app will have a simple structure with:
- Song List - A list of available songs to play
- Now Playing Screen - Shows the currently playing song with controls
- Playback Controls - Play, pause, previous, next buttons
- Progress Indicator - Shows current position and duration
Step 4: Creating the Data Model
First, let's create our data model. Create a new file called Song.kt:
package com.example.musicplayer
// Data class representing a song in our player
data class Song(
val id: String,
val title: String,
val artist: String,
val audioUrl: String,
val coverImageUrl: String
)
What this does:
id- Unique identifier for each songtitle- The title of the songartist- The artist nameaudioUrl- URL for the audio filecoverImageUrl- URL for the album cover image
Step 5: Creating Sample Data
Now let's create sample data for our app. Create SampleSongData.kt:
package com.example.musicplayer
// Sample song data using public test audio files
object SampleSongData {
fun getSampleSongs(): List {
return listOf(
Song(
id = "1",
title = "Sample Song 1",
artist = "Test Artist",
audioUrl = "https://www.soundhelix.com/examples/mp3/SoundHelix-Song-1.mp3",
coverImageUrl = "https://picsum.photos/400/400?random=1"
),
Song(
id = "2",
title = "Sample Song 2",
artist = "Test Artist",
audioUrl = "https://www.soundhelix.com/examples/mp3/SoundHelix-Song-2.mp3",
coverImageUrl = "https://picsum.photos/400/400?random=2"
),
Song(
id = "3",
title = "Sample Song 3",
artist = "Test Artist",
audioUrl = "https://www.soundhelix.com/examples/mp3/SoundHelix-Song-3.mp3",
coverImageUrl = "https://picsum.photos/400/400?random=3"
)
)
}
}
Step 6: Creating an Audio Player Manager
Let's create a class to manage audio playback. Create AudioPlayerManager.kt:
package com.example.musicplayer
import android.content.Context
import com.google.android.exoplayer2.ExoPlayer
import com.google.android.exoplayer2.MediaItem
class AudioPlayerManager(private val context: Context) {
private var exoPlayer: ExoPlayer? = null
fun initializePlayer(): ExoPlayer {
if (exoPlayer == null) {
exoPlayer = ExoPlayer.Builder(context).build()
}
return exoPlayer!!
}
fun playSong(audioUrl: String) {
val player = initializePlayer()
val mediaItem = MediaItem.fromUri(audioUrl)
player.setMediaItem(mediaItem)
player.prepare()
player.play()
}
fun pause() {
exoPlayer?.pause()
}
fun resume() {
exoPlayer?.play()
}
fun stop() {
exoPlayer?.stop()
}
fun release() {
exoPlayer?.release()
exoPlayer = null
}
fun getPlayer(): ExoPlayer? = exoPlayer
}
Step 7: Creating the Now Playing Screen
Now let's create a screen that shows the currently playing song with controls. Create NowPlayingScreen.kt:
package com.example.musicplayer
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.*
import androidx.compose.material3.*
import androidx.compose.runtime.*
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
import com.google.android.exoplayer2.Player
@Composable
fun NowPlayingScreen(
song: Song,
player: Player?,
onPlayPauseClick: () -> Unit,
onBackClick: () -> Unit,
modifier: Modifier = Modifier
) {
val isPlaying = player?.isPlaying ?: false
val currentPosition = remember { mutableLongStateOf(0L) }
val duration = player?.duration ?: 0L
// Update current position periodically
LaunchedEffect(player) {
while (player != null) {
currentPosition.longValue = player.currentPosition
kotlinx.coroutines.delay(100) // Update every 100ms
}
}
Scaffold(
topBar = {
TopAppBar(
title = { Text("Now Playing") },
navigationIcon = {
IconButton(onClick = onBackClick) {
Icon(
imageVector = Icons.Default.ArrowBack,
contentDescription = "Back"
)
}
}
)
}
) { paddingValues ->
Column(
modifier = modifier
.fillMaxSize()
.padding(paddingValues)
.padding(16.dp),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center
) {
// Album cover
AsyncImage(
model = song.coverImageUrl,
contentDescription = "${song.title} by ${song.artist}",
modifier = Modifier
.size(300.dp)
.clip(CircleShape),
contentScale = ContentScale.Crop
)
Spacer(modifier = Modifier.height(32.dp))
// Song info
Text(
text = song.title,
style = MaterialTheme.typography.headlineMedium
)
Spacer(modifier = Modifier.height(8.dp))
Text(
text = song.artist,
style = MaterialTheme.typography.titleMedium,
color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.7f)
)
Spacer(modifier = Modifier.height(32.dp))
// Progress bar
if (duration > 0) {
LinearProgressIndicator(
progress = { (currentPosition.longValue.toFloat() / duration.toFloat()).coerceIn(0f, 1f) },
modifier = Modifier.fillMaxWidth()
)
Spacer(modifier = Modifier.height(8.dp))
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween
) {
Text(
text = formatTime(currentPosition.longValue),
style = MaterialTheme.typography.bodySmall
)
Text(
text = formatTime(duration),
style = MaterialTheme.typography.bodySmall
)
}
}
Spacer(modifier = Modifier.height(32.dp))
// Playback controls
Row(
horizontalArrangement = Arrangement.spacedBy(24.dp),
verticalAlignment = Alignment.CenterVertically
) {
IconButton(onClick = { /* Previous song */ }) {
Icon(
imageVector = Icons.Default.SkipPrevious,
contentDescription = "Previous",
modifier = Modifier.size(32.dp)
)
}
FloatingActionButton(
onClick = onPlayPauseClick,
modifier = Modifier.size(64.dp)
) {
Icon(
imageVector = if (isPlaying) Icons.Default.Pause else Icons.Default.PlayArrow,
contentDescription = if (isPlaying) "Pause" else "Play",
modifier = Modifier.size(32.dp)
)
}
IconButton(onClick = { /* Next song */ }) {
Icon(
imageVector = Icons.Default.SkipNext,
contentDescription = "Next",
modifier = Modifier.size(32.dp)
)
}
}
}
}
}
fun formatTime(milliseconds: Long): String {
val seconds = (milliseconds / 1000) % 60
val minutes = (milliseconds / (1000 * 60)) % 60
return String.format("%02d:%02d", minutes, seconds)
}
Step 8: Creating the Song List Screen
Let's create a screen that shows a list of available songs. Create SongListScreen.kt:
package com.example.musicplayer
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.shape.CircleShape
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 SongListScreen(
songs: List,
onSongClick: (Song) -> 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(songs) { song ->
SongListItem(
song = song,
onClick = { onSongClick(song) }
)
}
}
}
@Composable
fun SongListItem(
song: Song,
onClick: () -> Unit,
modifier: Modifier = Modifier
) {
Card(
modifier = modifier
.fillMaxWidth()
.clickable(onClick = onClick)
) {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(16.dp),
verticalAlignment = Alignment.CenterVertically
) {
// Album cover thumbnail
AsyncImage(
model = song.coverImageUrl,
contentDescription = "${song.title} by ${song.artist}",
modifier = Modifier
.size(64.dp)
.clip(CircleShape),
contentScale = ContentScale.Crop
)
Spacer(modifier = Modifier.width(16.dp))
// Song info
Column(modifier = Modifier.weight(1f)) {
Text(
text = song.title,
style = MaterialTheme.typography.titleMedium
)
Text(
text = song.artist,
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.7f)
)
}
}
}
}
Step 9: Updating MainActivity
Finally, let's update MainActivity.kt to use our music player:
package com.example.musicplayer
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 androidx.compose.ui.platform.LocalContext
import com.example.musicplayer.ui.theme.MusicPlayerTheme
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()
setContent {
MusicPlayerTheme {
Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding ->
MusicPlayerApp(modifier = Modifier.padding(innerPadding))
}
}
}
}
}
@Composable
fun MusicPlayerApp(modifier: Modifier = Modifier) {
val context = LocalContext.current
val audioPlayerManager = remember { AudioPlayerManager(context) }
var selectedSong by remember { mutableStateOf(null) }
val songs = remember { SampleSongData.getSampleSongs() }
// Initialize player
val player = remember { audioPlayerManager.initializePlayer() }
// Clean up when done
DisposableEffect(Unit) {
onDispose {
audioPlayerManager.release()
}
}
if (selectedSong == null) {
SongListScreen(
songs = songs,
onSongClick = { song ->
selectedSong = song
audioPlayerManager.playSong(song.audioUrl)
}
)
} else {
NowPlayingScreen(
song = selectedSong!!,
player = player,
onPlayPauseClick = {
if (player.isPlaying) {
audioPlayerManager.pause()
} else {
audioPlayerManager.resume()
}
},
onBackClick = {
audioPlayerManager.stop()
selectedSong = null
}
)
}
}
Understanding Each Part of the Implementation
Audio Player Setup:
AudioPlayerManager- Manages the ExoPlayer instanceinitializePlayer()- Creates or returns the playerplaySong()- Loads and plays an audio filepause()/resume()- Controls playback
Playback Controls:
isPlaying- Checks if audio is currently playingcurrentPosition- Gets the current playback positionduration- Gets the total duration of the audioLaunchedEffect- Updates position periodically
UI Components:
LinearProgressIndicator- Shows playback progressformatTime()- Formats milliseconds to MM:SS- Play/Pause button - Toggles playback state
What This App Demonstrates
This MusicPlayer app demonstrates several important concepts:
Basic Audio Playback
- Loading Audio - How to load audio from URLs
- Playback Control - Play, pause, and stop functionality
- State Management - Managing playback state
Progress Tracking
- Current Position - Tracking where you are in the song
- Duration - Getting the total length
- Progress Display - Showing progress visually
Running the App
To run the MusicPlayer app:
- Open the project in Android Studio
- Build and run the app on a device or emulator
- Test the following features:
- Browse the list of songs
- Click on a song to play it
- Use the play/pause button
- Observe the progress bar
- Go back to the song list
Why This Implementation is Important
This example demonstrates how to:
- Play audio files from the internet or local storage
- Control playback with play, pause, and stop
- Track progress and show it to users
- Manage player lifecycle to avoid memory leaks
- Create a music player UI with controls and information
By using ExoPlayer for audio playback, you create apps that can reliably play audio with proper controls and state management. This is essential for any app that needs to play audio, whether it's a music app, podcast app, or any app with sound effects.
Tips for Success
- Use ExoPlayer for audio playback - it works great for both audio and video
- Always release the player when done to avoid memory leaks
- Use
DisposableEffectto clean up resources properly - Update progress indicators periodically using
LaunchedEffect - Format time display in a user-friendly way (MM:SS)
- Handle network errors gracefully when streaming audio
- Show loading states while audio prepares
- Add internet permission when streaming audio from URLs
- Test with different audio formats and qualities
- Consider background playback for music apps
Common Mistakes to Avoid
- Forgetting to release the ExoPlayer instance, causing memory leaks
- Not adding internet permission for streaming audio
- Forgetting to add ExoPlayer dependencies
- Not handling errors when audio fails to load
- Not updating progress indicators, leaving them static
- Playing multiple audio files at once without proper management
- Ignoring player lifecycle and resource cleanup
- Not providing proper error messages when playback fails
- Forgetting to format time display properly
- Not testing on slower devices or network connections
Best Practices
- Always release ExoPlayer instances when done to prevent memory leaks
- Use
DisposableEffectfor proper resource cleanup - Update progress indicators regularly for good user experience
- Format time display in a readable format (MM:SS or HH:MM:SS)
- Handle network errors and playback failures gracefully
- Show loading states while audio prepares
- Test with various audio formats and network conditions
- Consider implementing background playback for music apps
- Provide clear error messages when audio fails to load
- Follow Android's media playback guidelines