Media3+Jetpack Compose音乐应用:通知切歌UI不更新问题求助
问题:Media3 + Jetpack Compose 音乐应用中,系统通知切换歌曲时UI不更新
使用Media3和Jetpack Compose开发音乐应用,通过自定义Composable组件(PlayerControls)切换歌曲时,UI能正常更新当前歌曲的歌手、歌名、封面等信息;但通过系统通知切换歌曲时,歌曲切换成功,但UI仍显示上一首歌曲的状态,未同步更新。
排查发现:非通知方式切歌时selectedSongIndex会变化,而通知切歌时索引保持不变,核心原因是UI未监听通知触发的播放事件。
以下是相关代码实现:
Song 数据类
data class Song( val mediaId: String = "", val artist: String = "", val songName: String = "", val songUrl: String = "", val imageUrl: String = "", var isSelected: Boolean = false, var state: PlayerStates = PlayerStates.STATE_IDLE )
PlayerStates 枚举
enum class PlayerStates { STATE_IDLE, STATE_READY, STATE_BUFFERING, STATE_ERROR, STATE_END, STATE_PLAYING, STATE_PAUSE, STATE_CHANGE_SONG }
PlayerEvents 接口
interface PlayerEvents { fun onPlayPauseClick() fun onPreviousClick() fun onNextClick() fun onSongClick(song: Song) fun onSeekBarPositionChanged(position: Long) }
SongServiceHandler 类
class SongServiceHandler @Inject constructor( private val player: ExoPlayer ) : Player.Listener { val mediaState = MutableStateFlow(PlayerStates.STATE_IDLE) val currentPlaybackPosition: Long get() = if (player.currentPosition > 0) player.currentPosition else 0L val currentSongDuration: Long get() = if (player.duration > 0) player.duration else 0L private var job: Job? = null init { player.addListener(this) job = Job() } fun initPlayer(songList: MutableList<MediaItem>) { player.setMediaItems(songList) player.prepare() } fun setUpSong(index: Int, isSongPlay: Boolean) { if (player.playbackState == Player.STATE_IDLE) player.prepare() player.seekTo(index, 0) if (isSongPlay) player.playWhenReady = true Log.d("service", "fun setUpSong()") } fun playPause() { if (player.playbackState == Player.STATE_IDLE) player.prepare() player.playWhenReady = !player.playWhenReady } fun releasePlayer() { player.release() } fun seekToPosition(position: Long) { player.seekTo(position) } override fun onPlayerError(error: PlaybackException) { super.onPlayerError(error) mediaState.tryEmit(PlayerStates.STATE_ERROR) Log.d("service", "override fun onPlayerError(error = ${mediaState.value})") } override fun onPlayWhenReadyChanged(playWhenReady: Boolean, reason: Int) { if (player.playbackState == Player.STATE_READY) { if (playWhenReady) { mediaState.tryEmit(PlayerStates.STATE_PLAYING) } else { mediaState.tryEmit(PlayerStates.STATE_PAUSE) } } } override fun onMediaItemTransition(mediaItem: MediaItem?, reason: Int) { super.onMediaItemTransition(mediaItem, reason) if (reason == MEDIA_ITEM_TRANSITION_REASON_AUTO) { mediaState.tryEmit(PlayerStates.STATE_CHANGE_SONG) mediaState.tryEmit(PlayerStates.STATE_PLAYING) } } override fun onPlaybackStateChanged(playbackState: Int) { when (playbackState) { Player.STATE_IDLE -> { mediaState.tryEmit(PlayerStates.STATE_IDLE) } Player.STATE_BUFFERING -> { mediaState.tryEmit(PlayerStates.STATE_BUFFERING) } Player.STATE_READY -> { mediaState.tryEmit(PlayerStates.STATE_READY) if (player.playWhenReady) { mediaState.tryEmit(PlayerStates.STATE_PLAYING) } else { mediaState.tryEmit(PlayerStates.STATE_PAUSE) } } Player.STATE_ENDED -> { mediaState.tryEmit(PlayerStates.STATE_END) } } Log.d("service", "override fun onPlaybackStateChanged(playbackState = $playbackState)") } }
扩展函数
fun MutableList<Song>.resetSongs() { this.forEach { song -> song.isSelected = false song.state = PlayerStates.STATE_IDLE } } fun CoroutineScope.collectPlayerState( songServiceHandler: SongServiceHandler, updateState: (PlayerStates) -> Unit ) { this.launch { songServiceHandler.mediaState.collect { updateState(it) } } } fun CoroutineScope.launchPlaybackStateJob( playbackStateFlow: MutableStateFlow<PlaybackState>, state: PlayerStates, songServiceHandler: SongServiceHandler ) = launch { do { playbackStateFlow.emit( PlaybackState( currentPlaybackPosition = songServiceHandler.currentPlaybackPosition, currentSongDuration = songServiceHandler.currentSongDuration ) ) delay(1000) } while (state == PlayerStates.STATE_PLAYING && isActive) }
SongViewModel 类
@HiltViewModel class SongViewModel @Inject constructor( private val songServiceHandler: SongServiceHandler, private val repository: SongRepository ) : ViewModel(), PlayerEvents { private val _songs = mutableStateListOf<Song>() val songs: List<Song> get() = _songs private var isSongPlay: Boolean = false var selectedSong: Song? by mutableStateOf(null) private set private var selectedSongIndex: Int by mutableStateOf(-1) private val _playbackState = MutableStateFlow(PlaybackState(0L, 0L)) val playbackState: StateFlow<PlaybackState> get() = _playbackState var isServiceRunning = false private var playbackStateJob: Job? = null private var isAuto: Boolean = false init { viewModelScope.launch { loadData() observePlayerState() } } private fun loadData() = viewModelScope.launch { _songs.addAll(repository.getAllSongs()) songServiceHandler.initPlayer( _songs.map { song -> MediaItem.Builder() .setMediaId(song.mediaId) .setUri(song.songUrl.toUri()) .setMediaMetadata( MediaMetadata.Builder() .setTitle(song.songName) .setArtist(song.artist) .setArtworkUri(song.imageUrl.toUri()) .build() ).build() }.toMutableList() ) } private fun onSongSelected(index: Int) { if (selectedSongIndex == -1) isSongPlay = true if (selectedSongIndex == -1 || selectedSongIndex != index) { _songs.resetSongs() selectedSongIndex = index setUpSong() } } private fun setUpSong() { if (!isAuto) { songServiceHandler.setUpSong( selectedSongIndex, isSongPlay ) isAuto = false } } private fun updateState(state: PlayerStates) { if (selectedSongIndex != -1) { isSongPlay = state == PlayerStates.STATE_PLAYING _songs[selectedSongIndex].state = state _songs[selectedSongIndex].isSelected = true selectedSong = null selectedSong = _songs[selectedSongIndex] updatePlaybackState(state) if (state == PlayerStates.STATE_CHANGE_SONG) { isAuto = true onNextClick() } if (state == PlayerStates.STATE_END) { onSongSelected(0) } } } private fun updatePlaybackState(state: PlayerStates) { playbackStateJob?.cancel() playbackStateJob = viewModelScope .launchPlaybackStateJob( _playbackState, state, songServiceHandler ) } private fun observePlayerState() { viewModelScope.collectPlayerState(songServiceHandler, ::updateState) } override fun onCleared() { super.onCleared() songServiceHandler.releasePlayer() } override fun onPlayPauseClick() { songServiceHandler.playPause() } override fun onPreviousClick() { if (selectedSongIndex > 0) { onSongSelected(selectedSongIndex - 1) } } override fun onNextClick() { if (selectedSongIndex < _songs.size - 1) { onSongSelected(selectedSongIndex + 1) } } override fun onSongClick(song: Song) { onSongSelected(_songs.indexOf(song)) } override fun onSeekBarPositionChanged(position: Long) { viewModelScope.launch { songServiceHandler.seekToPosition(position) } } }
解决方案
问题核心是系统通知切歌时,SongViewModel中的selectedSongIndex未同步更新——通知触发的切歌直接操作ExoPlayer的previous()/next()方法,但现有代码只监听了自动切歌的事件,未覆盖手动切歌(含通知触发)的场景。
1. 扩展SongServiceHandler的媒体项监听逻辑
添加状态流暴露当前播放的媒体项索引,同时修改onMediaItemTransition覆盖所有切歌场景:
// 在SongServiceHandler中新增状态流 val currentMediaItemIndex = MutableStateFlow(-1) override fun onMediaItemTransition(mediaItem: MediaItem?, reason: Int) { super.onMediaItemTransition(mediaItem, reason) // 同步当前播放的媒体项索引 currentMediaItemIndex.tryEmit(player.currentMediaItemIndex) // 所有切歌场景都发送状态更新 mediaState.tryEmit(PlayerStates.STATE_CHANGE_SONG) mediaState.tryEmit(if (player.playWhenReady) PlayerStates.STATE_PLAYING else PlayerStates.STATE_PAUSE) } // 初始化和手动切歌后同步索引 fun initPlayer(songList: MutableList<MediaItem>) { player.setMediaItems(songList) player.prepare() currentMediaItemIndex.tryEmit(player.currentMediaItemIndex) } fun setUpSong(index: Int, isSongPlay: Boolean) { if (player.playbackState == Player.STATE_IDLE) player.prepare() player.seekTo(index, 0) if (isSongPlay) player.playWhenReady = true currentMediaItemIndex.tryEmit(index) Log.d("service", "fun setUpSong()") }
2. 在SongViewModel中监听索引变化
添加对currentMediaItemIndex的监听,索引变化时同步更新UI状态:
init { viewModelScope.launch { loadData() observePlayerState() // 新增:监听媒体项索引变化 songServiceHandler.currentMediaItemIndex.collect { newIndex -> if (newIndex != -1 && newIndex != selectedSongIndex) { selectedSongIndex = newIndex updateSelectedSongUI() } } } } // 新增方法:更新选中歌曲的UI状态 private fun updateSelectedSongUI() { _songs.resetSongs() val currentState = songServiceHandler.mediaState.value _songs[selectedSongIndex].apply { isSelected = true state = if (currentState == PlayerStates.STATE_PLAYING) PlayerStates.STATE_PLAYING else PlayerStates.STATE_PAUSE } selectedSong = _songs[selectedSongIndex] updatePlaybackState(currentState) }
3. 优化updateState方法的自动切歌逻辑
移除原逻辑中通过STATE_CHANGE_SONG触发onNextClick()的代码,避免重复操作:
private fun updateState(state: PlayerStates) { if (selectedSongIndex != -1) { isSongPlay = state == PlayerStates.STATE_PLAYING _songs[selectedSongIndex].state = state _songs[selectedSongIndex].isSelected = true selectedSong = _songs[selectedSongIndex] updatePlaybackState(state) // 移除自动切歌触发逻辑 if (state == PlayerStates.STATE_END) { onSongSelected(0) } } }
4. 确保通知切歌直接调用ExoPlayer方法
在媒体服务的MediaSession.Callback中,处理通知事件时直接调用player的对应方法:
override fun onSkipToPrevious() { player.previous() } override fun onSkipToNext() { player.next() }
修改后,无论通过UI组件还是系统通知切歌,selectedSongIndex都会同步更新,UI也会自动刷新当前歌曲信息。
内容的提问来源于stack exchange,提问作者LucasMilhan
相关产品推荐
相关产品推荐

