Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to implement a filter for RecyclerView populated from Firestore?

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.

like image 471
aglazkov Avatar asked Jan 25 '19 17:01

aglazkov


People also ask

How do I get data to my RecyclerView adapter?

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 Android app and display data in Firebase?

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.

How to create friends FireStore recycling adapter in Android Studio?

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

How to filter a recyclerview with a searchview on Android?

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

What is recyclerview adapter in firebaseui for FireStore?

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.


1 Answers

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;
            }
        });
like image 122
aglazkov Avatar answered Oct 15 '22 16:10

aglazkov