我们将使用现代 Android 开发技术栈,包括 Kotlin、Jetpack Compose(UI)、ExoPlayer(核心播放引擎)和 WorkManager(后台播放)。

核心技术选型
为什么选择这些技术?
- ExoPlayer: 这是 Google 官方推荐的高级媒体播放器,它功能强大、高度可定制、性能优异,并且支持各种媒体格式(包括 HLS, DASH)、DRM 和广告,相比
MediaPlayer,ExoPlayer 是异步的,不会阻塞 UI 线程,是网络播放的不二之选。 - Jetpack Compose: 现代 Android UI 工具包,让我们可以用更声明式、更简洁的代码来构建 UI,减少样板代码。
- WorkManager: 用于处理后台任务,当用户将应用退到后台或锁屏时,我们需要让音乐继续播放,WorkManager 可以确保播放服务在后台稳定运行,并且能很好地处理系统资源回收(如低内存情况)。
- MediaPlayerService: 一个前台服务,用于在后台播放音乐,使用前台服务可以防止应用被系统杀死,并且必须在状态栏显示一个通知。
- MediaSession: 这是连接 UI 和播放服务的桥梁,它允许你通过锁屏、通知、甚至是蓝牙耳机来控制播放,MediaSession 会创建一个标准的媒体控制器,其他应用(如 Android Auto)可以通过它来与你的播放器交互。
项目结构
一个清晰的项目结构有助于代码维护:
com.example.musicplayer/
├── data/
│ ├── model/ // 数据模型,如 Song, Playlist
│ └── repository/ // 数据仓库,负责从网络获取数据
├── di/ // 依赖注入 (可选,但推荐)
├── service/
│ └── MusicService.kt // 音乐播放服务
├── ui/
│ ├── theme/ // Compose 主题
│ └── screen/
│ ├── HomeScreen.kt // 主界面,显示播放列表
│ └── PlayerScreen.kt // 播放器界面
├── util/
│ └ Constants.kt // 常量,如 API URL
└── MainActivity.kt // 入口 Activity
实现步骤
添加依赖
在 app/build.gradle.kts (或 build.gradle) 文件中添加必要的依赖:
// build.gradle.kts
dependencies {
// ExoPlayer
implementation("androidx.media3:media3-exoplayer:1.4.1") // 核心播放器
implementation("androidx.media3:media3-exoplayer-hls:1.4.1") // 支持 HLS 流
implementation("androidx.media3:media3-ui:1.4.1") // ExoPlayer 的默认 UI 组件
// WorkManager
implementation("androidx.work:work-runtime-ktx:2.9.1")
// 其他 Jetpack Compose 依赖...
}
创建数据模型
定义一个简单的歌曲数据类。

