Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Polling using kotlin coroutine with custom scope and view lifecycle

I am just starting with Kotlin coroutines. I am trying to poll the server using coroutine and want to stop polling when Activity or Fragment is paused and resume polling accordingly. So my pollScope has a shorter lifecycle than the one provided by ViewModel.viewModelScope. I am not fully satisfied with the implementation that I currently have, a few questions:

  1. Is this the right way to create pollScope. I want it to cancel when viewModelScope is canceled as well so that's why I am specifying parent job.
  2. Why coroutine doesn't start in onResume() if I cancel pollJobs using coroutineContext.cancel()? They start fine if I keep a list of jobs and cancel them.
  3. Is this the overall right approach? Is there a better way?
    import androidx.lifecycle.LifecycleOwner
    import androidx.lifecycle.ViewModel
    import androidx.lifecycle.viewModelScope
    import com.spruce.messenger.utils.FullLifecycleObserverAdapter
    import kotlinx.coroutines.*
    import java.io.IOException
    import java.util.concurrent.CopyOnWriteArrayList
    import kotlin.coroutines.CoroutineContext


    suspend fun poll(initialDelay: Long = 5000,
                     maxDelay: Long = 30000,
                     factor: Double = 2.0,
                     block: suspend () -> Unit) {

        var currentDelay = initialDelay
        while (true) {
            try {
                try {
                    block()
                    currentDelay = initialDelay
                } catch (e: IOException) {
                    currentDelay = (currentDelay * factor).toLong().coerceAtMost(maxDelay)
                }
                delay(currentDelay)
                yield()
            } catch (e: CancellationException) {
                break
            }
        }
    }


    class MyDataModel : ViewModel() {
        val pollScope = CloseableCoroutineScope(SupervisorJob(parent = viewModelScope.coroutineContext[Job]) + Dispatchers.Main)

        private val pollJobs = CopyOnWriteArrayList<Job>()

        inner class CloseableCoroutineScope(context: CoroutineContext) : FullLifecycleObserverAdapter(), CoroutineScope {
            override val coroutineContext: CoroutineContext = context

            override fun onPause(owner: LifecycleOwner) {
                super.onPause(owner)
                // coroutineContext.cancel() cancels it but then coroutine doesn't start again in onResume() thats why cancelling jobs instead
                pollJobs.forEach { it.cancel() }
            }

            override fun onResume(owner: LifecycleOwner) {
                super.onResume(owner)
                refresh()
            }
        }

        fun refresh() {
            if (pollJobs.count { it.isActive } == 0) {
                startPoll()
            }
        }

        private fun startPoll() = pollScope.launch {
            try {
                poll {
                    //fetch data from server
                }
            } catch (e: Exception) {
                //ignore
            }
        }.also {
            track(it)
        }

        private fun track(job: Job) {
            pollJobs.add(job)
            job.invokeOnCompletion {
                pollJobs.remove(job)
            }
        }
    }

Then in my fragment I add pollScope as lifecycle observer viewLifecycleOwner.lifecycle.addObserver(viewModel.pollScope).

like image 493
M-WaJeEh Avatar asked Aug 02 '19 11:08

M-WaJeEh


1 Answers

  1. Your pollScope seems fine to me.

  2. When you're cancelling a Job, it cancels all Coroutines of that Job.

  3. I would use a ViewModel with a CoroutineScope and then poll from it. Making sure to manage my Job and cancel my Coroutines when the VM dies.

class MyViewModel() : ViewModel(),
CoroutineScope by CoroutineScope(Dispatchers.Main + SupervisorJob()) {
    // ...
    override fun onCleared() {
        cancel()
        super.onCleared()
    }
}
like image 92
shkschneider Avatar answered Nov 13 '22 05:11

shkschneider