Throttle onQueryTextChange in SearchView
Asked Answered
M

9

16

What's the best way to "throttle" onQueryTextChange so that my performSearch() method is called only once every second instead of every time the user types?

public boolean onQueryTextChange(final String newText) {
    if (newText.length() > 3) {
        // throttle to call performSearch once every second
        performSearch(nextText);
    }
    return false;
}
Melinamelinda answered 22/1, 2016 at 20:10 Comment(0)
M
-1

I ended up with a solution similar to below. That way it should fire once every half second.

        public boolean onQueryTextChange(final String newText) {

            if (newText.length() > 3) {

                if (canRun) {
                    canRun = false;
                    handler.postDelayed(new Runnable() {
                        @Override
                        public void run() {

                            canRun = true;
                            handleLocationSearch(newText);
                        }
                    }, 500);
                }
            }

            return false;
        }
Melinamelinda answered 25/1, 2016 at 14:17 Comment(2)
this is a wrong solution. see pls proandroiddev.com/…Perforate
this solution lacks cancellation. you should keep in mind to remove callbacks from handler if you no longer need to observe query text changesAppeal
H
30

If you are using Kotlin and coroutines you could do the following:

var queryTextChangedJob: Job? = null

...

fun onQueryTextChange(query: String) {

    queryTextChangedJob?.cancel()
    
    queryTextChangedJob = launch(Dispatchers.Main) {
        delay(500)
        performSearch(query)
    }
}
Hoskinson answered 24/4, 2018 at 17:14 Comment(2)
Coroutines is the way forward! Thanks for the answer.Extrude
Thanks, @jc12, my code is mostly reduced.Immigrate
G
25

Building on aherrick's code, I have a better solution. Instead of using a boolean 'canRun', declare a runnable variable and clear the callback queue on the handler each time the query text is changed. This is the code I ended up using:

@Override
public boolean onQueryTextChange(final String newText) {
    searchText = newText;

    // Remove all previous callbacks.
    handler.removeCallbacks(runnable);

    runnable = new Runnable() {
        @Override
        public void run() {
            // Your code here.
        }
    };
    handler.postDelayed(runnable, 500);

    return false;
}
Glen answered 22/5, 2016 at 18:57 Comment(0)
F
9

I've come up to a solution using RxJava, particularly it's debounce operator.

With Jake Wharton's handy RxBinding, we'll have this:

RxSearchView.queryTextChanges(searchView)
        .debounce(1, TimeUnit.SECONDS) // stream will go down after 1 second inactivity of user
        .observeOn(AndroidSchedulers.mainThread())
        .subscribe(new Consumer<CharSequence>() {
            @Override
            public void accept(@NonNull CharSequence charSequence) throws Exception {
                // perform necessary operation with `charSequence`
            }
        });
Fernyak answered 27/3, 2017 at 6:12 Comment(0)
O
5
  1. Create abstract class:

    public abstract class DelayedOnQueryTextListener implements SearchView.OnQueryTextListener {
    
     private Handler handler = new Handler();
     private Runnable runnable;
    
     @Override
     public boolean onQueryTextSubmit(String s) {
         return false;
     }
    
     @Override
     public boolean onQueryTextChange(String s) {
         handler.removeCallbacks(runnable);
         runnable = () -> onDelayerQueryTextChange(s);
         handler.postDelayed(runnable, 400);
         return true;
     }
    
     public abstract void onDelayedQueryTextChange(String query);
    

    }

  2. Set it like this:

    searchView.setOnQueryTextListener(new DelayedOnQueryTextListener() {
         @Override
         public void onDelayedQueryTextChange(String query) {
             // Handle query
         }
     });
    
Oast answered 20/3, 2019 at 9:18 Comment(0)
M
3

For Korlin

In case of coroutineScope.launch(Dispatchers.Main) {} you may run into a problem Suspend function '...' should be called only from a coroutine or another suspend function.

I found the following way

private var queryTextChangedJob: Job? = null
private lateinit var searchText: String

Next don't forget use implementation "androidx.lifecycle:lifecycle-runtime-ktx:2.3.0-alpha05"

