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();
}
}
}
@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