8

I have a screen with Jetpack Compose in which I have a TextField for the user to write a text.

With this text I will make a query to obtain data. I want this query to be made when the user finishes typing.

Is there a way to know if the user takes 2 seconds without writing (for example) to launch this query?

Paul9999
  • 751
  • 1
  • 11
  • 26
  • Does [this](https://stackoverflow.com/questions/70632237/avoid-edittext-textwatcher-firing-events-on-every-character-change/70633680#70633680) help? – Tyler V Jan 09 '22 at 23:03
  • 2
    Does this answer your question? [Jetpack Compose and Room DB: Performance overhead of auto-saving user input?](https://stackoverflow.com/questions/69689839/jetpack-compose-and-room-db-performance-overhead-of-auto-saving-user-input) – Phil Dukhov Jan 10 '22 at 05:32
  • Kotlin supports onblurchangelisteners that fire if a user disables focus on the TextField. Maybe that helps. – GabeRAMturn Jul 09 '22 at 10:48

2 Answers2

11

Another way is to avoid the viewmodel completely. Utilise the LaunchedEffect that will cancel/restart itself on every key (text) change. I find this to be way cleaner than to couple your debounce code to your viewmodel.

@Composable
private fun TextInput(
    dispatch: (ViewModelEvent) : Unit,
    modifier: Modifier = Modifier
) {

    var someInputText by remember { mutableStateOf(TextFieldValue("")) }

    TextField(
        value = someInputText,
        onValueChange = {
            someInputText = it
        },
    )

    LaunchedEffect(key1 = someInputText) {
        // this check is optional if you want the value to emit from the start
        if (someInputText.text.isBlank()) return@LaunchedEffect

        delay(2000)
        // print or emit to your viewmodel
        dispatch(SomeViewModelEvent(someInputText.text))
    }
}
Sam
  • 178
  • 2
  • 11
bko
  • 1,038
  • 1
  • 9
  • 17
  • This does not help, you are calling the function upon every event, just with a delay. Its not the same as debounce. – ino Jul 14 '22 at 13:41
  • 7
    From the documentation: *"If LaunchedEffect is recomposed with different keys (see the Restarting Effects section below), the existing coroutine will be cancelled and the new suspend function will be launched in a new coroutine."* Meaning the actual dispatch will be called ONLY when 2 seconds elapse and no further changes to the input were made – bko Jul 15 '22 at 11:35
7

To query after 2 seconds after user stop typing, I think you can use debounce operator (similar idea to the answer here Jetpack Compose and Room DB: Performance overhead of auto-saving user input?)

Here is an example to handle text change on TextField, then query to database and return the result to dbText

class VM : ViewModel() {
    val text = MutableStateFlow("")
    val dbText = text.debounce(2000)
        .distinctUntilChanged()
        .flatMapLatest {
            queryFromDb(it)
        }

    private fun queryFromDb(query: String): Flow<String> {
        Log.i("TAG", "query from db: " + query)
        if (query.isEmpty()) {
            return flowOf("Empty Result")
        }
        // TODO, do query from DB and return result
    }
}

In Composable

Column {
    val text by viewModel.text.collectAsState()
    val dbText by viewModel.dbText.collectAsState("Empty Result")

    TextField(value = text, onValueChange = { viewModel.text.value = it })
    Text(text = dbText)
}
Linh
  • 57,942
  • 23
  • 262
  • 279
  • 2
    Nice answer, thank you. Just don't forget to launch it in viewmodel scope with `launchIn(viewModelScope)` or `viewModelScope.launch { }`. Otherwise `.flatMapLatest { queryFromDb(it) }` doesn't trigger like in my case :) – AtaerCaner Jul 22 '22 at 10:13