I'm currently learning how to build a Music App using compose and media3. onIsPlayingChanged(isPlaying: Boolean) in this method I'm using GlobalScope, Android studio give me this waring "This is a delicate API and its use requires care. Make sure you fully read and understand documentation of the declaration that is marked as a delicate API."
I tried to use another job variable, but I'm not sure how to do so. How to fix the code to follow Google android development best practices. this the code of 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)
}
}
this 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()
}
}
You can remove the Global Scope usage with some small changes. Have your MusicHandler
class implement CoroutineScope
class MusicServiceHandler(private val exoPlayer: Player) : Player.Listener, CoroutineScope
You then need to override the coroutineContext
variable
private val job = SupervisorJob()
override val coroutineContext: CoroutineContext
get() = Dispatchers.Main.immediate + job
Then change your GlobalScope usage to just launch
launch() {
startProgressUpdate()
}
Update your startProgressUpdate
as follows
private suspend fun startProgressUpdate() {
while (true) {
delay(500)
_musicStates.value = MusicStates.MediaProgress(exoPlayer.currentPosition)
}
}
If you want to further improve your code you should remove the while(true)
from startProgressUpdate
(its never good to have a while(true) loop)
private suspend fun startProgressUpdate() {
delay(500)
_musicStates.value = MusicStates.MediaProgress(exoPlayer.currentPosition)
}
and move it into the launch
launch() {
while(isActive){
startProgressUpdate()
}
}
this way when you cancel the job the while loop will also cancel since the scope is not active