https://kotlinlang.org logo
#getting-started
Title
# getting-started
k

Ky

02/15/2021, 2:53 AM
Hey guys, I’m coming from RxJava world and having some trouble implementing a debounce operator on a Jetpack compose TextField using
Flow
. So this works as expected
Copy code
CoroutineScope(IO).launch {
            flow<Int>{
                emit(1)
                delay(350)
                emit(2)
                delay(100)
                emit(3)
            }.debounce(300)
                .collect {
                    Timber.d(it.toString())
                }
        }

Output: 1, 3
However trying to apply this same idea to a searchview I have..
Copy code
TextField(
                    modifier = Modifier
                        .fillMaxSize()
                        .background(
                            color = MaterialTheme.colors
                                .elevatedSurface(3.dp)
                                .copy(alpha = 0.95f)
                        ),
                    value = searchTerm.value, onValueChange = { newValue ->
                        searchTerm.value = newValue
                        CoroutineScope(IO).launch {
                            queryFlow(newValue.text, viewModel)
                        }
                    })
This is
queryFlow
Copy code
suspend fun queryFlow(query: String, viewModel: TwitMainViewModel) = flow {
    Timber.d("Emitting: ${query}")
    emit(query)
    delay(500)
}.debounce(1000)
    .filterNot {
        it.isEmpty()
    }
    .collect {
        Timber.d("MAKING REQUEST FOR: $it")
        viewModel.search(it)
    }
So in this case, even if I rapid type
nnn
a request is made for each; being
n
nn
and
nnn
. The debounce operator has no effect here. I can’t identify why however, any ideas? Thanks
a

Adam Powell

02/15/2021, 3:03 AM
onValueChange
runs for every change request from the text field, so this is going to launch a new independent query coroutine for each change that happens. The debounce will delay a search request, but since each request is independent, it won't have the intended effect.
What you're looking for is likely something more like this:
Copy code
// remember a channel we can send to.
// Conflated because we want old values to be replaced by new.
// queryChannel.offer will always succeed.
val queryChannel = remember { Channel<String>(Channel.CONFLATED) }

// LaunchedEffect will run our queries and cancel everything when
// this composable leaves the composition. If we get a different
// viewModel object this will cancel the old effect and launch a new one.
LaunchedEffect(viewModel) {
  // Use withContext here to change dispatchers if desired
  queryChannel.receiveAsFlow()
    .debounce(1000)
    .filterNot { it.isEmpty() }
    .collect {
      viewModel.search(it)
    }
}

TextField(
  // ...
  onValueChange = { newValue ->
    searchTerm.value = newValue
    queryChannel.offer(newValue.text)
  }
)
You might also enjoy the #compose and #coroutines channels!
a

Albert Chang

02/15/2021, 3:15 AM
https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.flow/flow.html
Creates a cold flow from the given suspendable block. The flow being cold means that the block is called every time a terminal operator is applied to the resulting flow.
k

Ky

02/15/2021, 2:07 PM
Thanks for the help!