任务四 显示当前歌曲和播放状态
点击列表中的歌曲已经能够播放,但底部播放栏仍显示占位歌名和默认图标。接下来使用 PlaybackState 保存播放信息,通过 StateFlow 提供给 PlayerFragment,让播放栏显示当前歌曲的名称、封面和播放状态。
一、任务目标
任务结束时,页面将能够:
- 收到服务发布的当前歌曲和播放状态。
- 随之刷新歌曲名称、专辑封面和播放/暂停图标。
- 只在 Fragment 视图存活期间收集状态,页面销毁后不再更新控件。
二、设计播放状态
1. 创建 PlaybackState
在 model 包中新建 PlaybackState.kt,用一个数据类集中描述页面需要的播放信息。将状态模型与服务分开,服务负责发布状态,数据类负责定义状态内容。
package com.sziit.musicplayer.model
data class PlaybackState( val isPlaying: Boolean = false, val currentPosition: Int = 0, val duration: Int = 0, val currentSong: Song? = null)四个字段各管一件事:isPlaying 决定播放图标,currentSong 提供歌曲名称和封面;duration 在播放器准备完成时写入,currentPosition 到下一任务才定时更新,这两个时间字段先占位。所有字段都带默认值,服务刚创建、还没播放任何歌曲时,也能发出一个完整状态。
2. 创建可变状态流
在 MusicService 中导入 StateFlow,并增加一个私有可变流和一个公开只读流。StateFlow 由初始工程的 kotlinx-coroutines-android 依赖提供,不需要添加新依赖。
import com.sziit.musicplayer.model.PlaybackStateimport kotlinx.coroutines.flow.MutableStateFlowimport kotlinx.coroutines.flow.StateFlowimport kotlinx.coroutines.flow.asStateFlow
class MusicService : Service() { // ... 省略已有成员。
// 服务内部修改状态,页面通过只读流接收状态。 private val _playbackState = MutableStateFlow(PlaybackState()) val playbackState: StateFlow<PlaybackState> = _playbackState.asStateFlow()
// ... 省略服务中的其他方法。}服务内部用 _playbackState 修改状态,对外只暴露只读的 playbackState。页面能看不能改,播放器状态只有一个修改入口。
| 想一想 为什么要用 |
三、在 MusicService 中发布状态
1. 播放准备完成时发布歌曲信息
找到任务二写好的 setOnPreparedListener,在播放器启动后发布当前歌曲和播放状态。
class MusicService : Service() { // ... 省略其他成员和方法。
private fun playImpl(index: Int) { // ... 省略歌曲校验及播放器数据源设置。 player.setOnPreparedListener { mediaPlayer -> currentIndex = index mediaPlayer.start() // 播放器准备完成后,发布当前歌曲及播放状态。 _playbackState.value = PlaybackState( isPlaying = true, currentSong = song, duration = mediaPlayer.duration.coerceAtLeast(0) ) } }}总时长要等播放器准备完成后才能读到,所以在回调里把当前歌曲、播放状态和 duration 一次性设置好。
2. 歌曲播放完成时更新状态
在同一个 playImpl() 方法中,为播放器添加完成监听。歌曲自然结束后,将播放状态改为停止,播放栏图标随之恢复。
// ... 省略 package 声明和其他导入。import kotlinx.coroutines.flow.update
class MusicService : Service() { // ... 省略其他成员和方法。
private fun playImpl(index: Int) { // ... 省略歌曲校验、数据源设置和准备完成监听。 player.setOnCompletionListener { _playbackState.update { it.copy(isPlaying = false) } } }}copy() 只更新播放状态,当前歌曲和总时长会保留。
四、收集状态并更新页面
1. 添加 Glide 依赖
打开 app/build.gradle.kts,在 dependencies 中加入 Glide 依赖。
dependencies { // ... 省略已有依赖。 implementation("com.github.bumptech.glide:glide:4.16.0")}保存文件并等待 Gradle 同步完成。
2. 更新歌曲名称和封面
添加 updateSongInfo()。歌曲为空时不更新控件,避免服务刚创建时访问空对象。网络封面加载期间,Glide 先显示布局中已有的 ic_launcher_foreground 默认图,加载完成后再显示专辑封面。
// ... 省略 package 声明和已有导入。import com.bumptech.glide.Glideimport com.sziit.musicplayer.Rimport com.sziit.musicplayer.model.Song
class PlayerFragment : Fragment() { // ... 省略已有字段和其他方法。
private fun updateSongInfo(song: Song?) { // 服务尚未选中歌曲时,不更新歌曲信息。 song ?: return binding.tvSongTitle.text = song.name Glide.with(this) .load(song.imagePath) .placeholder(R.drawable.ic_launcher_foreground) .into(binding.imgPic) }}3. 更新播放按钮图标
updatePlayState() 根据服务状态在播放图标 ic_start 和暂停图标 ic_stop 之间切换。
// ... 省略 package 声明和已有导入。
class PlayerFragment : Fragment() { // ... 省略已有字段和其他方法。
private fun updatePlayState(isPlaying: Boolean) { // 播放时显示暂停图标,停止时显示播放图标。 val icon = if (isPlaying) { R.drawable.ic_stop } else { R.drawable.ic_start } binding.btnPlay.setBackgroundResource(icon) }}4. 使用视图生命周期收集状态
在 PlayerFragment 中添加收集方法。viewLifecycleOwner.lifecycleScope 会在 Fragment 视图销毁时自动取消协程,不会出现页面已经销毁还在更新控件的情况。lifecycleScope 来自初始工程的 lifecycle-runtime-ktx,launch 和 collectLatest 来自 kotlinx-coroutines-android,都不需要新增依赖。
// ... 省略 package 声明和已有导入。import androidx.lifecycle.lifecycleScopeimport kotlinx.coroutines.flow.collectLatestimport kotlinx.coroutines.launch
class PlayerFragment : Fragment() { // ... 省略已有字段和其他方法。
private fun observePlaybackState() { viewLifecycleOwner.lifecycleScope.launch { musicService?.playbackState?.collectLatest { state -> updateSongInfo(state.currentSong) updatePlayState(state.isPlaying) } } }}collectLatest 收到新状态后处理最新值。页面不需要自己保存“当前歌曲”或“是否播放”的副本,一切以服务发布的状态为准。
5. 在服务连接成功后开始收集
最后,在 PlayerFragment 的服务连接回调中调用 observePlaybackState()。服务绑定成功后,页面即可开始接收播放状态。
class PlayerFragment : Fragment() { // ... 省略已有字段。
private val serviceConnection = object : ServiceConnection { override fun onServiceConnected(name: ComponentName, service: IBinder) { musicService = (service as MusicService.MusicBinder).getService() observePlaybackState() }
// ... 省略已经完成的 onServiceDisconnected()。 }}歌曲开始播放后,播放栏信息和图标由服务发布的状态驱动。暂停、继续和切歌操作会在下一任务接入。
6. 验证状态同步
运行应用,点击歌曲开始播放,检查歌曲名称、专辑封面和播放图标是否随状态更新;播放自然结束后,图标应恢复。再点击其他歌曲,或离开后返回播放器页面,确认显示内容仍与服务状态一致。
若歌曲名称已更新但封面没有显示,检查 Glide 依赖是否已加入并完成 Gradle 同步。
任务小结
MusicService 用 PlaybackState 保存播放信息,并通过 StateFlow 提供给 PlayerFragment。播放栏收到状态后更新歌曲名称、封面和播放图标,视图销毁时停止收集。