Using GeoFire to Populate Firebase Recycler View in android
Asked Answered
G

2

8

I want to populate my recycler view so that, I can see who are the people/places nearby. I am using GeoFire to Query my database, which looks something like this.

 GeoQuery geoQuery = geoFire.queryAtLocation(new GeoLocation(latLngCenter.latitude, latLngCenter.longitude), 0.1);
        geoQuery.addGeoQueryEventListener(new GeoQueryEventListener() {
            @Override
            public void onKeyEntered(String key, GeoLocation location) {
                System.out.println(String.format("Key %s entered the search area at [%f,%f]", key, location.latitude, location.longitude));
                Log.e("TAG", key + location.latitude + location.longitude);

            }

            @Override
            public void onKeyExited(String key) {
                System.out.println(String.format("Key %s is no longer in the search area", key));
            }

            @Override
            public void onKeyMoved(String key, GeoLocation location) {
                System.out.println(String.format("Key %s moved within the search area to [%f,%f]", key, location.latitude, location.longitude));
                Log.e("TAG", key + location.latitude + location.longitude);
            }

            @Override
            public void onGeoQueryReady() {
                System.out.println("All initial data has been loaded and events have been fired!");
            }

            @Override
            public void onGeoQueryError(DatabaseError error) {
                System.err.println("There was an error with this query: " + error);
            }
        });

and I am using this Firebase RecyclerView

 RecyclerView recycler = (RecyclerView) findViewById(R.id.RecyclerView);
    recycler.setHasFixedSize(true);
    recycler.setLayoutManager(new LinearLayoutManager(this));

    FirebaseRecyclerAdapter<Chat, ChatHolder> mAdapter = new FirebaseRecyclerAdapter<Chat, ChatHolder>(Chat.class, R.layout.recyclerview, ChatHolder.class, mUsers) {


        @Override
        public void populateViewHolder(final ChatHolder chatMessageViewHolder, final Chat chatMessage, int position) {


                    chatMessageViewHolder.setName(chatMessage.getName());
                    chatMessageViewHolder.setText(chatMessage.getText());
                    chatMessageViewHolder.setTimestamp(chatMessage.getTimestamp());



        }
    };
    recycler.setAdapter(mAdapter);

with these Chat Holder class and chat object class

public static class ChatHolder extends RecyclerView.ViewHolder {
    View mView;

    public ChatHolder(View itemView) {
        super(itemView);
        mView = itemView;
    }



    public void setName(String name) {
        TextView field = (TextView) mView.findViewById(R.id.textViewName);
        field.setText(name);
    }

    public void setText(String text) {
        TextView field = (TextView) mView.findViewById(R.id.textViewMessage);
        field.setText(text);
    }

    public void setTimestamp(String text) {
        TextView field = (TextView) mView.findViewById(R.id.textViewTime);
        field.setText(text);
    }
}

public static class Chat {

    String name;
    String text;
    String uid;
    String timestamp;

    public Chat() {
    }

    public Chat(String name, String uid, String message, String timestamp) {
        this.name = name;
        this.text = message;
        this.uid = uid;
        this.timestamp = timestamp;
    }

    public String getName() {
        return name;
    }

    public String getUid() {
        return uid;
    }

    public String getText() {
        return text;
    }

    public String getTimestamp() {
        return timestamp;
    }
}

Currently this, adapter which is provided in FirebaseUI library, populates recyclerview so that, only one reference is used and all child events are shown in the view, Now, I want to populate my recyclerView so that when ever a key enters it populates my recyclerview based on my key = to my reference, this how my firebase database looksmy firebase database

Glisten answered 7/8, 2016 at 10:27 Comment(7)
@Frank van Puffen, dude, I can't get this thing working, tried all sort of trial and error, there are many people stuck here, would really appreciate if you could answer the questionGlisten
@Frank van Puffen Hey i am also stuck with the same problem. Please help.Brachiate
Hey did you find any solution?Brachiate
Nope not really! I have been stuck here forever! can't get them working! i made my own adapter, but still can't get, all of them to update in realtime! lemme know if you find anythingGlisten
okay sure will post whenever i will find out. ThanksBrachiate
@Glisten any help yet?Whirlybird
Yeah! I had to make a custom adapter! I saw some code from firebase's friendlypix example (clone of instagram). and it seems to work for me! but highly specific solution!Glisten
S
1

