Categories
Android

Debouncing a function

I will start with a very common use case – say we have events that occurs a lot and in random, sometimes small intervals and we want to have them written to disk as soon as possible. We could simply write them to disk immediately after every event, but if we look back at rule 3 of Ten Commandments For Developing a SDK on Android (which applies to simple android applications too) then we should spare some of that IO overhead to avoid waste of user’s device resources. What we can do, is whenever we get an event, we will debounce the writing to say 1 second later, so that we wont do any IO for at least 1 second per IO, this will also force us to write more data at once which also saves precious device resources.

There are various ways to do that, I always support the approach of not reinventing the wheel so my first recommendation is to use RxJava for that and if you want to do that there are tons of other articles you can google. if you wish to refrain using RxJava in your project for any reason (e.g you are writing an SDK or an Android Library and you are trying to avoid adding 3rd party dependencies, like section 5 of Ten Commandments For Developing a SDK on Android) it still can be easily implemented, I will show here two approaches one using good old Handler class, and the second uses Kotlin Coroutines.

Let’s start implementing it as simple and basic as possible beginning with as relaxed requirements as possible, and then add support for more cases and fulfilling more requirements.

We start with this rather simple debounceable functions factory implementation:

				
					fun debounceable(handler: Handler = MAIN_THREAD, runnable: Runnable): (Long) -> Unit {
    return { delay: Long ->
        handler.removeCallbacks(runnable)
        handler.postDelayed(runnable, delay)
    }
}
				
			

Here is an example of usage:

				
					    fun testDebounceable() {
        val start = System.currentTimeMillis()
        val debounce = debounceable {
            Log.d("DEBOUNCEABLE", (System.currentTimeMillis() - start).toString())
        }
        for (time in arrayOf(100L,100,400,100,200)) {
            debounce(300)
            Thread.sleep(time)
        }
    }
				
			

This will log at time ~500 (100+100+300) and then again at ~1000 (100+100+400+100+200). 

But there is some major setback for this factory implementation (this is the point where you try to think what it is before continue reading…) 

Ok the setback is that it is only guaranteed to work well in a single thread environment, so if we want to make sure our factory creates thread safe debounceable functions we can alter the factory like this: 

 

				
					fun debounceable(handler: Handler = MAIN_THREAD, runnable: Runnable): (Long) -> Unit {
    return { delay: Long ->
        synchronized(runnable) {
            handler.removeCallbacks(runnable)
            handler.postDelayed(runnable, delay)
        }
    }
}

				
			

To make our factory more Kotlin freindly, we can overload it with another signature:

 

 

				
					fun debounceable(handler: Handler = MAIN_THREAD, func: () -> Unit): (Long) -> Unit {
    return debounceable(handler, Runnable { func })
}
				
			

We can also add support for passing a parameter along with the delay in each debounce:

				
					fun <P> debounceable(handler: Handler = MAIN_THREAD, procedure: (P) -> Unit): (Long, P) -> Unit {
    var runnable: Runnable? = null
    return { delay: Long, p: P ->
        synchronized(procedure) {
            runnable?.let {
                handler.removeCallbacks(it)
            }
            val notnullRunnable = Runnable { procedure(p) }
            runnable = notnullRunnable
            handler.postDelayed(notnullRunnable, delay)
        }
    }
}
				
			

And use it like that :

 

				
					fun testDebounceable() {
        val start = System.currentTimeMillis()
        val debounce = debounceable { expectedTime: Long ->
            val actualTime = System.currentTimeMillis() - start
            Log.d("DEBOUNCEABLE", actualTime.toString())
            assert(abs(expectedTime - actualTime) < 25)
        }
        var accTime = 0L
        for (time in arrayOf(100L, 100, 400, 100, 200)) {
            accTime += time
            debounce(300, accTime + 300)
            Thread.sleep(time)
        }
    }

				
			

This time, we will exploit Kotlin coroutines and do it more Android Lifecycle friendly:

 

				
					fun <P> debounceable(scope: CoroutineScope = GlobalScope, dispatcher: CoroutineDispatcher = Dispatchers.Main, procedure: (P) -> Unit): (Long, P) -> Unit {
    var job: Job? = null
    return { _delay: Long, p: P ->
        synchronized(procedure) {
            job?.cancel()
            job = scope.launch(dispatcher) {
                delay(_delay)
                procedure(p)
            }
        }
    }
}
				
			

Let’s alter our test to use this new feature:

 

				
					    fun testDebounceable() {
        val start = System.currentTimeMillis()
        val coroutineScope = CoroutineScope(Dispatchers.IO)
        val debounce = debounceable(coroutineScope) { expectedTime: Long ->
            val actualTime = System.currentTimeMillis() - start
            Log.d("DEBOUNCEABLE", actualTime.toString())
            assert(abs(expectedTime - actualTime) < 25)
        }
        var accTime = 0L
        for (time in arrayOf(100L, 100, 400, 100, 200)) {
            accTime += time
            debounce(300, accTime + 300)
            Thread.sleep(time)
            if (accTime >= 600)
                coroutineScope.cancel()
        }
    }
				
			

This will log at time ~500 (100+100+300) and wont log 1000 since the scope is canceled before.

Leave a Reply

Your email address will not be published. Required fields are marked *