我目前正在学习如何使用 compose 和 media3 构建音乐应用程序。 onIsPlayingChanged(isPlaying: Boolean) 在这个方法中我使用 GlobalScope,Android studio 给我这个警告“这是一个微妙的 API,它的使用需要小心。确保你完全阅读并理解标记为微妙的声明的文档API。”
我尝试使用另一个作业变量,但我不知道该怎么做。如何修复代码以遵循 Google android 开发最佳实践。 这是 MusicServiceHandler 的代码
import androidx.media3.common.MediaItem
import androidx.media3.common.Player
import androidx.media3.exoplayer.ExoPlayer
import com.techullurgy.media3musicplayer.utils.MediaStateEvents
import com.techullurgy.media3musicplayer.utils.MusicStates
import kotlinx.coroutines.DelicateCoroutinesApi
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.Job
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch
class MusicServiceHandler(
private val exoPlayer: Player,
) : Player.Listener {
private val _musicStates: MutableStateFlow<MusicStates> = MutableStateFlow(MusicStates.Initial)
val musicStates: StateFlow<MusicStates> = _musicStates.asStateFlow()
private var job: Job? = null
init {
exoPlayer.addListener(this)
}
fun setMediaItem(mediaItem: MediaItem) {
exoPlayer.setMediaItem(mediaItem)
exoPlayer.prepare()
}
fun setMediaItemList(mediaItems: List<MediaItem>) {
exoPlayer.setMediaItems(mediaItems)
exoPlayer.prepare()
}
suspend fun onMediaStateEvents(
mediaStateEvents: MediaStateEvents,
selectedMusicIndex: Int = -1,
seekPosition: Long = 0,
) {
when (mediaStateEvents) {
MediaStateEvents.Backward -> exoPlayer.seekBack()
MediaStateEvents.Forward -> exoPlayer.seekForward()
MediaStateEvents.PlayPause -> playPauseMusic()
MediaStateEvents.SeekTo -> exoPlayer.seekTo(seekPosition)
MediaStateEvents.SeekToNext -> exoPlayer.seekToNext()
MediaStateEvents.SeekToPrevious -> exoPlayer.seekToPrevious()
MediaStateEvents.Stop -> stopProgressUpdate()
MediaStateEvents.SelectedMusicChange -> {
when (selectedMusicIndex) {
exoPlayer.currentMediaItemIndex -> {
playPauseMusic()
}
else -> {
exoPlayer.seekToDefaultPosition(selectedMusicIndex)
_musicStates.value = MusicStates.MediaPlaying(
isPlaying = true
)
exoPlayer.playWhenReady = true
startProgressUpdate()
}
}
}
is MediaStateEvents.MediaProgress -> {
exoPlayer.seekTo(
(exoPlayer.duration * mediaStateEvents.progress).toLong()
)
}
}
}
override fun onPlaybackStateChanged(playbackState: Int) {
when (playbackState) {
ExoPlayer.STATE_BUFFERING -> _musicStates.value =
MusicStates.MediaBuffering(exoPlayer.currentPosition)
ExoPlayer.STATE_READY -> _musicStates.value = MusicStates.MediaReady(exoPlayer.duration)
Player.STATE_ENDED -> {
// no-op
}
Player.STATE_IDLE -> {
// no-op
}
}
}
@OptIn(DelicateCoroutinesApi::class)
override fun onIsPlayingChanged(isPlaying: Boolean) {
_musicStates.value = MusicStates.MediaPlaying(isPlaying = isPlaying)
_musicStates.value = MusicStates.CurrentMediaPlaying(exoPlayer.currentMediaItemIndex)
if (isPlaying) {
GlobalScope.launch(Dispatchers.Main) {
startProgressUpdate()
}
} else {
stopProgressUpdate()
}
}
private suspend fun playPauseMusic() {
if (exoPlayer.isPlaying) {
exoPlayer.pause()
stopProgressUpdate()
} else {
exoPlayer.play()
_musicStates.value = MusicStates.MediaPlaying(
isPlaying = true
)
startProgressUpdate()
}
}
private suspend fun startProgressUpdate() = job.run {
while (true) {
delay(500)
_musicStates.value = MusicStates.MediaProgress(exoPlayer.currentPosition)
}
}
private fun stopProgressUpdate() {
job?.cancel()
_musicStates.value = MusicStates.MediaPlaying(isPlaying = false)
}
}
这个 MusicViewModel
@OptIn(SavedStateHandleSaveableApi::class)
class MusicViewModel(
savedStateHandle: SavedStateHandle,
) : ViewModel(), KoinComponent {
private val musicServiceHandler: MusicServiceHandler by inject<MusicServiceHandler>()
private val repository: MusicRepository by inject<MusicRepository>()
private var duration by savedStateHandle.saveable { mutableLongStateOf(0L) }
var progress by savedStateHandle.saveable { mutableFloatStateOf(0f) }
private var progressValue by savedStateHandle.saveable { mutableStateOf("00:00") }
var isMusicPlaying by savedStateHandle.saveable { mutableStateOf(false) }
var currentSelectedMusic by mutableStateOf(
AudioItem(
0L,
"".toUri(),
"",
"",
0,
"",
"",
null
)
)
var musicList by mutableStateOf(listOf<AudioItem>())
private val _homeUiState: MutableStateFlow<HomeUIState> =
MutableStateFlow(HomeUIState.InitialHome)
val homeUIState: StateFlow<HomeUIState> = _homeUiState.asStateFlow()
init {
getMusicData()
}
init {
viewModelScope.launch {
musicServiceHandler.musicStates.collectLatest { musicStates: MusicStates ->
when (musicStates) {
MusicStates.Initial -> _homeUiState.value = HomeUIState.InitialHome
is MusicStates.MediaBuffering -> progressCalculation(musicStates.progress)
is MusicStates.MediaPlaying -> isMusicPlaying = musicStates.isPlaying
is MusicStates.MediaProgress -> progressCalculation(musicStates.progress)
is MusicStates.CurrentMediaPlaying -> {
currentSelectedMusic = musicList[musicStates.mediaItemIndex]
}
is MusicStates.MediaReady -> {
duration = musicStates.duration
_homeUiState.value = HomeUIState.HomeReady
}
}
}
}
}
fun onHomeUiEvents(homeUiEvents: HomeUiEvents) = viewModelScope.launch {
when (homeUiEvents) {
HomeUiEvents.Backward -> musicServiceHandler.onMediaStateEvents(MediaStateEvents.Backward)
HomeUiEvents.Forward -> musicServiceHandler.onMediaStateEvents(MediaStateEvents.Forward)
HomeUiEvents.SeekToNext -> musicServiceHandler.onMediaStateEvents(MediaStateEvents.SeekToNext)
HomeUiEvents.SeekToPrevious -> musicServiceHandler.onMediaStateEvents(MediaStateEvents.SeekToPrevious)
is HomeUiEvents.PlayPause -> {
musicServiceHandler.onMediaStateEvents(
MediaStateEvents.PlayPause
)
}
is HomeUiEvents.SeekTo -> {
musicServiceHandler.onMediaStateEvents(
MediaStateEvents.SeekTo,
seekPosition = ((duration * homeUiEvents.position) / 100f).toLong()
)
}
is HomeUiEvents.CurrentAudioChanged -> {
musicServiceHandler.onMediaStateEvents(
MediaStateEvents.SelectedMusicChange,
selectedMusicIndex = homeUiEvents.index
)
}
is HomeUiEvents.UpdateProgress -> {
musicServiceHandler.onMediaStateEvents(
MediaStateEvents.MediaProgress(
homeUiEvents.progress
)
)
progress = homeUiEvents.progress
}
}
}
private fun getMusicData() {
viewModelScope.launch {
val musicData = repository.getAudioData()
musicList = musicData
setMusicItems()
}
}
private fun setMusicItems() {
musicList.map { audioItem ->
MediaItem.Builder()
.setUri(audioItem.uri)
.setMediaMetadata(
MediaMetadata.Builder()
.setAlbumArtist(audioItem.artist)
.setDisplayTitle(audioItem.title)
.setSubtitle(audioItem.displayName)
.build()
)
.build()
}.also {
musicServiceHandler.setMediaItemList(it)
}
}
private fun progressCalculation(currentProgress: Long) {
progress =
if (currentProgress > 0) ((currentProgress.toFloat() / duration.toFloat()) * 100f)
else 0f
progressValue = formatDurationValue(currentProgress)
}
private fun formatDurationValue(duration: Long): String {
val minutes = MINUTES.convert(duration, MILLISECONDS)
val seconds = (minutes) - minutes * SECONDS.convert(1, MINUTES)
return String.format("%02d:%02d", minutes, seconds)
}
override fun onCleared() {
viewModelScope.launch {
musicServiceHandler.onMediaStateEvents(MediaStateEvents.Stop)
}
super.onCleared()
}
}
您可以通过一些小的更改来删除全局范围的使用。让您的
MusicHandler
班级实现 CoroutineScope
class MusicServiceHandler(private val exoPlayer: Player) : Player.Listener, CoroutineScope
然后您需要覆盖
coroutineContext
变量
private val job = SupervisorJob()
override val coroutineContext: CoroutineContext
get() = Dispatchers.Main.immediate + job
然后将您的 GlobalScope 使用更改为仅启动
launch() {
startProgressUpdate()
}
按如下方式更新您的
startProgressUpdate
private suspend fun startProgressUpdate() {
while (true) {
delay(500)
_musicStates.value = MusicStates.MediaProgress(exoPlayer.currentPosition)
}
}
如果你想进一步改进你的代码,你应该从
while(true)
中删除 startProgressUpdate
(有 while(true) 循环从来都不是好事)
private suspend fun startProgressUpdate() {
delay(500)
_musicStates.value = MusicStates.MediaProgress(exoPlayer.currentPosition)
}
并将其移至发布中
launch() {
while(isActive){
startProgressUpdate()
}
}
这样,当您取消作业时,while 循环也会取消,因为作用域未处于活动状态