Skip to content

Instantly share code, notes, and snippets.

@Kishanjvaghela
Last active January 14, 2021 03:02
Show Gist options
  • Save Kishanjvaghela/67c42f8f32efaa2fadb682bc980e9280 to your computer and use it in GitHub Desktop.
Save Kishanjvaghela/67c42f8f32efaa2fadb682bc980e9280 to your computer and use it in GitHub Desktop.
Custom Filterable FirebaseRecyclerAdapter
import android.databinding.DataBindingUtil;
import android.support.v7.widget.RecyclerView;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import com.app.wanna.android.R;
import com.app.wanna.android.data.User;
import com.app.wanna.android.databinding.LayoutItemPeopleBinding;
import com.app.wanna.android.utils.firebaseadapter.FirebaseRecyclerAdapter;
import com.firebase.ui.common.ChangeEventType;
import com.firebase.ui.database.FirebaseRecyclerOptions;
import com.google.firebase.database.DataSnapshot;
import com.squareup.picasso.Picasso;
public class ExamplePeopleAdapter extends FirebaseRecyclerAdapter<User, PeopleListAdapter.PeopleViewHolder> {
private RecycleItemClick recycleItemClick;
private static final String TAG = "PeopleListAdapter";
public ExamplePeopleAdapter(FirebaseRecyclerOptions<User> options) {
super(options, true);
}
public interface RecycleItemClick {
void onItemClick(String userId, User user, int position);
}
public void setRecycleItemClick(RecycleItemClick recycleItemClick) {
this.recycleItemClick = recycleItemClick;
}
@Override
public PeopleViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
View view = LayoutInflater.from(parent.getContext())
.inflate(R.layout.layout_item_people, parent, false);
return new PeopleViewHolder(view);
}
@Override
protected void onBindViewHolder(PeopleViewHolder holder, int position, User model) {
holder.bind(model);
}
@Override
protected void onChildUpdate(User model,
ChangeEventType type,
DataSnapshot snapshot,
int newIndex,
int oldIndex) {
model.setUserId(snapshot.getKey());
super.onChildUpdate(model, type, snapshot, newIndex, oldIndex);
}
@Override
protected boolean filterCondition(User model, String filterPattern) {
return model.getFirstName().toLowerCase().contains(filterPattern) ||
model.getLastName().toLowerCase().contains(filterPattern);
}
public class PeopleViewHolder extends RecyclerView.ViewHolder {
LayoutItemPeopleBinding mBinding;
PeopleViewHolder(View view) {
super(view);
mBinding = DataBindingUtil.bind(view);
}
public void bind(User user) {
Picasso.with(mBinding.peopleImage.getContext())
.load(user.getImage())
.placeholder(R.drawable.place_holder_user)
.into(mBinding.peopleImage);
mBinding.peopleName.setText(String.format("%s %s", user.getFirstName(), user.getLastName()));
itemView.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
int pos = getAdapterPosition();
User user = getItem(pos);
recycleItemClick.onItemClick(user.getUserId(), user, pos);
}
});
}
}
}
import android.arch.lifecycle.LifecycleObserver;
import android.support.annotation.RestrictTo;
import com.firebase.ui.database.ChangeEventListener;
import com.firebase.ui.database.FirebaseArray;
import com.firebase.ui.database.ObservableSnapshotArray;
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
interface FirebaseAdapter<T> extends ChangeEventListener, LifecycleObserver {
/**
* If you need to do some setup before the adapter starts listening for change events in the
* database, do so it here and then call {@code super.startListening()}.
*/
void startListening();
/**
* Removes listeners and clears all items in the backing {@link FirebaseArray}.
*/
void stopListening();
ObservableSnapshotArray<T> getSnapshots();
T getItem(int position);
}
import android.arch.lifecycle.Lifecycle;
import android.arch.lifecycle.LifecycleOwner;
import android.arch.lifecycle.OnLifecycleEvent;
import android.support.v7.widget.RecyclerView;
import android.util.Log;
import android.widget.Filter;
import android.widget.Filterable;
import com.firebase.ui.common.ChangeEventType;
import com.firebase.ui.database.FirebaseRecyclerOptions;
import com.firebase.ui.database.ObservableSnapshotArray;
import com.google.firebase.database.DataSnapshot;
import com.google.firebase.database.DatabaseError;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
/**
* This class is a generic way of backing a {@link RecyclerView} with a Firebase location. It
* handles all of the child events at the given Firebase location and marshals received data into
* the given class type.
* <p>
* See the <a href="https://github.com/firebase/FirebaseUI-Android/blob/master/database/README.md">README</a>
* for an in-depth tutorial on how to set up the FirebaseRecyclerAdapter.
*
* @param <T> The Java class that maps to the type of objects stored in the Firebase location.
* @param <VH> The {@link RecyclerView.ViewHolder} class that contains the Views in the layout that
* is shown for each object.
*/
public abstract class FirebaseRecyclerAdapter<T, VH extends RecyclerView.ViewHolder>
extends RecyclerView.Adapter<VH> implements FirebaseAdapter<T>, Filterable {
private static final String TAG = "FirebaseRecyclerAdapter";
private final ObservableSnapshotArray<T> mSnapshots;
private final List<T> list, backupList;
private CustomFilter mCustomFilter;
private boolean isFiltarable;
/**
* Initialize a {@link RecyclerView.Adapter} that listens to a Firebase query. See
* {@link FirebaseRecyclerOptions} for configuration options.
*/
public FirebaseRecyclerAdapter(FirebaseRecyclerOptions<T> options, boolean isFiltarable) {
mSnapshots = options.getSnapshots();
list = new ArrayList<>();
backupList = new ArrayList<>();
if (options.getOwner() != null) {
options.getOwner().getLifecycle().addObserver(this);
}
this.isFiltarable = isFiltarable;
}
@OnLifecycleEvent(Lifecycle.Event.ON_START)
public void startListening() {
if (!mSnapshots.isListening(this)) {
mSnapshots.addChangeEventListener(this);
}
}
@OnLifecycleEvent(Lifecycle.Event.ON_STOP)
public void stopListening() {
mSnapshots.removeChangeEventListener(this);
notifyDataSetChanged();
}
@OnLifecycleEvent(Lifecycle.Event.ON_DESTROY)
void cleanup(LifecycleOwner source) {
source.getLifecycle().removeObserver(this);
}
@Override
public void onChildChanged(ChangeEventType type,
DataSnapshot snapshot,
int newIndex,
int oldIndex) {
T model = mSnapshots.get(newIndex);
onChildUpdate(model, type, snapshot, newIndex, oldIndex);
}
protected void onChildUpdate(T model, ChangeEventType type,
DataSnapshot snapshot,
int newIndex,
int oldIndex) {
switch (type) {
case ADDED:
addItem(snapshot.getKey(), model);
notifyItemInserted(newIndex);
break;
case CHANGED:
addItem(snapshot.getKey(), model, newIndex);
notifyItemChanged(newIndex);
break;
case REMOVED:
removeItem(newIndex);
notifyItemRemoved(newIndex);
break;
case MOVED:
moveItem(snapshot.getKey(), model, newIndex, oldIndex);
notifyItemMoved(oldIndex, newIndex);
break;
default:
throw new IllegalStateException("Incomplete case statement");
}
}
private void moveItem(String key, T t, int newIndex, int oldIndex) {
list.remove(oldIndex);
list.add(newIndex, t);
if (isFiltarable) {
backupList.remove(oldIndex);
backupList.add(newIndex, t);
}
}
private void removeItem(int newIndex) {
list.remove(newIndex);
if (isFiltarable)
backupList.remove(newIndex);
}
private void addItem(String key, T t, int newIndex) {
list.remove(newIndex);
list.add(newIndex, t);
if (isFiltarable) {
backupList.remove(newIndex);
backupList.add(newIndex, t);
}
}
private void addItem(String id, T t) {
list.add(t);
if (isFiltarable)
backupList.add(t);
}
@Override
public void onDataChanged() {
}
@Override
public void onError(DatabaseError error) {
Log.w(TAG, error.toException());
}
@Override
public ObservableSnapshotArray<T> getSnapshots() {
return mSnapshots;
}
@Override
public T getItem(int position) {
return list.get(position);
}
@Override
public int getItemCount() {
return list.size();
}
@Override
public void onBindViewHolder(VH holder, int position) {
onBindViewHolder(holder, position, getItem(position));
}
/**
* @param model the model object containing the data that should be used to populate the view.
* @see #onBindViewHolder(RecyclerView.ViewHolder, int)
*/
protected abstract void onBindViewHolder(VH holder, int position, T model);
/**
* filter condition for Filter
*
* @param model model T
* @param filterPattern filter pattern with Lower Case
*/
protected boolean filterCondition(T model, String filterPattern) {
return true;
}
@Override
public Filter getFilter() {
if (mCustomFilter == null) {
mCustomFilter = new CustomFilter();
}
return mCustomFilter;
}
public class CustomFilter extends Filter {
@Override
protected FilterResults performFiltering(CharSequence constraint) {
final FilterResults results = new FilterResults();
if (constraint.length() == 0) {
results.values = backupList;
results.count = backupList.size();
} else {
List<T> filteredList = new ArrayList<>();
final String filterPattern = constraint.toString().toLowerCase().trim();
for (T t : backupList) {
if (filterCondition(t, filterPattern)) {
filteredList.add(t);
}
}
results.values = filteredList;
results.count = filteredList.size();
}
return results;
}
@Override
protected void publishResults(CharSequence constraint, FilterResults results) {
list.clear();
list.addAll((Collection<? extends T>) results.values);
notifyDataSetChanged();
}
}
}
@Kishanjvaghela
Copy link
Author