// data/model/Song.kt
data class Song(
val id: String,
val title: String,
val artist: String,
val streamUrl: String, // 网络音频流地址
val coverUrl: String? = null, // 封面图片地址
val duration: Long = 0 // 歌曲时长 (毫秒)
)
创建 MusicService (核心)
这是整个播放器的核心,它负责播放逻辑、处理 MediaSession 的回调,并管理通知。
// service/MusicService.kt
class MusicService : Service(), Player.Listener {
private lateinit var exoPlayer: ExoPlayer
private lateinit var mediaSession: MediaSessionCompat
private lateinit var notificationBuilder: NotificationCompat.Builder
private var isForegroundService = false
// 当前播放列表和索引
private var playList: List<Song> = emptyList()
private var currentIndex = 0
// 从 Intent 中获取数据
private fun getSongFromIntent(intent: Intent?): Song? {
return intent?.getParcelableExtra("SONG")
}
override fun onCreate() {
super.onCreate()
initializePlayer()
initializeMediaSession()
initializeNotification()
}
private fun initializePlayer() {
exoPlayer = ExoPlayer.Builder(this).build().apply {
addListener(this@MusicService)
setMediaItem(MediaItem.fromUri(getSongFromIntent(intent)?.streamUrl ?: ""))
prepare()
}
}
private fun initializeMediaSession() {
val mediaButtonIntent = Intent(Intent.ACTION_MEDIA_BUTTON).apply {
setClass(this@MusicService, MediaButtonReceiver::class.java)
}
val mediaButtonReceiverPendingIntent = PendingIntent.getBroadcast(
this, 0, mediaButtonIntent, PendingIntent.FLAG_IMMUTABLE
)
mediaSession = MediaSessionCompat(this, "MusicService").apply {
setCallback(object : MediaSessionCompat.Callback() {
override fun onPlay() {
exoPlayer.play()
}
override fun onPause() {
exoPlayer.pause()
}
override fun onSkipToNext() {
// ... 实现下一首逻辑
}
override fun onSkipToPrevious() {
// ... 实现上一首逻辑
}
override fun onStop() {
stopSelf()
}
})
setPlaybackState(PlaybackStateCompat.STATE_PLAYING)
setActive(true)
setMediaButtonReceiver(mediaButtonReceiverPendingIntent)
}
}
private fun initializeNotification() {
val notificationIntent = Intent(this, MainActivity::class.java)
val pendingIntent = PendingIntent.getActivity(
this, 0, notificationIntent, PendingIntent.FLAG_IMMUTABLE
)
notificationBuilder = NotificationCompat.Builder(this, "music_channel")
.setSmallIcon(R.drawable.ic_music_note) // 替换为你自己的图标
.setContentTitle("正在播放")
.setContentText("艺术家")
.setContentIntent(pendingIntent)
.setStyle(androidx.media3.session.MediaStyleCompat.Builder(mediaSession)
.setMediaSession(mediaSession)
.setShowActionsInCompactView(0, 1, 2) // 设置通知栏紧凑模式下的按钮
.build())
.setPriority(NotificationCompat.PRIORITY_DEFAULT)
.setOnlyAlertOnce(true)
}
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
val song = getSongFromIntent(intent)
if (song != null) {
// 更新播放列表和当前歌曲
playList = listOf(song)
currentIndex = 0
exoPlayer.setMediaItem(MediaItem.fromUri(song.streamUrl))
exoPlayer.prepare()
exoPlayer.play()
}
// 启动为前台服务,显示通知
if (!isForegroundService) {
startForeground(1, notificationBuilder.build())
isForegroundService = true
}
return START_STICKY
}
override fun onPlaybackStateChanged(state: Player.State) {
super.onPlaybackStateChanged(state)
when (state) {
Player.STATE_READY -> {
updateNotification()
}
Player.STATE_ENDED -> {
// 播放结束,处理下一首或停止
}
else -> {}
}
}
private fun updateNotification() {
val song = playList.getOrNull(currentIndex)
if (song != null) {
notificationBuilder.setContentTitle(song.title)
notificationBuilder.setContentText(song.artist)
// 更新通知
val notificationManager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
notificationManager.notify(1, notificationBuilder.build())
}
}
override fun onDestroy() {
super.onDestroy()
mediaSession.release()
exoPlayer.release()
}
override fun onBind(intent: Intent?): IBinder? {
return null // 我们不绑定服务,通过 MediaSession 控制
}
}
注意: 你还需要在 AndroidManifest.xml 中声明这个服务,并请求必要的权限。
<!-- AndroidManifest.xml -->
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.WAKE_LOCK" /> <!-- 防止屏幕休眠导致播放暂停 -->
<application ...>
<service android:name=".service.MusicService" />
</application>
创建 UI (使用 Jetpack Compose)
PlayerScreen.kt
@Composable
fun PlayerScreen(song: Song, onBack: () -> Unit) {
val context = LocalContext.current
var isPlaying by remember { mutableStateOf(false) }
// 获取 MediaController 来控制播放
val mediaController = rememberMediaController()
LaunchedEffect(song) {
mediaController.setSessionToken(song.sessionToken) // 假设 Song 类有 sessionToken
}
Column(
modifier = Modifier
.fillMaxSize()
.padding(16.dp),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center
) {
// 封面图片
Image(
painter = rememberAsyncImagePainter(song.coverUrl),
contentDescription = null,
modifier = Modifier.size(250.dp)
)
Spacer(modifier = Modifier.height(32.dp))
// 歌曲信息
Text(text = song.title, style = MaterialTheme.typography.h5)
Text(text = song.artist, style = MaterialTheme.typography.body1)
Spacer(modifier = Modifier.height(48.dp))
// 播放/暂停按钮
IconButton(onClick = {
if (isPlaying) {
mediaController.pause()
} else {
mediaController.play()
}
isPlaying = !isPlaying
}) {
Icon(
imageVector = if (isPlaying) Icons.Filled.Pause else Icons.Filled.PlayArrow,
contentDescription = if (isPlaying) "Pause" else "Play",
modifier = Modifier.size(64.dp)
)
}
}
}
// 辅助函数:获取 MediaController
@Composable
fun rememberMediaController(): MediaController {
val context = LocalContext.current
val mediaController = remember { MediaController(context) }
return mediaController
}
如何启动服务和 UI?

