Skip to content Skip to sidebar Skip to footer

Solved: Searchview Doesn't Filter In Each Child Tab Of Tablayout

Here, I have a toolbar in an Activity which contains a SearchView. And that activity has multiple fragments. One main fragment out of them have 10 more fragments inside itself. All

Solution 1:

You can manage the filter on nested list by using an Observable/Observer pattern, this will update each nested list from one Observable parent. I fixed all troubles and it works well now to achieve the right behaviour.

Therefore, here's what I did to achieve it:

  1. Using one parent SearchView in Activity
  2. (optional) Create a Filter class (android.widget.Filter) in nested list Adapter
  3. Then, using an Observable/Observer pattern for nested Fragment with Activity

Background: When I tried your code, I had three problems:

  • I cannot do a search using the ActionBar: onQueryTextChange seems to be never called in Fragments. When I tap on search icon, it seems to me that SearchView (edittext, icon, etc) is not attached with the search widget (but attached to the activity's widget).
  • I cannot run the custom method filter2: I mean, when I resolved the previous point, this method doesn't work. Indeed, I have to play with custom class extending by Filter and its two methods: performFiltering and publishResults. Without it, I got a blank screen when I tap a word in search bar. However, this could be only my code and maybe filter2() works perfectly for you...
  • I cannot have a persistent search between fragments: for each child fragment a new SearchView is created. It seems to me that you repeatedly call this line SearchView sv = new SearchView(...); in nested fragment. So each time I switch to the next fragment, the expanded searchview removes its previous text value.

Anyway, after some researches, I found this answer on SO about implementing a Search fragment. Almost the same code as yours, except that you "duplicate" the options menu code in parent activity and in fragments. You shouldn't do it - I think it's the cause of my first problem in previous points. Besides, the pattern used in the answer's link (one search in one fragment) might not be adapted to yours (one search for multiple fragments). You should call one SearchView in the parent Activity for all nested Fragment.


Answer : This is how I managed it:

#1 Using a parent SearchView:

It will avoid duplicate functions and let the parent activity supervise all its children. Futhermore, this will avoid your duplication icon in the menu. This is the main parent Activity class:

publicclassActivityNameextendsAppCompatActivityimplementsSearchView.OnQueryTextListener {

    @OverridepublicbooleanonCreateOptionsMenu(Menu menu) {
        getMenuInflater().inflate(R.menu.menu_main, menu);

        MenuItem item = menu.findItem(R.id.action_search);
        SearchView searchview = newSearchView(this);
        SearchManager searchManager = (SearchManager) getSystemService(SEARCH_SERVICE);
        searchview.setSearchableInfo(searchManager.getSearchableInfo(getComponentName()));
        ...
        MenuItemCompat.setShowAsAction(item, 
                MenuItemCompat.SHOW_AS_ACTION_COLLAPSE_ACTION_VIEW | 
                MenuItemCompat.SHOW_AS_ACTION_IF_ROOM);
        MenuItemCompat.setActionView(item, searchview);
        searchview.setOnQueryTextListener(this);
        searchview.setIconifiedByDefault(false);

        returnsuper.onCreateOptionsMenu(menu);
    }

    privatevoidchangeSearchViewTextColor(View view) { ... }

    @OverridepublicbooleanonQueryTextSubmit(String query) { returnfalse; }

    @OverridepublicbooleanonQueryTextChange(String newText) {
        // update the observer here (aka nested fragments)returntrue;
    }
}

#2 (optional) Create a Filter widget:

Like I said previously, I cannot get it work with filter2(), so I create a Filter class as any example on the web. It quickly looks like, in the adapter of nested fragment, as follows:

privateArrayList<String> originalList; // I used String objects in my testsprivateArrayList<String> filteredList;
privateListFilter filter = newListFilter();

@Overridepublic int getCount() {
    return filteredList.size();
}

publicFiltergetFilter() {
    return filter;
}

privateclassListFilterextendsFilter {
    @OverrideprotectedFilterResultsperformFiltering(CharSequence constraint) {
        FilterResults results = newFilterResults();
        if (constraint != null && constraint.length() > 0) {
            constraint = constraint.toString().toLowerCase();
            final List<String> list = originalList;
            int count = list.size();

            final ArrayList<String> nlist = newArrayList<>(count);
            String filterableString;
            for (int i = 0; i < count; i++) {
                filterableString = list.get(i);
                if (filterableString.toLowerCase().contains(constraint)) {
                    nlist.add(filterableString);
                }
            }

            results.values = nlist;
            results.count = nlist.size();
        } else {
            synchronized(this) {
                results.values = originalList;
                results.count = originalList.size();
            }
        }
        return results;
    }

    @SuppressWarnings("unchecked")
    @OverrideprotectedvoidpublishResults(CharSequence constraint, FilterResults results) {
        if (results.count == 0) {
            notifyDataSetInvalidated();
            return;
        }

        filteredList = (ArrayList<String>) results.values;
        notifyDataSetChanged();
    }
}

#3 Using an Observable/Observer pattern:

The activity - with the searchview - is the Observable object and the nested fragments are the Observers (see Observer pattern). Basically, when the onQueryTextChange will be called, it will trigger the update() method in the existant observers. Here's the declaration in parent Activity:

privatestaticActivityName instance;
privateFilterManager filterManager;

@OverrideprotectedvoidonCreate(Bundle savedInstanceState) {
    ...
    instance = this;
    filterManager = newFilterManager();
}

publicstaticFilterManagergetFilterManager() {
    return instance.filterManager; // return the observable class
}

@OverridepublicbooleanonQueryTextChange(String newText) {
    filterManager.setQuery(newText); // update the observable valuereturntrue;
}

This is the Observable class which will listen and "pass" the updated data:

publicclassFilterManagerextendsObservable {
    privateString query;

    publicvoidsetQuery(String query) {
        this.query = query;
        setChanged();
        notifyObservers();
    }

    publicStringgetQuery() {
        return query;
    }
}

In order to add the observer fragments to listen the searchview value, I do it when they are initialized in the FragmentStatePagerAdapter. So in the parent fragment, I create the content tabs by passing the FilterManager:

privateViewPager pager;
privateViewPagerAdapter pagerAdapter;

@OverridepublicViewonCreateView(...) {
    ...
    pagerAdapter = newViewPagerAdapter(
         getActivity(),                    // pass the context,getChildFragmentManager(),        // the fragment managerMainActivity.getFilterManager()   // and the filter manager
    );
}

The adapter will add the observer to the parent observable and remove it when the child fragments are destroyed. Here's the ViewPagerAdapter of parent fragment:

publicclassViewPagerAdapterextendsFragmentStatePagerAdapter {

    private Context context;
    private FilterManager filterManager;

    publicViewPagerAdapter(FragmentManager fm) {
        super(fm);
    }

    publicViewPagerAdapter(Context context, FragmentManager fm, 
               FilterManager filterManager) {
        super(fm);
        this.context = context;
        this.filterManager = filterManager;
    }

    @Overridepublic Fragment getItem(int i) {
        NestedFragmentfragment=newNestedFragment(); // see (*)
        filterManager.addObserver(fragment); // add the observerreturn fragment;
    }

    @OverridepublicintgetCount() {
        return10;
    }

    @OverridepublicvoiddestroyItem(ViewGroup container, int position, Object object) {
        NestedFragmentfragment= (NestedFragment) object; // see (*)
        filterManager.deleteObserver(fragment); // remove the observersuper.destroyItem(container, position, object);
    }
}

Finally, when filterManager.setQuery() in activity is called with onQueryTextChange(), this will be received in nested fragment in update() method which are implementing Observer. This is the nested fragments with the ListView to filter:

publicclassNestedFragmentextendsFragmentimplementsObserver {
    privateboolean listUpdated = false; // init the update checking value
    ...
    // setup the listview and the list adapter
    ...
    // use onResume to filter the list if it's not already done@OverridepublicvoidonResume() {
        super.onResume();
        // get the filter value
        final String query = MainActivity.getFilterManager().getQuery();
        if (listview != null && adapter != null 
                     && query != null && !listUpdated) {
            // update the list with filter value
            listview.post(newRunnable() {
                @Overridepublicvoidrun() {
                    listUpdated = true; // set the update checking value
                    adapter.getFilter().filter(query);
                }
            });
        }
    }
    ...
    // automatically triggered when setChanged() and notifyObservers() are calledpublicvoidupdate(Observable obs, Object obj) {
        if (obs instanceofFilterManager) {
            String result = ((FilterManager) obs).getQuery(); // retrieve the search valueif (listAdapter != null) {
                listUpdated = true; // set the update checking value
                listAdapter.getFilter().filter(result); // filter the list (with #2)
            }
        }
    }
}

