Search code examples
androidkotlinandroid-jetpack-composekotlin-coroutinesandroid-media3

how to fix this warning "GlobalScope, This is a delicate API and its use requires care"?


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

}


Solution

  • 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