override fun onQueryTextChange(newText: String?): Boolean {

    val text = newText ?: return false
    searchText = text

    queryTextChangedJob?.cancel()
    queryTextChangedJob = lifecycleScope.launch(Dispatchers.Main) {
        println("async work started...")
        delay(2000)
        doSearch()
        println("async work done!")
    }

    return false
}

If you want use launch inside ViewModel then use implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:2.3.0-alpha05"

queryTextChangedJob = viewModelScope.launch(Dispatchers.Main) {
        //...
}
Mumford answered 26/6, 2020 at 14:46 Comment(0)
I
2

You can easily achieve it with RxJava. Also, you will need RxAndroid and RxBinding (but you probably already have them in your project if you are using RxJava).

RxTextView.textChangeEvents(yourEditText)
          .debounce(1, TimeUnit.SECONDS)
          .observeOn(AndroidSchedulers.mainThread())
          .subscribe(performSearch());

Here the full example by Kaushik Gopal.

Isomerize answered 22/1, 2016 at 20:25 Comment(2)
Thanks! However I'm not really interesting in bringing in RxJava at this point. Can it be done natively?Melinamelinda
Unfortunately, I don't know any common clean solutions for this problem. I definitely could be achieved with Handler delays or Timer and TimerTasks.Isomerize
S
1

I didn't find a solution that enable me to use some flow operators like distinctUntilChanged , utile I tried this approach, maybe someone need it:

First create an extension function from SearchView

private fun SearchView.getQueryTextChangeStateFlow(): StateFlow<String> {
    val query = MutableStateFlow("")
    
    setOnQueryTextListener(object : SearchView.OnQueryTextListener {
        override fun onQueryTextSubmit(query: String?): Boolean {
            return true
        }
        override fun onQueryTextChange(newText: String): Boolean {
            query.value = newText
            return true
        }
    })

    return query
}

Then

 private fun setUpSearchStateFlow(searchView: SearchView) {
    lifecycleScope.launch {
        searchView.getQueryTextChangeStateFlow()
            .debounce(300)
            .distinctUntilChanged()
            .map { query ->
                startSearchRequestHere(query)
            }
            .flowOn(Dispatchers.Default)
            .collect {}
    }
}

Reference

Shackleton answered 27/6, 2022 at 3:35 Comment(0)
A
0

Using Coroutines and Flow:

private fun SearchView.onQueryTextChanged(): ReceiveChannel<String> =
    Channel<String>(capacity = Channel.UNLIMITED).also { channel ->
        setOnQueryTextListener(object : SearchView.OnQueryTextListener{
            override fun onQueryTextSubmit(query: String?): Boolean {
                return false
            }

            override fun onQueryTextChange(newText: String?): Boolean {
                newText.orEmpty().let(channel::offer)
                return true
            }
        })
    }

@ExperimentalCoroutinesApi
fun <T> ReceiveChannel<T>.debounce(time: Long, unit: TimeUnit = TimeUnit.MILLISECONDS, scope: CoroutineScope): ReceiveChannel<T> =
    Channel<T>(capacity = Channel.CONFLATED).also { channel ->
        scope.launch {
            var value = receive()
            whileSelect {
                onTimeout(time) {
                    channel.offer(value)
                    value = receive()
                    true
                }
                onReceive {
                    value = it
                    true
                }
            }
        }
    }

And add to your search view like this:

lifecycleScope.launch {
   searchView.onQueryTextChanged().debounce(time = 500, scope = lifecycleScope).consumeEach { newText ->
          //Use debounced query
   }
}
Allopatric answered 23/7, 2021 at 10:42 Comment(0)
M
-1

I ended up with a solution similar to below. That way it should fire once every half second.

        public boolean onQueryTextChange(final String newText) {

            if (newText.length() > 3) {

                if (canRun) {
                    canRun = false;
                    handler.postDelayed(new Runnable() {
                        @Override
                        public void run() {

                            canRun = true;
                            handleLocationSearch(newText);
                        }
                    }, 500);
                }
            }

            return false;
        }
Melinamelinda answered 25/1, 2016 at 14:17 Comment(2)
this is a wrong solution. see pls proandroiddev.com/…Perforate
this solution lacks cancellation. you should keep in mind to remove callbacks from handler if you no longer need to observe query text changesAppeal

© 2022 - 2024 — McMap. All rights reserved.