Filtering RecyclerView's list with LiveData co

2019-05-27 03:46发布

问题:

I've created RecyclerView which contains simple list of words (String names of GroupVc objects). As list can be very long I want to make it filterable with SearchView in Toolbar. Architecture of my application is based on Android Architecture Components, where all GroupVc objects exist in Room database and access to DataBase from UI is provided through ViewModel and Repository objects. I'm filing my RecyclerView with list of all GroupsVc wrapped in LiveData to keep it updated. My problem is that I don't know how to make RecyclerView filterable. I tried to do it with implementing of Filterable Interface in adapter:

public class GroupsVcAdapter extends
    RecyclerView.Adapter<GroupsViewHolder> implements Filterable{

private LayoutInflater mInflater;
private List<GroupVc> mGroupsVc;
private List<GroupVc> filteredGroupsVc;
private OnItemClicked onClick;
public GroupsVcAdapter(Context context, OnItemClicked onClick) {
    mInflater = LayoutInflater.from(context);
    this.onClick = onClick;
}

public List<GroupVc> getmGroupsVc() {
    return mGroupsVc;
}

@Override
public GroupsViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
    View itemView = mInflater.inflate(R.layout.layout_list_of_groups, parent, false);
    return new GroupsViewHolder(itemView, onClick);
}

@Override
public void onBindViewHolder(final GroupsViewHolder holder, int position) {
    if (mGroupsVc != null) {
        GroupVc current = mGroupsVc.get(position);
        holder.getNameView().setText(current.getNameGroup());
    } else {
        holder.getNameView().setText(R.string.nogroups);
    }
}

public void setGroupsVc(List<GroupVc> mGroupsVc) {
    this.mGroupsVc = mGroupsVc;
    notifyDataSetChanged();
}

@Override
public int getItemCount() {
    if (mGroupsVc != null)
        return mGroupsVc.size();
    else return 0;
}

@Override
public Filter getFilter() {
    return new Filter() {
        @Override
        protected void publishResults(CharSequence constraint, FilterResults results) {
                mGroupsVc = (List<GroupVc>) results.values;
                notifyDataSetChanged();
        }

        @Override
        protected FilterResults performFiltering(CharSequence constraint) {
            filteredGroupsVc = null;
            if (constraint.length() == 0) {
                filteredGroupsVc = mGroupsVc;
            } else {
                filteredGroupsVc = getFilteredResults(constraint.toString().toLowerCase());
            }

            FilterResults results = new FilterResults();
            results.values = filteredGroupsVc;
            return results;
        }
    };
}

protected List<GroupVc> getFilteredResults(String constraint) {
    List<GroupVc> results = new ArrayList<>();

    for (GroupVc item : mGroupsVc) {
        if (item.getNameGroup().toLowerCase().contains(constraint)) {
            results.add(item);
        }
    }
    return results;
}
}

And then in Activity I've written a method:

@Override
public boolean onCreateOptionsMenu(Menu menu) {
    MenuInflater inflater = getMenuInflater();
    inflater.inflate(R.menu.menu_activity_words, menu);
    // Associate searchable configuration with the SearchView
    SearchManager searchManager =
            (SearchManager) getSystemService(Context.SEARCH_SERVICE);
    SearchView searchView =
            (SearchView) menu.findItem(R.id.action_search).getActionView();
    searchView.setSearchableInfo(
            searchManager.getSearchableInfo(getComponentName()));
    searchView.setOnQueryTextListener(new SearchView.OnQueryTextListener() {
        @Override
        public boolean onQueryTextSubmit(String text) {
            return false;
        }

        @Override
        public boolean onQueryTextChange(String text) {
            adapter.getFilter().filter(text);
            return true;
        }
    });
    return true;
}

The result was that my RecyclerView got filtered correctly but it wasn't restored after I closed SearchView. The only way to do it is to reload activity. **So I'm interested in How can I restore RecyclerView's list and can I use for filtering any LiveData's capacities?** Below I post the complete code of Activity, ViewModel and Repository:

Activity

public class GroupsActivity extends AppCompatActivity {

private static final String DELETE_DIALOG = "Delete dialog";
private static final String EDIT_DIALOG = "Edit dialog";
private RecyclerView mRecyclerView;
private GroupsVcAdapter adapter;
private GroupsViewModel mGroupsViewModel;

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_groups);
    //Creating of toolbar with title Groups
    Toolbar myToolbar = findViewById(R.id.toolbar_groups);
    setSupportActionBar(myToolbar);
    //Enable Up Button
    ActionBar ab = getSupportActionBar();
    ab.setDisplayHomeAsUpEnabled(true);
    //RecyclerView containing the list of groups with sound icons
    mRecyclerView = initRecyclerView();
    //Using ViewModel to observe GroupVc data
    mGroupsViewModel = ViewModelProviders.of(this).get(GroupsViewModel.class);
    mGroupsViewModel.getAllGroups().observe(this, new Observer<List<GroupVc>>() {
        @Override
        public void onChanged(@Nullable List<GroupVc> groupVcs) {
            adapter.setGroupsVc(groupVcs);
        }
    });
}

