Bradleycorn
02/18/2021, 4:36 PMSimplePoller
class that will run a block: suspend () -> Unit
on a given interval. it looks like this (code condensed a bit for posting in Slack):
class SimplePoller(private val interval: Long, private val block: suspend () -> Unit) {
private var job: Job? = null
fun start(scope: CoroutineScope) {
if (job == null || job?.isCompleted == true) {
job = scope.launch {
while (isActive) {
try {
block()
delay(interval)
} catch (ex: Exception) { cancel() } // stop on errors
}
}
}
}
fun stop() {
job?.let { if (it.isActive) it.cancel() }
}
}
Most of the time, it's used to poll the block
while some Android Lifecycle
is in a certain state. For example:
val myPoller = SimplePoller(60_000) {
//do some work that needs to be repeated like call a network api, etc
}
lifecycleScope.launch {
whenResumed {
myPoller.start(this)
}
}
Inside the poller, I launch a new job in the passed in scope so that I can "manually" (via a call to stop()
) stop the polling if necessary.
Does this seem like a good approach?
It seems to work pretty well. In testing the poller runs while the lifecycle is resumed, and suspends (because the passed in scope uses a magical Android "PausingDispatcher") when lifecycle < resumed. It also seems flexible enough that I could really use it in other, non-android-lifecycle situations:
someScope.launch {
myPoller.start(this)
}
// And then sometime later:
myPoller.stop()
I don't see anything immediately wrong, but as I mentioned, I'm still new to coroutines, so... 🤷Adam Powell
02/18/2021, 4:56 PMclass SimplePoller(
private val interval: Long,
private val block: suspend () -> Unit
) {
suspend fun poll() {
while (isActive) {
block()
delay(interval)
}
}
}
and get this class out of the business of scope and job management altogether, especially with how you're using it in the usage example.
But it also begs other questions: why not use a flow {}
instead? Is this periodic poller only executed for its side effects? What are those side effects? Using a flow with well-defined outputs in situations like this often helps make the overall system more deterministic, less tightly coupled, and easier to test.Adam Powell
02/18/2021, 4:56 PMAdam Powell
02/18/2021, 4:58 PMAdam Powell
02/18/2021, 4:59 PMuli
02/18/2021, 5:04 PMstart
?
Most of us would get that wrong or suboptimal. That’s why it is better to resort on the existing infrastructure.Adam Powell
02/18/2021, 5:05 PMBradleycorn
02/18/2021, 5:11 PMwhenResumed
would use an observer to launch a corouting when entring the resumed state and cancel it when the lifecycle "leaves" the resumed state. I was quite surprised to find on my first attempt that when the lifecycle when to STOPPED, that the whenResumed
scope didn't get just canceled! What?!?!?! I had to dig through the source to find the pausing dispatcher and realize what was going on.Bradleycorn
02/18/2021, 5:22 PMval pollingJob = someScope.launch {
myPoller.start(60_000)
}
//and later...
pollingJob.cancel()
Bradleycorn
02/18/2021, 5:24 PMsuspend fun poll(interval: Long, block: ()->Unit) {
while (isActive) {
block()
delay(interval)
}
}
Adam Powell
02/18/2021, 5:25 PMAdam Powell
02/18/2021, 5:25 PMBradleycorn
02/18/2021, 5:27 PM