It'll be simpler to push all data (which is also uid) you have retrieved from geofire to a new node to store geofire results in firebase, something like

my-node //your new firebase node to store all uids retrieved from geofire
- {chatUid}: true //true is just a dummy data, you can use anything else except null and empty string.
- {chatUid}: true
- ...

and set FirebaseRecyclerAdapter ref to that node.

DatabaseReference ref = FirebaseDatabase.getInstance().getReference("my-node");
FirebaseRecyclerAdapter fra = new FirebaseRecyclerAdapter<Boolean, MyVH>(Boolean.class, R.layout.my_layout, MyVH.class, ref) { ... }

and then, in populateViewHolder() method in your implementation of FirebaseRecyclerAdapter, you can use the String key to fetch data from main node that contains the data.

public void populateViewHolder(MyVH viewHolder, Boolean model, int position){
    // Get references of child views
    final TextView nameTextView = viewHolder.getItemView().findViewById(R.id.my_name_text_view_in_vh_layout);
    final TextView addressTextView = viewHolder.getItemView().findViewById(R.id.my_address_text_view_in_vh_layout);

    // Key in this position.
    String key = getRef(position).key;

    // Query the full data of the current key located in the `main-data-node`
    FirebaseDatabase.getInstance().getReference("main-data-node").child(key).addValueEventListener(new ValueEventListener(){
        ... //Truncated onCancelled

        @Override
        public void onDataChange(snap: DataSnapshot){
            MyDataModel model = snap.getValue(MyDataModel.class);

            nameTextView = model.getName();
            addressTextView = model.getAddress();
        }
    }
}

// The data model class
public class MyDataModel { 
    private String name; 
    private String address; 
    ... // Truncated getter, setter, constructor
}

For any changes in geofire result, just push those results to my-node, and it will automatically informs your FirebaseRecyclerAdapter.

P.S. I'm too lazy to match my solution to your data model (sorry), so I made a simplified sample class, so if anyone stumbled upon this, they can understand it easier.

P.S.S. It's been a while since I code in java, I mainly use kotlin (and you soon should too lol), so, if there're some syntax mistakes out there, feel free to edit.

Saddlebacked answered 8/7, 2017 at 11:48 Comment(6)
could you flesh out how to actually implement the "//TODO: Use given key to fetch the data from your main node." part? That's the part I'm not clear about.Perineurium
It's just like : FirebaseDatabase.getInstance().getReference("nodeThatContainsFullData").child(key).addValueEventListener(...). ... is the basic operation on how you initialize a new ValueEventListener object. Read the official guide in the firebase website, under realtime database section. Good luck.Saddlebacked
Thanks for the rapid response! So, I've already played with that a little bit. Since I have to override onDataChange and type cast my snapshot into my model from within there, I'm not sure how to then bind that object, given that viewHolder is not available from inside onDataChange. Does that make sense?Perineurium
@Perineurium well it doesn't matter actually. You can call as many as addValueEventListener you want inside the onDataChange, and attach it to referenced child views in the viewholders. Actually I'm on mobile now, I'll update my post for full solution once I get back.Saddlebacked
@Perineurium well it doesn't matter actually. You can call as many as "inner" addValueEventListener you want inside the onDataChange, and attach it to referenced child views in the viewholders. Actually I'm on mobile now, I'll update my post for full solution once I get back.Saddlebacked
Much appreciated, @Moses Aprico!Perineurium
M
0

This is the Emanuelet's custom FirebaseListAdapter from which I have created by FirebaseRecyclerAdapter.

    public abstract class FirebaseRecyclerAdapter<T, VH extends RecyclerView.ViewHolder> extends RecyclerView.Adapter<VH > implements Filterable {

        private static final String LOG_TAG = "FirebaseListAdapter";
        private Query mRef;
        private Class<T> mModelClass;
        private int mLayout;
        private LayoutInflater mInflater;
        protected Class<VH> mViewHolderClass;
        private List<T> mModels;
        private List<T> mFilteredModels;
        private List<String> mKeys = new ArrayList<>();
        private Map<String, T> mModelKeys;
        private Map<String, T> mFilteredKeys;
        private ChildEventListener mListener;
        private FirebaseRecyclerAdapter.ValueFilter valueFilter;


        /**
         * @param mRef        The Firebase location to watch for data changes. Can also be a slice of a location, using some
         *                    combination of <code>limit()</code>, <code>startAt()</code>, and <code>endAt()</code>,
         * @param mModelClass Firebase will marshall the data at a location into an instance of a class that you provide
         * @param mLayout     This is the mLayout used to represent a single list item. You will be responsible for populating an
         *                    instance of the corresponding view with the data from an instance of mModelClass.
         * @param activity    The activity containing the ListView
         */
        public FirebaseRecyclerAdapter(Query mRef, Class<T> mModelClass, int mLayout, Activity activity, Class<VH> viewHolderClass) {
            this.mRef = mRef;
            this.mModelClass = mModelClass;
            this.mLayout = mLayout;
            this.mViewHolderClass = viewHolderClass;
            mInflater = activity.getLayoutInflater();
            mModels = new ArrayList<>();
            mModelKeys = new HashMap<>();
            // Look for all child events. We will then map them to our own internal ArrayList, which backs ListView
            mListener = this.mRef.addChildEventListener(new ChildEventListener() {
                @Override
                public void onChildAdded(DataSnapshot dataSnapshot, String previousChildName) {
                    T model = dataSnapshot.getValue(FirebaseRecyclerAdapter.this.mModelClass);
                    mModelKeys.put(dataSnapshot.getKey(), model);

                    // Insert into the correct location, based on previousChildName
                    if (previousChildName == null) {
                        mModels.add(0, model);
                    } else {
                        T previousModel = mModelKeys.get(previousChildName);
                        int previousIndex = mModels.indexOf(previousModel);
                        int nextIndex = previousIndex + 1;
                        if (nextIndex == mModels.size()) {
                            mModels.add(model);
                            mKeys.add(dataSnapshot.getKey());
                        } else {
                            mModels.add(nextIndex, model);
                            mKeys.add(dataSnapshot.getKey());
                        }
                    }

                    notifyDataSetChanged();
                }

                @Override
                public void onChildChanged(DataSnapshot dataSnapshot, String s) {
                    Log.d(LOG_TAG, "onChildChanged");
                    // One of the mModels changed. Replace it in our list and name mapping
                    String modelName = dataSnapshot.getKey();
                    T oldModel = mModelKeys.get(modelName);
                    T newModel = dataSnapshot.getValue(FirebaseRecyclerAdapter.this.mModelClass);
                    int index = mModels.indexOf(oldModel);

                    mModels.set(index, newModel);
                    mModelKeys.put(modelName, newModel);

                    notifyDataSetChanged();
                }

                @Override
                public void onChildRemoved(DataSnapshot dataSnapshot) {
                    Log.d(LOG_TAG, "onChildRemoved");
                    // A model was removed from the list. Remove it from our list and the name mapping
                    String modelName = dataSnapshot.getKey();
                    T oldModel = mModelKeys.get(modelName);
                    mModels.remove(oldModel);
                    mKeys.remove(dataSnapshot.getKey());
                    mModelKeys.remove(modelName);
                    notifyDataSetChanged();
                }

                @Override
                public void onChildMoved(DataSnapshot dataSnapshot, String previousChildName) {
                    Log.d(LOG_TAG, "onChildMoved");
                    // A model changed position in the list. Update our list accordingly
                    String modelName = dataSnapshot.getKey();
                    T oldModel = mModelKeys.get(modelName);
                    T newModel = dataSnapshot.getValue(FirebaseRecyclerAdapter.this.mModelClass);
                    int index = mModels.indexOf(oldModel);
                    mModels.remove(index);
                    if (previousChildName == null) {
                        mModels.add(0, newModel);
                        mKeys.add(dataSnapshot.getKey());
                    } else {
                        T previousModel = mModelKeys.get(previousChildName);
                        int previousIndex = mModels.indexOf(previousModel);
                        int nextIndex = previousIndex + 1;
                        if (nextIndex == mModels.size()) {
                            mModels.add(newModel);
                            mKeys.add(dataSnapshot.getKey());
                        } else {
                            mModels.add(nextIndex, newModel);
                            mKeys.add(dataSnapshot.getKey());
                        }
                    }
                    notifyDataSetChanged();
                }

                @Override
                public void onCancelled(DatabaseError error) {
                    Log.e("FirebaseListAdapter", "Listen was cancelled, no more updates will occur");
                }
            });
        }

        public void cleanup() {
            // We're being destroyed, let go of our mListener and forget about all of the mModels
            mRef.removeEventListener(mListener);
            mModels.clear();
            mModelKeys.clear();
            mKeys.clear();
        }

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

        public T getItem(int position) {
            return mModels.get(position);
        }

        @Override
        public void onBindViewHolder(VH holder, int position) {
            T model = getItem(position);
            populateViewHolder(holder, model, position, mKeys);
        }

        @Override
        public long getItemId(int i) {
            return i;
        }

        @Override
        public int getItemViewType(int position) {
            return mLayout;
        }

        public void remove(String key) {
            T oldModel = mModelKeys.get(key);
            mModels.remove(oldModel);
            mKeys.remove(key);
            mModelKeys.remove(key);
            notifyDataSetChanged();
        }

        @Override
        public VH onCreateViewHolder(ViewGroup parent, int viewType) {
            View view = LayoutInflater.from(parent.getContext()).inflate(viewType, parent, false);
            try {
                Constructor<VH> constructor = mViewHolderClass.getConstructor(View.class);
                return constructor.newInstance(view);
            } catch (NoSuchMethodException e) {
                throw new RuntimeException(e);
            } catch (InvocationTargetException e) {
                throw new RuntimeException(e);
            } catch (InstantiationException e) {
                throw new RuntimeException(e);
            } catch (IllegalAccessException e) {
                throw new RuntimeException(e);
            }
        }

        /**
         * Each time the data at the given Firebase location changes, this method will be called for each item that needs
         * to be displayed. The arguments correspond to the mLayout and mModelClass given to the constructor of this class.
         * <p/>
         * Your implementation should populate the view using the data contained in the model.
         *
         * @param viewHolder     The view to populate
         * @param model The object containing the data used to populate the view
         */
        protected abstract void populateViewHolder(VH viewHolder, T model, int position, List<String> mKeys);

        public void addSingle(DataSnapshot snapshot) {
            T model = snapshot.getValue(FirebaseRecyclerAdapter.this.mModelClass);
            mModelKeys.put(snapshot.getKey(), model);
            mModels.add(model);
            mKeys.add(snapshot.getKey());
            notifyDataSetChanged();
        }

        public void update(DataSnapshot snapshot, String key) {
            T oldModel = mModelKeys.get(key);
            T newModel = snapshot.getValue(FirebaseRecyclerAdapter.this.mModelClass);
            int index = mModels.indexOf(oldModel);

            if (index >= 0) {
                mModels.set(index, newModel);
                mModelKeys.put(key, newModel);
                notifyDataSetChanged();
            }
        }

        public boolean exists(String key) {
            return mModelKeys.containsKey(key);
        }

        @Override
        public Filter getFilter() {
            if (valueFilter == null) {
                valueFilter = new FirebaseRecyclerAdapter.ValueFilter();
            }
            return valueFilter;
        }

        protected abstract List<T> filters(List<T> models, CharSequence constraint);

        private class ValueFilter extends Filter {

            //Invoked in a worker thread to filter the data according to the constraint.
            @Override
            protected FilterResults performFiltering(CharSequence constraint) {
                FilterResults results = new Filter.FilterResults();
                if (mFilteredModels == null) {
                    mFilteredModels = new ArrayList<>(mModels); // saves the original data in mOriginalValues
                    mFilteredKeys = new HashMap<>(mModelKeys); // saves the original data in mOriginalValues
                }
                if (constraint != null && constraint.length() > 0) {
                    List<T> filtered = filters(mFilteredModels, constraint);

                    results.count = filtered.size();
                    results.values = filtered;
                    mModelKeys = filterKeys(mModels);
                } else {
                    results.count = mFilteredModels.size();
                    results.values = mFilteredModels;
                    mModelKeys = mFilteredKeys;
                }
                return results;
            }


            //Invoked in the UI thread to publish the filtering results in the user interface.
            @SuppressWarnings("unchecked")
            @Override
            protected void publishResults(CharSequence constraint,
                                          FilterResults results) {
                Log.d(LOG_TAG, "filter for " + constraint + ", results nr: " + results.count);
                mModels = (List<T>) results.values;

                notifyDataSetChanged();
            }
        }

        protected abstract Map<String, T> filterKeys(List<T> mModels);

}

Extend the PostsQueryAdapter with the FirebaseRecyclerAdapter

public class FirebasePostsQueryAdapter extends FirebaseRecyclerAdapter<Feeds, PostViewHolder> {
    Activity mActivity;


    /**
     * @param mRef            The Firebase location to watch for data changes. Can also be a slice of a location, using some
     *                        combination of <code>limit()</code>, <code>startAt()</code>, and <code>endAt()</code>,
     * @param mLayout         This is the mLayout used to represent a single list item. You will be responsible for populating an
     *                        instance of the corresponding view with the data from an instance of mModelClass.
     * @param activity        The activity containing the ListView
     * @param viewHolderClass This is the PostsViewHolder Class which will be used to populate data.
     */
    Query query;
    public FirebasePostsQueryAdapter(Query mRef, int mLayout, Activity activity, Class<PostViewHolder> viewHolderClass) {
        super(mRef, Feeds.class, mLayout, activity, viewHolderClass);
        this.query = mRef;
        this.mActivity = activity;
    }

    @Override
    protected void populateViewHolder(final PostViewHolder viewHolder, final Feeds model, final int position, final List<String> mKeys) {
        viewHolder.setPhoto(model.getThumb_url());
        viewHolder.setTimestamp(DateUtils.getRelativeTimeSpanString(
                (long) model.getTimestamp()).toString());
        viewHolder.setAuthor(model.getUser().getFull_name(), model.getUser().getUid());
        viewHolder.setIcon(model.getUser().getProfile_picture(), model.getUser().getUid());
        viewHolder.setText(model.getText());
        viewHolder.mPhotoView.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                Intent intent = new Intent(mActivity, SingleVideoView.class);
                intent.putExtra(Constants.INTENT_VIDEO,model.getVideo_url());
                intent.putExtra(Constants.KEY, mKeys.get(position));
                mActivity.startActivity(intent);
            }
        });
        ValueEventListener likeListener = new ValueEventListener() {
            @Override
            public void onDataChange(DataSnapshot dataSnapshot) {
                    viewHolder.setNumLikes(dataSnapshot.getChildrenCount());
                if (dataSnapshot.hasChild(FirebaseUtil.getCurrentUserId())) {
                    viewHolder.setLikeStatus(PostViewHolder.LikeStatus.LIKED, mActivity);
                } else {
                    viewHolder.setLikeStatus(PostViewHolder.LikeStatus.NOT_LIKED, mActivity);
                }
            }

            @Override
            public void onCancelled(DatabaseError databaseError) {

            }
        };

        FirebaseUtil.getLikesRef().child(mKeys.get(position)).addValueEventListener(likeListener);
        viewHolder.mLikeListener = likeListener;
    }

    @Override
    protected List<Feeds> filters(List<Feeds> models, CharSequence constraint) {
        return null;
    }

    @Override
    protected Map<String, Feeds> filterKeys(List<Feeds> mModels) {
        return null;
    }
}