private RecyclerView initRecyclerView() {
    RecyclerView recyclerView = findViewById(R.id.groups_recycler_view);
    LinearLayoutManager layoutManager = new LinearLayoutManager(this);
    recyclerView.setLayoutManager(layoutManager);
    OnItemClicked listener = (v, position) -> {
        switch (v.getId()) {
            case R.id.delete_group:
                Log.i(DELETE_DIALOG, "Delete button");
                break;
            case R.id.edit_group:
                Log.i(EDIT_DIALOG, "Edit button");
                break;
            case R.id.surface:
                Log.i(SURFACE, "Surface button");
                break;
        }
    };
    adapter = new GroupsVcAdapter(this, listener);
    recyclerView.setAdapter(adapter);
    return recyclerView;
}

@Override
public boolean onCreateOptionsMenu(Menu menu) {
    MenuInflater inflater = getMenuInflater();
    inflater.inflate(R.menu.menu_activity_words, menu);
    // Associate searchable configuration with the SearchView
    SearchManager searchManager =
            (SearchManager) getSystemService(Context.SEARCH_SERVICE);
    SearchView searchView =
            (SearchView) menu.findItem(R.id.action_search).getActionView();
    searchView.setSearchableInfo(
            searchManager.getSearchableInfo(getComponentName()));
    searchView.setOnQueryTextListener(new SearchView.OnQueryTextListener() {
        @Override
        public boolean onQueryTextSubmit(String text) {
            return false;
        }

        @Override
        public boolean onQueryTextChange(String text) {
            adapter.getFilter().filter(text);
            return true;
        }
    });
    return true;
}
}

ViewModel

public class GroupsViewModel extends AndroidViewModel {

private LiveData<List<GroupVc>> mAllGroups;
private GroupRepository mRepository;

public GroupsViewModel(@NonNull Application application) {
    super(application);
    mRepository = new GroupRepository(application);
    mAllGroups = mRepository.getAllGroupVc();
}

public LiveData<List<GroupVc>> getAllGroups() {
    return mAllGroups;
}
}

Repository

public class GroupRepository {
private LiveData<List<GroupVc>> mAllGroups;
private GroupVcDao mGroupVcDao;

public GroupRepository(Application application) {
    AppDatabase db = AppDatabase.getInstance(application);
    mGroupVcDao = db.groupVcDao();
    mAllGroups = mGroupVcDao.getAllGroupVc();
}

public LiveData<List<GroupVc>> getAllGroupVc() {
    return mAllGroups;
}
}

回答1:

The problem that your data is not restored correctly when you delete the filter is, because you overwrite your data list when you publish the filter result.

Here is the solution:

public GroupsVcAdapter(Context context, OnItemClicked onClick) {
  mInflater = LayoutInflater.from(context);
  this.onClick = onClick;

  // init the lists
  mGroupsVc = new ArrayList<>();
  filteredGroupsVc = new ArrayList<>();
}

public List<GroupVc> getFilteredGroupsVc() {
  return filteredGroupsVc;
}

@Override
public int getItemCount() {
  return filteredGroupsVc != null ? filteredGroupsVc.size() : 0;
}

@Override
public Filter getFilter() {
  return new Filter() {
    @Override
    protected void publishResults(CharSequence constraint, FilterResults results) {
      filteredGroupsVc = (List<GroupVc>) results.values;
      notifyDataSetChanged();
    }

    @Override
    protected FilterResults performFiltering(CharSequence constraint) {
      filteredGroupsVc.clear();
      if (constraint.length() == 0) {
        filteredGroupsVc.addAll(mGroupsVc);
      else {
        filteredGroupsVc = getFilteredResults(constraint.toString().toLowerCase());
      }

      FilterResults results = new FilterResults();
      results.values = filteredGroupsVc;
      results.count = filteredGroupsVc.size();
      return results;
    }
  };
}

If you set a new list of data to the adapter, you have to call adapter.getFilter().filter(text); or you save the last filter string inside your adapter and call your filter inside setGroupsVc()

Note: You don't have any animations if you use notifyDataSetChanged();. If you want to have animations, use the other notify methods.

Hope this helps.

EDIT

Also update onBindViewHolder() to get the data from your filtered list.

@Override
public void onBindViewHolder(final GroupsViewHolder holder, int position) {
  if (filteredGroupsVc != null) {
    GroupVc current = filteredGroupsVc.get(position);
    holder.getNameView().setText(current.getNameGroup());
  } else {
    holder.getNameView().setText(R.string.nogroups);
  }
}

Now you always get the data from the filtered list. The initial state is that your lists is empty. That's what you saw in your RecyclerView. If you call setGroupsVc() you set a new list to the adapter. Remember you always get the data from the filtered list. So you have to update the filtered list (old data) with the new list which you set to the adapter (new data).

You have 2 options:

  1. Call filter on the outside after you call setGroupsVc():

Like

...
setGroupsVc(yourNewList);
adapter.getFilter().filter(searchview.getQuery());
...
  1. Save the last filter in your adapter and call filter inside your setGroupsVc():

Add new field lastFilter in your adapter

private String lastFilter = "";

Inside performFiltering() save the filter string

lastFilter = contraint.toString();

At least call filter in setGroupsVc()

public void setGroupsVc(List<GroupVc> mGroupsVc) {
  this.mGroupsVc = mGroupsVc;
  getFilter().filter(lastFilter);
  // notifyDataSetChanged(); -> not necessary anymore because we already call this inside our filter
}