当用户点击一首歌时,你需要:
-
启动服务:
val intent = Intent(context, MusicService::class.java).apply { putExtra("SONG", selectedSong) } ContextCompat.startForegroundService(context, intent) -
导航到 PlayerScreen,并将歌曲信息传递过去。
处理后台播放和 WorkManager
当应用完全退出或锁屏时,UI 不可见,但服务仍在运行,ExoPlayer 和 MediaSession 会确保通知和控制依然可用。
WorkManager 在这里主要用于更复杂的后台任务,
- 定期检查更新:检查用户喜欢的歌曲是否有新版本。
- 下载播放列表:如果需要离线播放,可以使用 WorkManager 来管理下载任务。
对于单纯的保持播放,ForegroundService 已经足够强大,但如果你想做一些“后台任务”,WorkManager 是更好的选择,因为它更省电且更可靠。
处理耳机和蓝牙事件
为了让音乐在耳机拔出时自动暂停,可以在 MusicService 中注册一个 BroadcastReceiver:
// 在 MusicService 的 onCreate() 中注册
private var headsetPlugReceiver: BroadcastReceiver? = null
private fun registerHeadsetPlugReceiver() {
headsetPlugReceiver = object : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
if (intent.action == Intent.ACTION_HEADSET_PLUG) {
val state = intent.getIntExtra("state", -1)
if (state == 0) { // 拔出
exoPlayer.pause()
}
}
}
}
registerReceiver(headsetPlugReceiver, IntentFilter(Intent.ACTION_HEADSET_PLUG))
}
// 在 onDestroy() 中注销
override fun onDestroy() {
super.onDestroy()
headsetPlugReceiver?.let { unregisterReceiver(it) }
// ... 其他清理代码
}
总结与最佳实践
- 始终使用 ExoPlayer: 不要再使用
MediaPlayer来处理网络流媒体。 - 使用 MediaSession: 这是实现标准媒体控制(通知、锁屏、车载)的关键。
- 使用 ForegroundService: 这是保证后台播放稳定性的标准做法。
- UI 与服务解耦: UI 通过
MediaController与MusicService交互,而不是直接绑定,这样 UI 可以随时被销毁而不会影响播放。 - 错误处理: ExoPlayer 会抛出各种异常(如网络错误、解析错误),务必在
Player.Listener中处理它们,并向用户展示友好的提示。 - 资源管理: 在
onDestroy()中正确释放ExoPlayer和MediaSession的资源,避免内存泄漏。 - 性能优化: 对于封面图片等网络资源,使用
Glide或Coil这样的图片加载库,它们能很好地处理缓存和内存管理。
通过以上步骤,你就可以构建一个功能完善、体验流畅的 Android 网络音乐播放器了,这只是一个基础框架,你可以在此基础上添加播放列表、均衡器、歌词同步等更高级的功能。
