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

然而,当我通过通知更改歌曲时,它确实可以正确切换,但我的用户界面仍然显示为正在播放上一首歌曲.因此,它没有更新.

如果未通过通知更改索引,则索引会更改,但当通过通知更改时,索引将保持不变.这就是为什么我的用户界面没有更新;它没有监听来self 的通知的事件.

我的宋代数据类:

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

我希望当通过通知(下一个/上一个索引)更改音乐时,我的用户界面会更新,就像不使用通知更改音乐时一样.

推荐答案

您可以在ViewModel中收集SongServiceHandler.mediaState个流.这可能会导致一些不受欢迎的行为,可能就是您正在观察的行为.

通常,ViewModel应该只转换它使用mapLatest()flatMapLatest()等从存储库中检索到的流.只有组合代码最终会对转换后的流调用collectAsStateWithLifecycle(),仅在需要时触发它们的执行.这样,您就不会有任何中间状态对象,当不一致的流订阅导致没有正确更新时,它们可能会变得陈旧.

您可以重构您的代码,以便拥有一个播放器服务,它是当前播放列表、设置(随机/重复)等内容的唯一真实来源;它从ViewModel或Notify接收到的任何更改都作为流发出.现在,ViewModel只需要将这些流转换为您的UI可以使用的内容:这基本上就是updateState()目前所做的事情,除非您不希望任何变量作为副作用更新,相反,所需的一切都必须包含在返回值中.然后,ViewModel可以根据此函数映射流.如果您需要将一个流转换为多个流,请确保底层流为SharedFlow.

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

这是一个更大的重构,但它可能会在此过程中解决您当前的问题.

Kotlin相关问答推荐

Lambda和普通Kotlin函数有什么区别?

最好的方法来创建一个 map 在kotlin从两个列表

为什么";";.equals(1)在柯特林语中是有效的,但";";=1是无效的?

用于将 0.5 变为 0 的 round() 函数的模拟

区分函数和扩展

第二个协程永远不会执行

在 Compose 中使用 Text() 时如何获取文本的大小?

如何限制 Kotlin 中的枚举?

Spring webflux bean验证不起作用

将 Gradle 子元素与 Kotlin 多平台一起使用

TextField maxLength - Android Jetpack Compose

如何在使用 Gradle 的 AppEngine 项目中使用 Kotlin

如何将 Kotlin 日期中的字符串或时间戳格式化为指定的首选格式?

Android插件2.2.0-alpha1无法使用Kotlin编译

Kotlin/JS,Gradle 插件:无法加载@webpack-cli/serve命令

如何从kotlin中的类实例化对象

Kotlin reflect proguard SmallSortedMap

Kotlin中的Memoization功能

我应该使用Kotlin数据类作为JPA实体吗?

var str:String是可变的还是不可变的?