当我通过媒体通知更改音乐时不要更新我的 UI

问题描述 投票:0回答:1

我正在使用 Media3 和 Jetpack Compose 创建音乐应用程序。当我通过可组合项 (PlayerControls) 更改歌曲时,我的 UI 会正确更新,显示艺术家、歌曲名称、图像和当前播放歌曲的其他详细信息。

但是,当我通过通知更改歌曲时,它确实切换正确,但我的 UI 仍然显示好像正在播放上一首歌曲。所以就不更新了。

当没有通过通知改变时,索引会改变,但是当通过通知改变时,索引保持不变。这就是为什么我的用户界面没有更新;它没有监听我通知中的事件。

我的歌曲数据类:

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
)

我的玩家状态:

enum class PlayerStates {
    STATE_IDLE,
    STATE_READY,
    STATE_BUFFERING,
    STATE_ERROR,
    STATE_END,
    STATE_PLAYING,
    STATE_PAUSE,
    STATE_CHANGE_SONG
}

我的玩家活动:

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) }
    }
}

我希望当通过通知(下一个/上一个索引)更改音乐时我的 UI 能够更新,就像在不使用通知的情况下更改音乐时一样。

kotlin service android-jetpack-compose media-player android-media3
1个回答
0
投票

您在 ViewModel 中收集

SongServiceHandler.mediaState
流。这可能会引入一些不良行为,可能就是您正在观察到的行为。

通常,ViewModel 应该只使用

mapLatest()
flatMapLatest()
等转换从存储库检索的流。只是 Compose 代码最终会在转换后的流上调用
collectAsStateWithLifecycle()
,仅在需要时触发它们的执行。这样,您就不会拥有任何因流订阅不一致而导致未正确更新时可能变得陈旧的中间状态对象。

您可以重构代码,以便拥有一个播放器服务,它是当前播放列表、设置(随机播放/重复)等内容的唯一事实来源;它从 ViewModel 或通知接收到的任何更改都会作为流发出。现在,ViewModel 只需要将这些流转换为您的 UI 可以使用的内容:这基本上就是

updateState()
当前所做的事情,除非您不希望更新任何变量作为副作用,相反,所需的所有内容都必须更新包含在返回值中。然后 ViewModel 可以根据此函数映射流。如果您需要将一个流转换为多个流,请确保底层流是
SharedFlow

这样你的 ViewModel 将不包含任何中间状态。毕竟,ViewModel 的职责是转换业务数据,以便 UI 可以轻松地显示它。更改此数据的请求将传递到存储库,它本身不需要更新任何状态。它本身应该存储(和更新)的唯一数据是 UI 相关状态,不能/不应该使用 Compose 存储。

© www.soinside.com 2019 - 2024. All rights reserved.