Hello Folks,
Alrighty, time to dive back into the world of flows! Today we’re putting the spotlight on two underrated tools: debounce
and sample
.
A lot of y’all know about debounce
, but sample
? Not so much. And let’s be real, some are even using debounce wrong. So, we’re about to break it all down and make these concepts crystal clear. Let’s get into it! 🔥
debounce:
- Where can we use debounce? There is a really amazing and regularly used example for that, which is Autocomplete Suggestions
Problem:
- Imagine you have a search box that provides autocomplete suggestions as the user types. Without debounce, the system would trigger a search for suggestions after each keystroke, potentially overloading the server or causing unnecessary processing. This leads to wasted resources and poor user experience, as users typically pause briefly before finalizing their query.
Solution with Debounce:
- By using a debounce mechanism, you only send a search request once the user has stopped typing for a specified time (e.g., 500 ms). This ensures that you avoid making too many unnecessary requests and only react to the final input after the user pauses.
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*
import kotlin.system.*
fun main() = runBlocking {
// Simulated user input flow
val userInputFlow = MutableStateFlow("") // A Flow that represents the user's input
// Debounce and handle the search after 500ms of inactivity
userInputFlow
.debounce(500) // Wait for 500ms of inactivity before emitting the final value
.filter { it.isNotEmpty() } // Filter out empty input to avoid unnecessary requests
.collectLatest { query ->
// Simulate a search request
performSearch(query)
}
// Simulate user typing at different intervals
launch {
listOf("a", "ab", "abc").forEachIndexed { index, text ->
delay(100L * index) // Simulate typing delay (user types "a", then "ab", then "abc" quickly)
println("User typed: $text")
userInputFlow.value = text
}
}
}
// Simulated search function
suspend fun performSearch(query: String) {
println("Searching for: $query")
delay(1000) // Simulate network delay for the search operation
println("Search results for: $query")
}
Explanation:
- It simply solves your server overloading problem, whatever happens after 500 ms, will only hit the server. So till 500 ms, no value will be emitted by flow.
- Hold on, we will dive deep into the internals of this code.
Problem Statement 2
- There is a case, where I have continuous data from the server every millisecond, such as trading data, where data changes every 100,200,500 milliseconds we don’t know about that, but what will happen on the UI side if we label this data? Just imagine every 100,200,500 ms of data, every time your UI will be re-render and your app performance will be compromised.
- But now have a solution what if we use debounce and print data after each second? That will be a relief for our UI component part.
- Now let’s create this situation via code and see what happened this time we use the debounce operator.
fun main() = runBlocking {
// Create a Flow that emits values every 300ms
val flow = flow {
var value = 0
while (true) {
delay(300) // Emit every 300ms
emit(value++)
}
}
// Collect values with a debounce of 1 second
flow
.debounce(1000) // Wait for 1 second before collecting values
.collectLatest { value ->
println("Collected value: $value")
}
}
Just try to think what will be the output for this code in your mind.
Explanation:
- Well answer is very surprising, it will not print anything for ages. You can try to run this code on your PC and you will get results.
- But why did this happen? debounce should help us to print the code after every second.
The thing with Debounce is,
- Your flow emits values every 300 ms.
- The
debounce(1000)
operator will only emit a value if no new value is emitted for 1 second. - Since the flow emits new values every 300 ms (which is faster than the 1-second debounce time), the debounce operator never has a chance to emit any value. The timer keeps resetting, as new values are continuously being emitted before the 1-second debounce window expires.
So, in such kind of cases, we really can’t rely on debounce, let’s understand internally how it handles this situation.
Let’s look at the internal code implementation:
Job Offers
- So there are two cases where value gets emitted.
// Compute timeout for this value
if (lastValue != null) {
timeoutMillis = timeoutMillisSelector(NULL.unbox(lastValue))
require(timeoutMillis >= 0L) { "Debounce timeout should not be negative" }
if (timeoutMillis == 0L) {
downstream.emit(NULL.unbox(lastValue))
lastValue = null // Consume the value
}
}
// wait for the next value with timeout
select<Unit> {
// Set timeout when lastValue exists and is not consumed yet
if (lastValue != null) {
onTimeout(timeoutMillis) {
downstream.emit(NULL.unbox(lastValue))
lastValue = null // Consume the value
}
}
}
- The first case is where you gave timeout value 0 to your debounce operator. in that case, it will not wait for anything and will emit value continuously which is also fair.
- But in the second case, values get emitted on timeout. Whenever it hits to timeout value will be emitted, till then nothing will happen. So our code gets stuck here because it never hit timeout, so every time a new value arrives timer gets reset and this situation never stops.
So here we understood about the code but what about our problem statement, how can we solve this situation?
And here sample operator comes into the picture.
sample:
- Sample periodically samples the latest value emitted by the flow within a specified time interval. It emits the most recent value at regular intervals, regardless of the emission frequency.
- Let’s look at the same code which we have used earlier
fun main() = runBlocking {
// Create a Flow that emits values every 300ms
val flow = flow {
var value = 0
while (true) {
emit(value++)
delay(300) // Emit every 300ms
}
}
// Collect values with a debounce of 1 second
flow
.sample(1000) // Wait for 1 second before collecting values
.collectLatest { value ->
println("Collected value: $value")
}
}
Output:-
Collected value: 3
Collected value: 6
Collected value: 9
Collected value: 13
Collected value: 16
Collected value: 19
Collected value: 23
Collected value: 26
Collected value: 29
Collected value: 32
Collected value: 36
...
So on
- This is exactly the behavior that we want.
- Before wrapping up this session, let’s look at the sample operator’s internal code.
Explanation:
- If you check out the code, you’ll see that the
fixedPeriodTicker
function sends aUnit
to notify the channel when the delay is over, signaling it to emit the latest value. So, whatever the most recent value is at that moment will be emitted.
So that’s it, for now, Hope you picked up some fresh insights on operators in Flow. Let me know in the comments if you have doubts.
See you guys in the next article.
This article is previously published on proandroiddev.com