#4 Conclusion:

This works well, the lists in all nested fragments are updated as expected by just one searchview. However, there is an incovenient in my above code that you should be aware of:

  • (see improvements below)I cannot call Fragment general object and add it to being an observer. Indeed, I have to cast and init with the specific fragment class (here NestedFragment); there might be a simple solution, but I didn't find it for now.

Despite this, I get the right behaviour and - I think - it might be a good pattern by keeping one search widget at the top, in activity. So with this solution, you could get a clue, a right direction, to achieve what you want. I hope you'll enjoy.


#5 Improvements (edit):

  • (see *) You can add the observers by keeping a global Fragment class extension on all nested fragments. This how I instantiate my fragments to the ViewPager:

    @OverridepublicFragmentgetItem(int index) {
        Fragment frag = null;
        switch (index) {
            case0:
                frag = newFirstNestedFragment();
                break;
            case1:
                frag = newSecondFragment();
                break;
            ...
        }
        return frag;
    }
    
    @OverridepublicObjectinstantiateItem(ViewGroup container, int position) {
        ObserverFragment fragment = 
                (ObserverFragment) super.instantiateItem(container, position);
        filterManager.addObserver(fragment); // add the observerreturn fragment;
    }
    
    @OverridepublicvoiddestroyItem(ViewGroup container, int position, Objectobject) {
        filterManager.deleteObserver((ObserverFragment) object); // delete the observersuper.destroyItem(container, position, object);
    }
    

    By creating the ObserverFragment class as follows:

    publicclassObserverFragmentextendsFragmentimplementsObserver {
        publicvoidupdate(Observable obs, Object obj) { /* do nothing here */ }
    }
    

    And then, by extending and overriding update() in the nested fragments:

    publicclassFirstNestedFragmentextendsObserverFragment {
        @Overridepublicvoidupdate(Observable obs, Object obj) { }
    }    
    

