Search code examples
javaandroidandroid-fragmentsfiltersearchview

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 10 fragments are showing data in listviews. Now I'm trying to filter all the lists of fragments by SearchView of MainActivity. But it never filters list of each fragment. Now I show you how I implemented it all.

MainActivity.java

public class MainActivity extends AppCompatActivity {
 @Override
public boolean onCreateOptionsMenu(Menu menu) {
    getMenuInflater().inflate(R.menu.menu_main, menu);
    final SearchView searchView = (SearchView) MenuItemCompat.getActionView(menu.findItem(R.id.action_search));
    SearchManager searchManager = (SearchManager) getSystemService(SEARCH_SERVICE);
    searchView.setSearchableInfo(searchManager.getSearchableInfo(getComponentName()));
    changeSearchViewTextColor(searchView);
    return true;
}
}

Fragment.java

public class CurrencyFragment2 extends android.support.v4.app.Fragment implements SearchView.OnQueryTextListener {

    @Override
public void setMenuVisibility(boolean menuVisible) {
    super.setMenuVisibility(menuVisible);
    if (menuVisible && getActivity() != null) {
        SharedPreferences pref = getActivity().getPreferences(0);
        int id = pref.getInt("viewpager_id", 0);
        if (id == 2)
            setHasOptionsMenu(true);
 }
 }
    @Override
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
    inflater.inflate(R.menu.main, menu); // removed to not double the menu items
    MenuItem item = menu.findItem(R.id.action_search);
    SearchView sv = new SearchView(((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);
    super.onCreateOptionsMenu(menu, inflater);
}

private void changeSearchViewTextColor(View view) {
    if (view != null) {
        if (view instanceof TextView) {
            ((TextView) view).setTextColor(Color.WHITE);
            ((TextView) view).setHintTextColor(Color.WHITE);
            ((TextView) view).setCursorVisible(true);
            return;
        } else if (view instanceof ViewGroup) {
            ViewGroup viewGroup = (ViewGroup) view;
            for (int i = 0; i < viewGroup.getChildCount(); i++) {
                changeSearchViewTextColor(viewGroup.getChildAt(i));
            }
        }
    }
}

@Override
public boolean onQueryTextSubmit(String query) {
    return true;
}

@Override
public boolean onQueryTextChange(String newText) {
    if (adapter != null) {
        adapter.filter2(newText);
    }
    return true;
}

Filter method inside Adapter class.

// Filter Class
public void filter2(String charText) {
    charText = charText.toLowerCase(Locale.getDefault());
    items.clear();
    if (charText.length() == 0) {
        items.addAll(arraylist);
    } else {
        for (EquityDetails wp : arraylist) {
            if (wp.getExpert_title().toLowerCase(Locale.getDefault()).contains(charText)) {
                items.add(wp);
            }
        }
    }
    notifyDataSetChanged();
}

Solution

  • 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.


    Solution: 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:

    public class ActivityName extends AppCompatActivity implements SearchView.OnQueryTextListener {
    
        @Override
        public boolean onCreateOptionsMenu(Menu menu) {
            getMenuInflater().inflate(R.menu.menu_main, menu);
    
            MenuItem item = menu.findItem(R.id.action_search);
            SearchView searchview = new SearchView(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);
    
            return super.onCreateOptionsMenu(menu);
        }
    
        private void changeSearchViewTextColor(View view) { ... }
    
        @Override
        public boolean onQueryTextSubmit(String query) { return false; }
    
        @Override
        public boolean onQueryTextChange(String newText) {
            // update the observer here (aka nested fragments)
            return true;
        }
    }
    

    #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:

    private ArrayList<String> originalList; // I used String objects in my tests
    private ArrayList<String> filteredList;
    private ListFilter filter = new ListFilter();
    
    @Override
    public int getCount() {
        return filteredList.size();
    }
    
    public Filter getFilter() {
        return filter;
    }
    
    private class ListFilter extends Filter {
        @Override
        protected FilterResults performFiltering(CharSequence constraint) {
            FilterResults results = new FilterResults();
            if (constraint != null && constraint.length() > 0) {
                constraint = constraint.toString().toLowerCase();
                final List<String> list = originalList;
                int count = list.size();
    
                final ArrayList<String> nlist = new ArrayList<>(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")
        @Override
        protected void publishResults(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:

    private static ActivityName instance;
    private FilterManager filterManager;
    
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        ...
        instance = this;
        filterManager = new FilterManager();
    }
    
    public static FilterManager getFilterManager() {
        return instance.filterManager; // return the observable class
    }
    
    @Override
    public boolean onQueryTextChange(String newText) {
        filterManager.setQuery(newText); // update the observable value
        return true;
    }
    

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

    public class FilterManager extends Observable {
        private String query;
    
        public void setQuery(String query) {
            this.query = query;
            setChanged();
            notifyObservers();
        }
    
        public String getQuery() {
            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:

    private ViewPager pager;
    private ViewPagerAdapter pagerAdapter;
    
    @Override
    public View onCreateView(...) {
        ...
        pagerAdapter = new ViewPagerAdapter(
             getActivity(),                    // pass the context,
             getChildFragmentManager(),        // the fragment manager
             MainActivity.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:

    public class ViewPagerAdapter extends FragmentStatePagerAdapter {
    
        private Context context;
        private FilterManager filterManager;
    
        public ViewPagerAdapter(FragmentManager fm) {
            super(fm);
        }
    
        public ViewPagerAdapter(Context context, FragmentManager fm, 
                   FilterManager filterManager) {
            super(fm);
            this.context = context;
            this.filterManager = filterManager;
        }
    
        @Override
        public Fragment getItem(int i) {
            NestedFragment fragment = new NestedFragment(); // see (*)
            filterManager.addObserver(fragment); // add the observer
            return fragment;
        }
    
        @Override
        public int getCount() {
            return 10;
        }
    
        @Override
        public void destroyItem(ViewGroup container, int position, Object object) {
            NestedFragment fragment = (NestedFragment) object; // see (*)
            filterManager.deleteObserver(fragment); // remove the observer
            super.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:

    public class NestedFragment extends Fragment implements Observer {
        private boolean 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
        @Override
        public void onResume() {
            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(new Runnable() {
                    @Override
                    public void run() {
                        listUpdated = true; // set the update checking value
                        adapter.getFilter().filter(query);
                    }
                });
            }
        }
        ...
        // automatically triggered when setChanged() and notifyObservers() are called
        public void update(Observable obs, Object obj) {
            if (obs instanceof FilterManager) {
                String result = ((FilterManager) obs).getQuery(); // retrieve the search value
                if (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:

      @Override
      public Fragment getItem(int index) {
          Fragment frag = null;
          switch (index) {
              case 0:
                  frag = new FirstNestedFragment();
                  break;
              case 1:
                  frag = new SecondFragment();
                  break;
              ...
          }
          return frag;
      }
      
      @Override
      public Object instantiateItem(ViewGroup container, int position) {
          ObserverFragment fragment = 
                  (ObserverFragment) super.instantiateItem(container, position);
          filterManager.addObserver(fragment); // add the observer
          return fragment;
      }
      
      @Override
      public void destroyItem(ViewGroup container, int position, Object object) {
          filterManager.deleteObserver((ObserverFragment) object); // delete the observer
          super.destroyItem(container, position, object);
      }
      

      By creating the ObserverFragment class as follows:

      public class ObserverFragment extends Fragment implements Observer {
          public void update(Observable obs, Object obj) { /* do nothing here */ }
      }
      

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

      public class FirstNestedFragment extends ObserverFragment {
          @Override
          public void update(Observable obs, Object obj) { }
      }