Where you populate the data into FirebasePostsQueryAdapter

   @Override
        public void onCreate() {
            super.onCreate();
            mFirebaseRef = FirebaseDatabase.getInstance().getReference(POSTS_STRING);
            mFirebaseRef.keepSynced(true);
            this.geoFire = new GeoFire(FirebaseDatabase.getInstance().getReference(GEO_POINTS));

mItemListAdapter = new FirebasePostQueryAdapter(mFirebaseRef.equalTo(GEOFIRE_CHILD), getActivity(), R.layout.list_item_items);


        }


    @Override
        public void onConnected(Bundle bundle) {
            startLocationUpdates();
            center = new GeoLocation(MainActivity.mLastLocation.getLatitude(), MainActivity.mLastLocation.getLongitude());
            if (!mActiveGeoQuery) {
                center = new GeoLocation(mCurrentLocation.getLatitude(), mCurrentLocation.getLongitude());
                startGeoQuery();
                mAdapter.notifyDataSetChanged();
            }

            center = new GeoLocation(MainActivity.mLastLocation.getLatitude(), MainActivity.mLastLocation.getLongitude());
            if (center.latitude != 0 && center.longitude != 0 && !mActiveGeoQuery) {
                startGeoQuery();
            } else if (mActiveGeoQuery) {
                Log.d(TAG, "geoquery already active");
            } else {
                Log.d(TAG, "center not setted");
                //I first try to set the center at the Last Known Location if retrieved
                if (Double.isNaN(mCurrentLocation.getLatitude()) && mCurrentLocation.getLatitude() != 0) {
                    center = new GeoLocation(mCurrentLocation.getLatitude(), mCurrentLocation.getLongitude());
                    startGeoQuery();

                }
            }
            fragment.checkForItems();
        }

     private void startGeoQuery() {
            query = geoFire.queryAtLocation(center, 2);
            Log.d(TAG, "center: " + center.toString() + ", radius: " + 2);
            query.addGeoQueryEventListener(new GeoQueryEventListener() {
                @Override
                public void onKeyEntered(String key, GeoLocation location) {
                    Log.d(TAG, "Key " + key + " entered the search area at [" + location.latitude + "," + location.longitude + "]");
                    DatabaseReference tempRef = mFirebaseRef.child(key);
                    tempRef.addValueEventListener(new ValueEventListener() {
                        @Override
                        public void onDataChange(DataSnapshot snapshot) {
                            // I  add the deal only if it doesn't exist already in the adapter
                            String key = snapshot.getKey();
                            if (!mAdapter.exists(key)) {
                                Log.d(TAG, "item added " + key);
                                mAdapter.addSingle(snapshot);
                                mAdapter.notifyDataSetChanged();
                            } else {
                                //...otherwise I will update the record
                                Log.d(TAG, "item updated: " + key);
                                mAdapter.update(snapshot, key);
                                mAdapter.notifyDataSetChanged();
                            }
                        }

                        @Override
                        public void onCancelled(DatabaseError databaseError) {

                        }
                    });
                }

                @Override
                public void onKeyExited(String key) {
                    Log.d(TAG, "deal " + key + " is no longer in the search area");
                    mAdapter.remove(key);

                    fragment.isListEmpty();

                }

                @Override
                public void onKeyMoved(String key, GeoLocation location) {
                    Log.d(TAG, String.format("Key " + key + " moved within the search area to [%f,%f]", location.latitude, location.longitude));
                    DatabaseReference tempRef = mFirebaseRef.child(key);
                    tempRef.addValueEventListener(new ValueEventListener() {
                        @Override
                        public void onDataChange(DataSnapshot snapshot) {
                            // I  add the deal only if it doesn't exist already in the adapter
                            String key = snapshot.getKey();
                            if (!mAdapter.exists(key)) {
                                Log.d(TAG, "item added " + key);
                                mAdapter.addSingle(snapshot);
                                mAdapter.notifyDataSetChanged();
                            } else {
                                //...otherwise I will update the record
                                Log.d(TAG, "item updated: " + key);
                                mAdapter.update(snapshot, key);
                                mAdapter.notifyDataSetChanged();
                            }
                        }

                        @Override
                        public void onCancelled(DatabaseError databaseError) {

                        }
                    });
                }

                @Override
                public void onGeoQueryReady() {
                    Log.d(TAG, "All initial data has been loaded and events have been fired!");
                    fragment.isListEmpty();
                    mActiveGeoQuery = true;
                }

                @Override
                public void onGeoQueryError(DatabaseError error) {
                    Log.e(TAG, "There was an error with this query: " + error);
                    mActiveGeoQuery = false;
                }
            });
            fragment.bindRecyclerView();
        }
Mass answered 30/1, 2017 at 6:37 Comment(4)
It's better just to link GitHub repo. You have fragment variable and you call its methods, so you should put it here as well which makes answer unreadable and hard to applyMechelle
@IgorKlimov what is so hard to understand. The GitHub gist has the almost the same code but it is for listView. I can give you the water but can't make you drink it.Mass
@Mass Did you have any issues with the mKeys not having all items from the Ref? For some reason it only has 2 items while I clearly have 3 in the Firebase Database Node. CheersNariko
@AndyStrife I have added the adapter insatiate code to onCreate do check again.Mass

© 2022 - 2024 — McMap. All rights reserved.