Kishanjvaghela commented Nov 1, 2017

Custom Filterable FirebaseRecyclerAdapter

  1. Pass true as isFiltarable in adapter's constructer to support filter in adapter
    FirebaseRecyclerAdapter(FirebaseRecyclerOptions<T> options, boolean isFiltarable)

  2. Override filter condition in your adapter

@Override
    protected boolean filterCondition(User model, String filterPattern) {
        return model.getFirstName().toLowerCase().contains(filterPattern) ||
                model.getLastName().toLowerCase().contains(filterPattern);
}
  1. Currently don't have support for DatabaseReference getRef(int position) method. If you want to get key of Data then you can override onChildUpdate method and manipulate your model object and set id with snapshot.getKey().
    protected void onChildUpdate(User model, ChangeEventType type, DataSnapshot snapshot, int newIndex, int oldIndex) {
        model.setUserId(snapshot.getKey());
        super.onChildUpdate(model, type, snapshot, newIndex, oldIndex);
    }

Dont forget to Exclude Id key if its dont have field in your database

public class User {
    @Exclude
    String userId;

    @Exclude
    public String getUserId() {
        return userId;
    }

    public void setUserId(String userId) {
        this.userId = userId;
    }
}

That's It.
Now you can filter your Recyclerview using

peopleListAdapter.getFilter().filter(newText);

