Real-time filtering RecyclingView populated from Firestore by replacing adapter with updated query shows really poor performance.
To filter RecyclerView
in my app in real time(onQueryTextChange
) I am using custom adapter that extends FirestoreRecyclerAdapter
and every time I need to filter data I create a new Query
object, create new FirestoreRecyclerOptions
object then create my custom adapter object where I pass FirestoreRecyclerOptions
object to FirestoreRecyclerAdapter
constructor and finally I call swapAdapter
on my RecyclerView
. The problem with this approach is that it results in a poor performance. I can see the view blinking every time the query is updated.
DocumentSearchActivity.java
package com.example.testapp;
import android.app.Activity;
import android.app.SearchManager;
import android.arch.lifecycle.LifecycleOwner;
import android.content.Context;
import android.content.Intent;
import android.os.Bundle;
import android.support.annotation.Nullable;
import android.support.v7.app.AppCompatActivity;
import android.support.v7.widget.LinearLayoutManager;
import android.support.v7.widget.RecyclerView;
import android.support.v7.widget.Toolbar;
import android.view.Menu;
import android.view.View;
import android.view.animation.AnimationUtils;
import android.view.animation.LayoutAnimationController;
import android.view.inputmethod.InputMethodManager;
import android.widget.SearchView;
import android.widget.TextView;
import com.example.testapp.adapter.ItemAdapter;
import com.example.testapp.model.Item;
import com.firebase.ui.firestore.FirestoreRecyclerOptions;
import com.google.firebase.firestore.FirebaseFirestore;
import com.google.firebase.firestore.Query;
public class DocumentSearchActivity extends AppCompatActivity {
private static final String TAG = DocumentSearchActivity.class.getSimpleName();
private RecyclerView mRecyclerView;
private ItemAdapter mAdapter;
private RecyclerView.LayoutManager mLayoutManager;
private TextView mNoResultsTextView;
private SearchView mSearchView;
private FirestoreQuery mQuery;
private Toolbar mToolbar;
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_document_search);
mToolbar = findViewById(R.id.app_bar);
setSupportActionBar(mToolbar);
getSupportActionBar().setDisplayShowTitleEnabled(false);
mRecyclerView = findViewById(R.id.recycler_view);
mSearchView = findViewById(R.id.search_bar);
mNoResultsTextView = findViewById(R.id.result);
mNoResultsTextView.setVisibility(View.GONE);
if(getIntent().hasExtra(MainActivity.QUERY_EXTRA)){
mSearchView.setQuery(getIntent().getStringExtra(MainActivity.QUERY_EXTRA), true);
mSearchView.requestFocus();
}
mQuery = new FirestoreQuery(this, "");
mLayoutManager = new LinearLayoutManager(this);
mRecyclerView.setLayoutManager(mLayoutManager);
Intent intent = getIntent();
if(Intent.ACTION_SEARCH.equals(intent.getAction())) {
mQuery.setQueryString(intent.getStringExtra(SearchManager.QUERY));
}
mAdapter = new ItemAdapter(mQuery.getOptions());
mRecyclerView.setAdapter(mAdapter);
mSearchView.setOnQueryTextListener(new SearchView.OnQueryTextListener() {
@Override
public boolean onQueryTextSubmit(String query) {
return true;
}
@Override
public boolean onQueryTextChange(String newText) {
mAdapter = new ItemAdapter(mQuery.setQueryString(newText).getOptions());
mRecyclerView.swapAdapter(mAdapter, true);
mAdapter.notifyDataSetChanged();
mAdapter.startListening();
return true;
}
});
}
private void runLayoutAnimation(final RecyclerView recyclerView) {
final Context context = recyclerView.getContext();
final LayoutAnimationController controller =
AnimationUtils.loadLayoutAnimation(context, R.anim.layout_animation_fall_down);
recyclerView.setLayoutAnimation(controller);
recyclerView.getAdapter().notifyDataSetChanged();
recyclerView.scheduleLayoutAnimation();
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
getMenuInflater().inflate(R.menu.actions_document_search_activity, menu);
return super.onCreateOptionsMenu(menu);
}
private class FirestoreQuery {
private LifecycleOwner mLifecycleOwner;
private String mQueryString;
private Query mQuery;
private FirestoreRecyclerOptions<Item> mOptions;
public FirestoreQuery(LifecycleOwner lifecycleOwner, String query) {
mLifecycleOwner = lifecycleOwner;
setQueryString(query);
}
public FirestoreQuery setQueryString(String queryString) {
if(queryString.length() != 0)
this.mQueryString = queryString.substring(0,1).toUpperCase() + queryString.substring(1).toLowerCase();
else
this.mQueryString = queryString;
updateQuery();
updateOptions();
return this;
}
private void updateOptions() {
mOptions = new FirestoreRecyclerOptions.Builder<Item>()
.setQuery(mQuery, Item.class)
.setLifecycleOwner(mLifecycleOwner)
.build();
}
private void updateQuery() {
if(mQueryString.isEmpty()){
this.mQuery = FirebaseFirestore.getInstance()
.collection("tubes_test")
.orderBy("number");
} else {
StringBuilder query_lower = new StringBuilder(mQueryString.length());
query_lower.append(mQueryString);
query_lower.setCharAt(mQueryString.length() - 1, (char) (query_lower.charAt(mQueryString.length() - 1) + 1));
if (mQueryString.matches("^[0-9]{1,3}[A-Z]*$")) {
this.mQuery = FirebaseFirestore.getInstance()
.collection("tubes_test")
.whereGreaterThanOrEqualTo("number", mQueryString)
.whereLessThan("number", query_lower.toString());
} else {
this.mQuery = FirebaseFirestore.getInstance()
.collection("tubes_test")
.whereGreaterThanOrEqualTo("name", mQueryString)
.whereLessThan("name", query_lower.toString());
}
}
}
public FirestoreRecyclerOptions<Item> getOptions() {
return mOptions;
}
}
}
ItemAdapter.java
package com.example.testapp.adapter;
import android.content.Context;
import android.content.Intent;
import android.support.annotation.NonNull;
import android.support.v7.widget.RecyclerView;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Button;
import android.widget.TextView;
import com.example.testapp.DocumentViewerActivity;
import com.example.testapp.R;
import com.example.testapp.model.Item;
import com.firebase.ui.firestore.FirestoreRecyclerAdapter;
import com.firebase.ui.firestore.FirestoreRecyclerOptions;
public class ItemAdapter extends FirestoreRecyclerAdapter<Item, ItemAdapter.ItemHolder> {
public ItemAdapter(@NonNull FirestoreRecyclerOptions<Item> options) {
super(options);
}
@Override
protected void onBindViewHolder(@NonNull ItemHolder holder, int position, @NonNull Item item) {
final String tubeNumber = item.getNumber();
holder.mNumberTextView.setText(item.getNumber());
holder.mNameTextView.setText(item.getName());
holder.mFormulaTextView.setText(item.getFormula());
holder.mRangeTextView.setText(item.getRange());
holder.mInfoButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Intent intent = new Intent(v.getContext(), DocumentViewerActivity.class);
intent.putExtra("number", tubeNumber);
intent.putExtra("doctype", "info");
v.getContext().startActivity(intent);
}
});
holder.mManualButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Intent intent = new Intent(v.getContext(), DocumentViewerActivity.class);
intent.putExtra("number", tubeNumber);
intent.putExtra("doctype", "manual");
v.getContext().startActivity(intent);
}
});
}
@NonNull
@Override
public ItemHolder onCreateViewHolder(@NonNull ViewGroup viewGroup, int i) {
Context context = viewGroup.getContext();
LayoutInflater inflater = LayoutInflater.from(context);
View tubeView = inflater.inflate(R.layout.item_tube, viewGroup, false);
return new ItemHolder(tubeView);
}
public static class ItemHolder extends RecyclerView.ViewHolder {
public TextView mNumberTextView;
public TextView mNameTextView;
public TextView mFormulaTextView;
public TextView mRangeTextView;
public Button mInfoButton;
public Button mManualButton;
public ItemHolder(View itemView){
super(itemView);
mNumberTextView = itemView.findViewById(R.id.number);
mNameTextView = itemView.findViewById(R.id.name);
mFormulaTextView = itemView.findViewById(R.id.formula);
mRangeTextView = itemView.findViewById(R.id.range);
mInfoButton = itemView.findViewById(R.id.info_btn);
mManualButton = itemView.findViewById(R.id.manual_btn);
}
}
}
I wonder if there is a better way to solve this problem. Also, I don't want to lose sweet Firestore functionality such as live data updates and offline access to the database. I was thinking about using a filterable List in my adapter, the question is how do i keep it in sync with Firestore. Please share your insights and guidelines.
Step 1: Open Android Studio and create a new project named “RecyclerView” with an empty activity. Step 2: Connect your Firebase project with your app. Step 3: Add the following dependency in your app/build. gradle file in order to get the FirebaseUI and Firebase Realtime Database support in the app.
How to add RecyclerView in the android app and display the data in Firebase Realtime Database. Log in to Firebase with your Google account if are not already logged in. Click on create the project. Write the name. Click on continue. Click on the toggle button. Click Continue. Firebase will create a project for you and open it for you.
FirestoreRecyc l erAdapter is an adapter class from Firebase UI Database library. So, how do we get started? Before you create the project in Android Studio, you should: Create a Firebase Project here. Go to Database menu and choose Cloud Firestore. Add Collection named "friends".
This example demonstrates how to filter a RecyclerView with a SearchView on Android. Step 1 − Create a new project in Android Studio, go to File ⇒ New Project and fill all required details to create a new project. Step 2 − Add the following code to res/layout/activity_main.xml. Step 3 − Add the following code to src/MainActivity.java
This is one type of RecyclerView adapters offered by the FirebaseUI for Firestore. How does it work? It binds the results of a query to a recyclerview while responding to all the real-time events i.e. items added/removed/moved/changed. It is advised to use this option when querying small result sets since all results are loaded at once.
Here is the solution that worked for me. Basically, I created a custom filterable adapter that I populate with data from Firestore once only.
CustomItemAdapter.java
package com.example.testapp.adapter;
import android.content.Intent;
import android.support.annotation.NonNull;
import android.support.v7.widget.RecyclerView;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Button;
import android.widget.Filter;
import android.widget.Filterable;
import android.widget.TextView;
import com.example.testapp.DocumentViewerActivity;
import com.example.testapp.R;
import com.example.testapp.model.Item;
import java.util.ArrayList;
import java.util.List;
public class CustomItemAdapter extends RecyclerView.Adapter<CustomItemAdapter.ItemHolder> implements Filterable {
private List<Item> mTubeList;
private List<Item> mTubeListFiltered;
public CustomItemAdapter(List<Item> tubeList){
mTubeList = tubeList;
mTubeListFiltered = tubeList;
}
@NonNull
@Override
public ItemHolder onCreateViewHolder(@NonNull ViewGroup viewGroup, int i) {
View tubeView = LayoutInflater.from(viewGroup.getContext())
.inflate(R.layout.item_tube, viewGroup, false);
return new ItemHolder(tubeView);
}
@Override
public void onBindViewHolder(@NonNull ItemHolder holder, int i) {
final Item item = mTubeListFiltered.get(i);
//Setting up view
}
@Override
public int getItemCount() {
return mTubeListFiltered.size();
}
@Override
public Filter getFilter() {
return new Filter() {
@Override
protected FilterResults performFiltering(CharSequence constraint) {
String pattern = constraint.toString().toLowerCase();
if(pattern.isEmpty()){
mTubeListFiltered = mTubeList;
} else {
List<Item> filteredList = new ArrayList<>();
for(Item tube: mTubeList){
if(tube.getNumber().toLowerCase().contains(pattern) || tube.getName().toLowerCase().contains(pattern)) {
filteredList.add(tube);
}
}
mTubeListFiltered = filteredList;
}
FilterResults filterResults = new FilterResults();
filterResults.values = mTubeListFiltered;
return filterResults;
}
@Override
protected void publishResults(CharSequence constraint, FilterResults results) {
mTubeListFiltered = (ArrayList<Item>) results.values;
notifyDataSetChanged();
}
};
}
public static class ItemHolder extends RecyclerView.ViewHolder {
//ViewHolder's code
}
}
Snippet from Activity:
Query query = FirebaseFirestore.getInstance()
.collection("test")
.orderBy("number");
query.get().addOnCompleteListener(new OnCompleteListener<QuerySnapshot>() {
@Override
public void onComplete(@NonNull Task<QuerySnapshot> task) {
mAdapter = new CustomItemAdapter(task.getResult().toObjects(Item.class));
mRecyclerView.setAdapter(mAdapter);
}
});
mSearchView.setOnQueryTextListener(new SearchView.OnQueryTextListener() {
@Override
public boolean onQueryTextSubmit(String query) {
return true;
}
@Override
public boolean onQueryTextChange(String newText) {
mAdapter.getFilter().filter(newText);
return true;
}
});
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With