Setup and context — Why Build a Music Player App?
A music player app is one of the best learning projects for mobile developers. It naturally combines essential skills — state management for playback controls, real-time UI updates for seek bars, list management for playlists, and platform-specific configuration for background audio.
With Rork, you can leverage AI assistance to scaffold a React Native-based audio player quickly, then refine the implementation with your own logic. In this guide, we'll walk through building a practical music player from scratch using expo-av, Expo's built-in audio and video library.
What you'll learn:
- Loading and controlling audio files with expo-av
- Implementing seek bars and volume controls
- Managing playlists with track switching
- Configuring background audio playback
This guide is aimed at developers who have some experience with Rork and understand basic React Native concepts.
Setting Up expo-av in Your Rork Project
Start by creating a new project in Rork. Tell Rork's AI something like "I want to build a music player app," and it will generate a solid project structure for you.
expo-av comes bundled with the Expo SDK, so it works out of the box in Rork projects. If you need to install it manually:
# Install expo-av
npx expo install expo-avNext, configure the audio mode when your app starts. Call Audio.setAudioModeAsync to set up playback behavior:
// hooks/useAudioSetup.ts
import { Audio } from 'expo-av';
import { useEffect } from 'react';
export function useAudioSetup() {
useEffect(() => {
async function configureAudio() {
await Audio.setAudioModeAsync({
allowsRecordingIOS: false,
staysActiveInBackground: true, // Enable background playback
playsInSilentModeIOS: true, // Play even in silent mode
shouldDuckAndroid: true, // Lower other apps' audio
});
}
configureAudio();
}, []);
}
// Expected behavior:
// - Audio mode is configured automatically on app launch
// - Music continues playing when the app is backgrounded
// - Audio plays even with iOS silent switch enabledThis setup only needs to run once. Setting staysActiveInBackground: true ensures that music keeps playing when the user switches to another app.
Audio Playback Basics — Loading, Playing, and Pausing
The Audio.Sound object from expo-av handles loading and controlling audio files. Here's a reusable custom hook that encapsulates the core playback logic:
// hooks/useAudioPlayer.ts
import { useState, useCallback, useRef } from 'react';
import { Audio, AVPlaybackStatus } from 'expo-av';
interface Track {
id: string;
title: string;
artist: string;
uri: string; // Local or remote audio file URL
artwork?: string; // Album artwork URL
}
interface PlayerState {
isPlaying: boolean;
isLoaded: boolean;
positionMillis: number;
durationMillis: number;
}
export function useAudioPlayer() {
const soundRef = useRef<Audio.Sound | null>(null);
const [playerState, setPlayerState] = useState<PlayerState>({
isPlaying: false,
isLoaded: false,
positionMillis: 0,
durationMillis: 0,
});
// Callback for playback status updates
const onPlaybackStatusUpdate = useCallback((status: AVPlaybackStatus) => {
if (status.isLoaded) {
setPlayerState({
isPlaying: status.isPlaying,
isLoaded: true,
positionMillis: status.positionMillis,
durationMillis: status.durationMillis ?? 0,
});
}
}, []);
// Load a track
const loadTrack = useCallback(async (track: Track) => {
// Release the existing sound object
if (soundRef.current) {
await soundRef.current.unloadAsync();
}
const { sound } = await Audio.Sound.createAsync(
{ uri: track.uri },
{ shouldPlay: false },
onPlaybackStatusUpdate
);
soundRef.current = sound;
}, [onPlaybackStatusUpdate]);
// Toggle play/pause
const togglePlayPause = useCallback(async () => {
if (!soundRef.current) return;
if (playerState.isPlaying) {
await soundRef.current.pauseAsync();
} else {
await soundRef.current.playAsync();
}
}, [playerState.isPlaying]);
// Seek to a specific position
const seekTo = useCallback(async (positionMillis: number) => {
if (!soundRef.current) return;
await soundRef.current.setPositionAsync(positionMillis);
}, []);
return { playerState, loadTrack, togglePlayPause, seekTo };
}The key here is the onPlaybackStatusUpdate callback. expo-av fires this callback whenever the playback position changes, enabling real-time UI updates without polling.
Building the Seek Bar and Playback Controls
The seek bar and playback controls define how intuitive your music player feels. Let's build a clean, responsive control interface:
// components/PlayerControls.tsx
import React from 'react';
import { View, Text, TouchableOpacity, StyleSheet } from 'react-native';
import Slider from '@react-native-community/slider';
import { Ionicons } from '@expo/vector-icons';
interface PlayerControlsProps {
isPlaying: boolean;
positionMillis: number;
durationMillis: number;
onTogglePlayPause: () => void;
onSeek: (position: number) => void;
onPrevious: () => void;
onNext: () => void;
}
// Format milliseconds to "MM:SS"
function formatTime(millis: number): string {
const totalSeconds = Math.floor(millis / 1000);
const minutes = Math.floor(totalSeconds / 60);
const seconds = totalSeconds % 60;
return `${minutes}:${seconds.toString().padStart(2, '0')}`;
}
export function PlayerControls({
isPlaying,
positionMillis,
durationMillis,
onTogglePlayPause,
onSeek,
onPrevious,
onNext,
}: PlayerControlsProps) {
return (
<View style={styles.container}>
{/* Seek bar */}
<Slider
style={styles.slider}
minimumValue={0}
maximumValue={durationMillis}
value={positionMillis}
onSlidingComplete={onSeek}
minimumTrackTintColor="#1DB954"
maximumTrackTintColor="#535353"
thumbTintColor="#1DB954"
/>
{/* Elapsed / remaining time */}
<View style={styles.timeRow}>
<Text style={styles.timeText}>
{formatTime(positionMillis)}
</Text>
<Text style={styles.timeText}>
-{formatTime(durationMillis - positionMillis)}
</Text>
</View>
{/* Playback control buttons */}
<View style={styles.controls}>
<TouchableOpacity onPress={onPrevious}>
<Ionicons name="play-skip-back" size={32} color="#fff" />
</TouchableOpacity>
<TouchableOpacity
onPress={onTogglePlayPause}
style={styles.playButton}
>
<Ionicons
name={isPlaying ? 'pause-circle' : 'play-circle'}
size={64}
color="#1DB954"
/>
</TouchableOpacity>
<TouchableOpacity onPress={onNext}>
<Ionicons name="play-skip-forward" size={32} color="#fff" />
</TouchableOpacity>
</View>
</View>
);
}
const styles = StyleSheet.create({
container: { padding: 20 },
slider: { width: '100%', height: 40 },
timeRow: {
flexDirection: 'row',
justifyContent: 'space-between',
marginTop: -8,
},
timeText: { color: '#b3b3b3', fontSize: 12 },
controls: {
flexDirection: 'row',
justifyContent: 'center',
alignItems: 'center',
gap: 32,
marginTop: 16,
},
playButton: { marginHorizontal: 24 },
});
// Expected rendering:
// - A green seek bar that moves with playback progress
// - Elapsed and remaining time displayed in "M:SS" format
// - Previous, play/pause, and next buttons in a rowUsing onSlidingComplete instead of onValueChange prevents excessive seek operations while the user is still dragging the slider, resulting in smoother performance.
Playlist Management and Track Switching
A playlist feature is the heart of any music player. Here's how to manage track indices and handle next/previous navigation with repeat and shuffle modes:
// hooks/usePlaylist.ts
import { useState, useCallback } from 'react';
interface Track {
id: string;
title: string;
artist: string;
uri: string;
artwork?: string;
}
export function usePlaylist(tracks: Track[]) {
const [currentIndex, setCurrentIndex] = useState(0);
const [isShuffled, setIsShuffled] = useState(false);
const [repeatMode, setRepeatMode] = useState<'off' | 'all' | 'one'>('off');
const currentTrack = tracks[currentIndex] ?? null;
// Advance to the next track
const playNext = useCallback(() => {
setCurrentIndex((prev) => {
if (repeatMode === 'one') return prev; // Single track repeat
const next = prev + 1;
if (next >= tracks.length) {
return repeatMode === 'all' ? 0 : prev; // Loop or stop
}
return next;
});
}, [tracks.length, repeatMode]);
// Go back to the previous track
const playPrevious = useCallback(() => {
setCurrentIndex((prev) => {
const prevIndex = prev - 1;
if (prevIndex < 0) {
return repeatMode === 'all' ? tracks.length - 1 : 0;
}
return prevIndex;
});
}, [tracks.length, repeatMode]);
// Toggle shuffle mode
const toggleShuffle = useCallback(() => {
setIsShuffled((prev) => !prev);
}, []);
// Cycle repeat mode: off → all → one → off
const toggleRepeat = useCallback(() => {
setRepeatMode((prev) => {
if (prev === 'off') return 'all';
if (prev === 'all') return 'one';
return 'off';
});
}, []);
return {
currentTrack,
currentIndex,
isShuffled,
repeatMode,
playNext,
playPrevious,
toggleShuffle,
toggleRepeat,
};
}The repeat mode cycles through three states: off, repeat all, and repeat one. For a production-ready shuffle implementation, consider using the Fisher-Yates algorithm to randomize the track order while keeping the original list intact.
For more on animation techniques you can apply to your player transitions, check out the Rork Animation Implementation Guide.
Background Audio and Lock Screen Controls
Background audio is essential for any music player. Users expect their music to keep playing when they switch apps or lock their screen. Here's how to set it up for both platforms.
iOS Configuration
Add the audio background mode to your app.json (or app.config.js):
{
"expo": {
"ios": {
"infoPlist": {
"UIBackgroundModes": ["audio"]
}
}
}
}Android Configuration
Android uses foreground services to maintain background playback. In addition to the staysActiveInBackground: true setting, add the following permission:
{
"expo": {
"android": {
"permissions": ["FOREGROUND_SERVICE"]
}
}
}Lock Screen Media Controls
expo-av's Audio.Sound automatically integrates with the system media session for basic lock screen controls. However, if you want to display album artwork and track metadata on the lock screen, consider adopting react-native-track-player:
// For advanced media controls, consider react-native-track-player:
//
// import TrackPlayer from 'react-native-track-player';
//
// await TrackPlayer.setupPlayer();
// await TrackPlayer.add({
// id: 'track-1',
// url: 'https://example.com/song.mp3',
// title: 'Track Title',
// artist: 'Artist Name',
// artwork: 'https://example.com/artwork.jpg',
// });
// await TrackPlayer.play();react-native-track-player provides full support for lock screen controls, notification bars, Apple CarPlay, and Android Auto. It's the recommended upgrade path when you need production-grade media features.
For design patterns on building reusable custom hooks like the ones in this article, see the Rork Custom Hooks Guide.
Summary
In this guide, we walked through building a music player app with Rork and expo-av. We covered audio configuration, playback controls, seek bar UI implementation, playlist management with repeat and shuffle modes, and background audio setup for both iOS and Android.
The custom hooks we built — useAudioPlayer and usePlaylist — are designed to be reusable across projects. Feel free to adapt them to your own needs. If you want to take your app's user experience to the next level, the Rork UX Design Patterns Complete Guide is an excellent next read.
To deepen your understanding of the topics covered here,