@dreamchaser96
Copy link

Do I have overwrite basic FirebaseAdapter? If yes, could you guide me how to customize it?

@galihlprakoso
Copy link

Thank you so much! you saved me!

@galihlprakoso
Copy link

GOD bless you!

@GuittonCandice
Copy link

Thank you a lot for this

@ahmadghufran
Copy link

May Allah give you Happiness, for this Kind work!

@Yair0007
Copy link

Is this applicable to FirestoreRecyclerAdapter as well ? thanks

@fixtse
Copy link

fixtse commented Jan 28, 2019

Really nice job, thank you, you saved me alot of time.

@boberproduction
Copy link

boberproduction commented Mar 12, 2019

Is this applicable to FirestoreRecyclerAdapter as well ? thanks

Here's the Firestore adapter:

import android.arch.lifecycle.Lifecycle;
import android.arch.lifecycle.LifecycleObserver;
import android.arch.lifecycle.LifecycleOwner;
import android.arch.lifecycle.OnLifecycleEvent;
import android.support.annotation.NonNull;
import android.support.v7.widget.RecyclerView;
import android.util.Log;
import android.widget.Filter;
import android.widget.Filterable;

import com.firebase.ui.common.ChangeEventType;
import com.firebase.ui.firestore.ChangeEventListener;
import com.firebase.ui.firestore.FirestoreRecyclerOptions;
import com.firebase.ui.firestore.ObservableSnapshotArray;
import com.google.firebase.firestore.DocumentSnapshot;
import com.google.firebase.firestore.FirebaseFirestoreException;

