Search code examples
androidandroid-viewmodelandroid-textwatcher

How to get ViewModel observer to not observe initially?


I have a SearchView icon on the Toolbar via onCreateOptionsMenu(Menu menu). A click on the icon creates an EditText line for the user to enter search inputs. I have a TextWatcher() attached to the EditText line so that when text is added to the line (afterTextChanged Editable s) a searchQuery method is run via the ViewModel that searches a Room database.

My problem is that the observer is firing upon creation even before the user enters any query. Using Toasts, I was able to confirm that the observer is running a blank query "%%" right when the SearchView icon is pressed by the user and before the user enters any search query. And this is happening even though the observer is set up in the afterTextChanged(). How do I get the ViewModel observer to only fire after text is entered by the user?

Activity
...
@Override
public boolean onCreateOptionsMenu(Menu menu) {

getMenuInflater().inflate(R.menu.mainactiv_menu, menu);
searchItem = menu.findItem(R.id.action_search);
menu.findItem(R.id.action_search).setVisible(false);
if (cardsAdapter != null && cardsAdapter.getItemCount() > 0) {
    menu.findItem(R.id.action_search).setVisible(true);
}
SearchManager searchManager = (SearchManager) MainActivity.this.getSystemService(Context.SEARCH_SERVICE);
if (searchItem != null) {
    mSearchView = (SearchView) searchItem.getActionView();
    if (mSearchView != null) {
        mSearchView.setSearchableInfo(searchManager.getSearchableInfo(getComponentName()));
        EditText mSearchEditText = mSearchView.findViewById(androidx.appcompat.R.id.search_src_text);
        mSearchEditText.setInputType(android.text.InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS);
        mSearchEditText.addTextChangedListener(new TextWatcher() {

            @Override
            public void beforeTextChanged(CharSequence s, int start, int count, int after) {
               // not needed
            }

            @Override
            public void onTextChanged(CharSequence s, int start, int before, int count) {
                // not needed
            }

            @Override
            public void afterTextChanged(Editable s) {

                String queryText = "%" + s.toString() + "%";

                mQuickcardViewModel.searchQuery(queryText).observe(MainActivity.this, searchCards -> {

                    searchList = searchCards;

                    if (!mSearchView.isIconified() && searchList.size() > 0) {
                        // do something
                    } 

                    ...     

Solution

  • How do I get the ViewModel observer to only fire after text is entered by the user?

    Use an if statement to see if the string is empty before observing:

    @Override
    public void afterTextChanged(Editable s) {
         if (s.toString().length() > 0) {
             // the rest of your code goes here, preferably with some modifications
         }
    }
    

    Other changes that I would recommend include:

    • Adding some measure of "debounce". Given your search expression, you appear to be hitting a database in searchQuery(). If the user types 10 characters in rapid succession, you will make 10 queries, which will make performance poor. "Debounce" says "only query after the user has paused for a bit", such as 500ms without any further input.

    • Adding in smarts to cancel an outstanding query when needed. Suppose the user types a bit, and then the debounce period elapses. So, you fire off a query. Because of your (presumed) LIKE expression, your query is going to do a "table scan", examining every row of the table. That could be slow, and the user might start typing some more before that first query completes. You no longer need that query, as its results are wrong (it is for the previous search expression, not the current one). So, you need a way to cancel that work.

    • Dealing with configuration changes. You might argue that you are using a ViewModel and LiveData, which should handle that... and it does, but only if you use them properly. In your case, you appear to be throwing away that LiveData after observing it, and so on a configuration change, you will not be in position re-observe it. This is why a lot of samples focus on decoupling the "please do the background work" from "please give me the results of the background work". You could still have a searchQuery() method that you call, but it would return void. The results would get piped through some LiveData held by the ViewModel, one that you observe in onCreate(). That way, after a configuration change, you once again observe that LiveData, and you get the data as it was prior to that configuration change.

    • Using FTS. LIKE is slow. If you are going to do this a lot consider using FTS3/FTS4 in SQLite for full-text searching. If you are using Room, there is built-in support for this in Room 2.2.0.