I did a similar thing using Firestore by creating a custom ArrayAdapter
that listens to a Firestore query and implements Filterable
.
public class ProductAdapter extends ArrayAdapter<Product>
implements Filterable, EventListener<QuerySnapshot> {
private final Object mLock = new Object();
private static final java.lang.String TAG = "Product Adapter";
private ArrayList<Product> mProducts = new ArrayList<>();
private ArrayList<Product> mOriginalValues;
private ArrayFilter mFilter;
private CharSequence mFilterPrefix;
private Query mQuery;
private ListenerRegistration mRegistration;
public ProductAdapter(Context context, int resource, Query query) {
super(context, resource);
mQuery = query;
}
@Override
public void onEvent(QuerySnapshot documentSnapshots, FirebaseFirestoreException e) {
if (e != null) {
Log.w(TAG, "onEvent:error", e);
return;
}
for (DocumentChange change : documentSnapshots.getDocumentChanges()) {
switch (change.getType()) {
case ADDED:
onDocumentAdded(change);
break;
case MODIFIED:
onDocumentModified(change);
break;
case REMOVED:
onDocumentRemoved(change);
break;
}
if (mFilterPrefix != null) {
getFilter().filter(mFilterPrefix);
}
}
}
public void startListening() {
if (mQuery != null && mRegistration == null) {
mRegistration = mQuery.addSnapshotListener(this);
}
}
public void stopListening() {
if (mRegistration != null) {
mRegistration.remove();
mRegistration = null;
}
synchronized (mLock) {
if (mOriginalValues != null) {
mOriginalValues.clear();
} else {
mProducts.clear();
}
}
notifyDataSetChanged();
}
public void setQuery(Query query) {
stopListening();
synchronized (mLock) {
if (mOriginalValues != null) {
mOriginalValues.clear();
}
mProducts.clear();
}
notifyDataSetChanged();
mQuery = query;
startListening();
}
protected void onDocumentAdded(DocumentChange change) {
synchronized (mLock) {
if (mOriginalValues != null) {
mOriginalValues.add(change.getNewIndex(), change.getDocument().toObject(Product.class));
} else {
mProducts.add(change.getNewIndex(), change.getDocument().toObject(Product.class));
}
}
notifyDataSetChanged();
}
protected void onDocumentModified(DocumentChange change) {
synchronized (mLock) {
if (change.getOldIndex() == change.getNewIndex()) {
if (mOriginalValues != null) {
mOriginalValues.set(change.getOldIndex(), change.getDocument().toObject(Product.class));
} else {
mProducts.set(change.getOldIndex(), change.getDocument().toObject(Product.class));
}
} else {
if (mOriginalValues != null) {
mOriginalValues.remove(change.getOldIndex());
mOriginalValues.add(change.getNewIndex(), change.getDocument().toObject(Product.class));
} else {
mProducts.remove(change.getOldIndex());
mProducts.add(change.getNewIndex(), change.getDocument().toObject(Product.class));
}
}
}
notifyDataSetChanged();
}
protected void onDocumentRemoved(DocumentChange change) {
synchronized (mLock) {
if (mOriginalValues != null) {
mOriginalValues.remove(change.getOldIndex());
} else {
mProducts.remove(change.getOldIndex());
}
}
notifyDataSetChanged();
}
@Override
public int getCount() {
return mProducts.size();
}
@Override
public Product getItem(int position){
return mProducts.get(position);
}
@Override
public long getItemId(int position){
return position;
}
@Override
public View getView(int position, final View convertView, final ViewGroup parent) {
TextView label = (TextView) super.getView(position, convertView, parent);
label.setText(mProducts.get(position).getName());
return label;
}
@Override
public View getDropDownView(int position, View convertView, ViewGroup parent) {
TextView label = (TextView) super.getDropDownView(position, convertView, parent);
label.setText(mProducts.get(position).getName());
return label;
}
@Override
public @NonNull Filter getFilter() {
if (mFilter == null) {
mFilter = new ArrayFilter();
}
return mFilter;
}
private class ArrayFilter extends Filter {
@Override
protected FilterResults performFiltering(CharSequence prefix) {
final FilterResults results = new FilterResults();
mFilterPrefix = prefix;
if (mOriginalValues == null) {
synchronized (mLock) {
mOriginalValues = new ArrayList<>(mProducts);
}
}
final ArrayList<Product> values;
synchronized (mLock) {
values = new ArrayList<>(mOriginalValues);
}
if (prefix == null || prefix.length() == 0) {
results.values = values;
results.count = values.size();
} else {
final String prefixString = prefix.toString().toLowerCase();
final int count = values.size();
final ArrayList<Product> newValues = new ArrayList<>();
for (int i = 0; i < count; i++) {
final Product product = values.get(i);
final String valueText = product.getName().toLowerCase();
if (valueText.startsWith(prefixString)) {
newValues.add(product);
} else {
final String[] words = valueText.split(" ");
if (words.length > 1) {
if (words[words.length - 1].startsWith(prefixString)) {
newValues.add(product);
} else {
for (int j = words.length - 2; j > 0; j--) {
words[j] = words[j] + " " + words[j + 1];
if (words[j].startsWith(prefixString)) {
newValues.add(product);
break;
}
}
}
}
}
}
results.values = newValues;
results.count = newValues.size();
}
return results;
}
@SuppressWarnings("unchecked")
@Override
protected void publishResults(CharSequence constraint, FilterResults results) {
mProducts = (ArrayList<Product>) results.values;
if (results.count > 0) {
notifyDataSetChanged();
} else {
notifyDataSetInvalidated();
}
}
}
}
I then set this adapter as the adapter of my AutoCompleteTextView
. In my case I was filtering a list of objects of class Product
by their names which I displayed in the AutoCompleteTextView
, but you could change this just to be strings.
To be clear this method listens to the query for the entire list of documents (products in my case) and then filters them locally, rather than making a query dependent on the text typed in the AutoCompleteTextView
. This way you don't need to make a query each time a new letter is typed and the list automatically updates if the Firestore database changes. If you wanted to instead make a new query each time the text changes you could try using something like this.