import java.util.ArrayList;
import java.util.Collection;
import java.util.List;

/**
 * @param <T>  model class, for parsing {@link DocumentSnapshot}s.
 * @param <VH> {@link RecyclerView.ViewHolder} class.
 */
public abstract class FilterableFirestoreRecyclerAdapter<T, VH extends RecyclerView.ViewHolder>
        extends RecyclerView.Adapter<VH>
        implements ChangeEventListener, LifecycleObserver, Filterable {

    private static final String TAG = "FirestoreRecycler";

    private final ObservableSnapshotArray<T> mSnapshots;
    private final List<T> list, backupList;
    private CustomFilter mCustomFilter;
    private boolean isFiltarable;

    /**
     * Create a new RecyclerView adapter that listens to a Firestore Query.  See {@link
     * FirestoreRecyclerOptions} for configuration options.
     */
    public FilterableFirestoreRecyclerAdapter(@NonNull FirestoreRecyclerOptions<T> options, boolean isFiltarable) {
        mSnapshots = options.getSnapshots();

        list = new ArrayList<>();
        backupList = new ArrayList<>();
        if (options.getOwner() != null) {
            options.getOwner().getLifecycle().addObserver(this);
        }
        this.isFiltarable = isFiltarable;
    }

    /**
     * Start listening for database changes and populate the adapter.
     */
    @OnLifecycleEvent(Lifecycle.Event.ON_START)
    public void startListening() {
        if (!mSnapshots.isListening(this)) {
            mSnapshots.addChangeEventListener(this);
        }
    }

    /**
     * Stop listening for database changes and clear all items in the adapter.
     */
    @OnLifecycleEvent(Lifecycle.Event.ON_STOP)
    public void stopListening() {
        mSnapshots.removeChangeEventListener(this);
        notifyDataSetChanged();
    }

    @OnLifecycleEvent(Lifecycle.Event.ON_DESTROY)
    void cleanup(LifecycleOwner source) {
        source.getLifecycle().removeObserver(this);
    }

    /**
     * Returns the backing {@link ObservableSnapshotArray} used to populate this adapter.
     *
     * @return the backing snapshot array
     */
    @NonNull
    public ObservableSnapshotArray<T> getSnapshots() {
        return mSnapshots;
    }

    /**
     * Gets the item at the specified position from the backing snapshot array.
     *
     * @see ObservableSnapshotArray#get(int)
     */
    @NonNull
    public T getItem(int position) {
        return list.get(position);
    }

    @Override
    public int getItemCount() {
        return list.size();
    }

    @Override
    public void onChildChanged(@NonNull ChangeEventType type,
                               @NonNull DocumentSnapshot snapshot,
                               int newIndex,
                               int oldIndex) {
        T model = mSnapshots.get(newIndex);
        onChildUpdate(model, type, snapshot, newIndex, oldIndex);
    }

    protected void onChildUpdate(T model, ChangeEventType type,
                                 DocumentSnapshot snapshot,
                                 int newIndex,
                                 int oldIndex) {
        switch (type) {
            case ADDED:
                addItem(snapshot.getId(), model);
                notifyItemInserted(newIndex);
                break;
            case CHANGED:
                addItem(snapshot.getId(), model, newIndex);
                notifyItemChanged(newIndex);
                break;
            case REMOVED:
                removeItem(newIndex);
                notifyItemRemoved(newIndex);
                break;
            case MOVED:
                moveItem(snapshot.getId(), model, newIndex, oldIndex);
                notifyItemMoved(oldIndex, newIndex);
                break;
            default:
                throw new IllegalStateException("Incomplete case statement");
        }
    }

    private void moveItem(String key, T t, int newIndex, int oldIndex) {
        list.remove(oldIndex);
        list.add(newIndex, t);
        if (isFiltarable) {
            backupList.remove(oldIndex);
            backupList.add(newIndex, t);
        }
    }

    private void removeItem(int newIndex) {
        list.remove(newIndex);
        if (isFiltarable)
            backupList.remove(newIndex);
    }

    private void addItem(String key, T t, int newIndex) {
        list.remove(newIndex);
        list.add(newIndex, t);
        if (isFiltarable) {
            backupList.remove(newIndex);
            backupList.add(newIndex, t);
        }
    }

    private void addItem(String id, T t) {
        list.add(t);
        if (isFiltarable)
            backupList.add(t);
    }

    @Override
    public void onDataChanged() {
    }

    @Override
    public void onError(@NonNull FirebaseFirestoreException e) {
        Log.w(TAG, "onError", e);
    }

    @Override
    public void onBindViewHolder(@NonNull VH holder, int position) {
        onBindViewHolder(holder, position, getItem(position));
    }

    /**
     * @param model the model object containing the data that should be used to populate the view.
     * @see #onBindViewHolder(RecyclerView.ViewHolder, int)
     */
    protected abstract void onBindViewHolder(@NonNull VH holder, int position, @NonNull T model);

    /**
     * filter condition for Filter
     *
     * @param model         model T
     * @param filterPattern filter pattern with Lower Case
     */
    protected boolean filterCondition(T model, String filterPattern) {
        return true;
    }

    @Override
    public Filter getFilter() {
        if (mCustomFilter == null) {
            mCustomFilter = new CustomFilter();
        }
        return mCustomFilter;
    }

    public class CustomFilter extends Filter {

        @Override
        protected FilterResults performFiltering(CharSequence constraint) {
            final FilterResults results = new FilterResults();
            if (constraint.length() == 0) {
                results.values = backupList;
                results.count = backupList.size();
            } else {
                List<T> filteredList = new ArrayList<>();
                final String filterPattern = constraint.toString().toLowerCase().trim();
                for (T t : backupList) {
                    if (filterCondition(t, filterPattern)) {
                        filteredList.add(t);
                    }
                }
                results.values = filteredList;
                results.count = filteredList.size();
            }

            return results;
        }

        @Override
        protected void publishResults(CharSequence constraint, FilterResults results) {
            list.clear();
            list.addAll((Collection<? extends T>) results.values);
            notifyDataSetChanged();
        }
    }
}