Solution 2:

Just to understand the question better. 1. You have a search view in the Action Bar 2. You have multiple Fragments (from your image Advisory/TopAdvisors...) 3. Within each of these there are multiple Fragments for each Tab (example Equity ... etc) 4. You want all list views in the fragments to filter their data based on the search content

Right??

In your current implementation what is the status? Is the list view which is current being displayed getting filtered?

Or even that is not working? Then you need to check notifydatasetChanged is propagating correctly to the adapter. Meaning it should be on the UIThread itself.

For updates to all fragments, meaning once which were not on screen when you were typing the search text, you need to consider the Fragment lifecycle and include code in onResume of the fragment to make sure list is filtered before it is used to initialise the adapter. This is for scenario where you have typed search text already and now are moving between tabs/fragments. So fragments would go on and off screen hence the need for onResume.

Update: Include checking search box for text, and if it has something calling adapter.filter2() in onResume, because that is basically what is filtering your list. .

Solution 3:

  • the problem is - when you launch a fragment, it "takes over" the activity's searchView and whatever you type AFTER this fragment is launched invokes the onQueryTextChange listener of JUST THIS fragment.. Hence, the search doesnt take place in any other fragment..
  • You want your fragments to check for the last search query (posted by any other fragment) when they are launched and perform a search in itself for that query. Also, any change in the search query must also be posted to the activity's search view so other fragments can read it when they are launched.

I'm assuming your viewpager/tabs are implemented in a way that whenever you switch the visible fragments, they get destroyed.

Make the following changes (read the comments)

publicclassMainActivityextendsAppCompatActivity {
final SearchView searchView; //make searchView a class variable/field@OverridepublicbooleanonCreateOptionsMenu(Menu menu) {
    getMenuInflater().inflate(R.menu.menu_main, menu);
    searchView = (SearchView) MenuItemCompat.getActionView(menu.findItem(R.id.action_search));
    SearchManagersearchManager= (SearchManager) getSystemService(SEARCH_SERVICE);
    searchView.setSearchableInfo(searchManager.getSearchableInfo(getComponentName()));
    changeSearchViewTextColor(searchView);
    returntrue;
}
}

Now, in all your fragments do this -

@OverridepublicvoidonCreateOptionsMenu(Menu menu, MenuInflater inflater) {
        inflater.inflate(R.menu.main, menu); // removed to not double the menu itemsMenuItemitem= menu.findItem(R.id.action_search);
        SearchViewsv=newSearchView(((MainActivity) getActivity()).getSupportActionBar().getThemedContext());
        changeSearchViewTextColor(sv);
        MenuItemCompat.setShowAsAction(item, MenuItemCompat.SHOW_AS_ACTION_COLLAPSE_ACTION_VIEW | MenuItemCompat.SHOW_AS_ACTION_IF_ROOM);
        MenuItemCompat.setActionView(item, sv);
        sv.setOnQueryTextListener(this);
        sv.setIconifiedByDefault(false);
        sv.setQuery(((MainActivity)getActivity()).searchView.getQuery()); // set the main activity's search view query in the fragment's search viewsuper.onCreateOptionsMenu(menu, inflater);
    }

Also, in your SearchView.OnQueryTextListener in the fragments, do this

SearchView.OnQueryTextListener() {
            @Override
            public boolean onQueryTextSubmit(String query) {
                search(query); // this is your usual adapter filtering stuff, which you are already doing
                ((MainActivity)getActivity()).searchView.setQuery(query);//sync activity's searchview query with fragment's searchview query, so other fragments can read from the activity's search query when they launch.
                return false;
            }

            @Override
            public boolean onQueryTextChange(String newText) {
                search(newText);// this is your usual adapter filtering stuff, which you are already doing
                ((MainActivity)getActivity()).searchView.setQuery(query);//sync activity's searchview query with fragment's searchview query
                return false;
            }
        });

I hope you get the general idea

  • You have different search view instances in each of your fragments and the activity
  • you need to sync the search queries among each of them, make each of them aware of what the other instances have..

I don't recommend this design though, what i would rather do is

  • just use the activity's search view and in the onQueryTextChange listener of activity's search view.
  • i would notify all the fragments which are alive from onQueryTextChange in the activity (the fragments would register and deregister onQueryTextChange listeners with the activity in their onResume & onPause of the respective fragments). [Side note - This design pattern is called the Observer pattern]

Post a Comment for "Solved: Searchview Doesn't Filter In Each Child Tab Of Tablayout"