@mattherbert1
Copy link

Where do I have to call "peopleListAdapter.getFilter().filter(newText);" method?

@CHRISTMardochee
Copy link

@mattherbert1 in your Activity

@janglapuk
Copy link

ADDED on onChildUpdate always called while back from another activities, making duplicate whole existing data.

@yagneshshinde
Copy link

Making duplicate whole existing data.

@finexo
Copy link

finexo commented Oct 23, 2019

i did everything but my recycleradapter is not filtrable
no idea why if you can help me cause im working on this of four days without solution

@Kishanjvaghela
Copy link
Author

Can you please share your code? or error log if you have

@Kishanjvaghela
Copy link
Author

Kishanjvaghela commented Oct 25, 2019

Please read Docs above

Pass true as isFiltarable in adapter's constructer to support filter in adapter
FirebaseRecyclerAdapter(FirebaseRecyclerOptions<T> options, boolean isFiltarable)

@finexo
Copy link

finexo commented Oct 25, 2019

I think its done here
public ExamplePeopleAdapter(FirebaseRecyclerOptions options) {
super(options, true);
}

@finexo
Copy link

finexo commented Oct 25, 2019

sorry i didnt get it, i still didnt find the solution can you pleaz help me

@Kishanjvaghela
Copy link
Author

Let's talk here. https://gitter.im/FirebaseRecyclerAdapter/community
This comments section is not preferable for long chats.

@essowe-agnek
Copy link

Thank you very match. You saved me. FilterableFirestoreRecyclerAdapter works fine for me

@essowe-agnek
Copy link

Please sir, when I uses FilterableFirestoreRecyclerAdapter, there is a bug, when a doument is removed from collection.
It says document out of line. the error is pointed on onChilchange method. Thank

@gppam
Copy link

gppam commented Feb 14, 2020

FilterableFirestoreRecyclerAdapter

Anyway I also have the same error as @essowe-agnek on the onChildChanged method. I found that the get value in the method modified to this works.
T model = mSnapshots.get(type!=ChangeEventType.REMOVED ? newIndex : oldIndex);

Also I found that the recyclerview is prone to duplicate items. I was able to mitigate it using the band-aid code below.

  public void startListening() {
        if (list.size() > 0) list.clear(); // Add this
        if (!mSnapshots.isListening(this)) {
            mSnapshots.addChangeEventListener(this);
        }
    }

@vito8916
Copy link

I also have the same error as @essowe-agnek on the onChildChanged method when i remove an item from the recyclerView.

The error log shows me that:
Process: io.beanario.ocho_sidekick, PID: 22924
java.lang.ArrayIndexOutOfBoundsException: length=33; index=-1
at java.util.ArrayList.remove(ArrayList.java:506)

The error is pointed
private void removeItem(int newIndex) {
>>>> list.remove(newIndex); <<<<Error pointed
if (isFiltarable)
backupList.remove(newIndex);
}

Help me, please!

@gppam
Copy link

gppam commented Feb 20, 2020

@vito8916 have you tried using my solution above? and also can you change list.remove(newIndex) to list.remove(oldIndex)?

@Akash-Shukla123
Copy link

Thank you, you made my day sir

@Akash-Shukla123
Copy link

Items get doubled when I minimizing the app, so add list.clear()
like this:-
@OnLifecycleEvent(Lifecycle.Event.ON_STOP)
public void stopListening() {
itemsList.getSnapshots().removeChangeEventListener(this);
list.clear();
notifyDataSetChanged();
}

@Kishanjvaghela
Copy link
Author

@Akash-Shukla123 I am just curious about it. 'list.clear()' should be on ON_STOP or ON_DESTROY

@Kishanjvaghela
Copy link
Author

Items get doubled when I minimizing the app, so add list.clear()
like this:-
@OnLifecycleEvent(Lifecycle.Event.ON_STOP)
public void stopListening() {
itemsList.getSnapshots().removeChangeEventListener(this);
list.clear();
notifyDataSetChanged();
}

@janglapuk, @yagneshshinde This may work for you. Thanks a lot @Akash-Shukla123

@100race
Copy link

100race commented Apr 21, 2020

I added

public void startListening() {
        if (list.size() > 0) list.clear(); // Add this
        if (!mSnapshots.isListening(this)) {
            mSnapshots.addChangeEventListener(this);
        }
    }

and also

@OnLifecycleEvent(Lifecycle.Event.ON_STOP)
public void stopListening() {
itemsList.getSnapshots().removeChangeEventListener(this);
list.clear(); // add this
notifyDataSetChanged();
}

but it still duplicated everytime the editText was cleared.
and then I added
backupList.clear() with list.clear()

 @OnLifecycleEvent(Lifecycle.Event.ON_STOP)
    public void stopListening() {
        mSnapshots.removeChangeEventListener(this);
        list.clear();
        backupList.clear(); //add this
        notifyDataSetChanged();
    }
@OnLifecycleEvent(Lifecycle.Event.ON_START)
    public void startListening() {
        if (list.size() > 0) list.clear(); 
        if (backupList.size() > 0) backupList.clear(); //add this
        if (!mSnapshots.isListening(this)) {
            mSnapshots.addChangeEventListener(this);
        }
    }

and all duplicate problem are gone. this worked for me

@Kishanjvaghela
Copy link
Author

@100race THanks :-)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment