0

I have a RecyclerView which is populated by posts stored in a Firestore database. Each post is written as a document with a unique postID, storing the posted message, a timestamp and a like-counter.

    //mUploads is defined as private List<Upload> mUploads;
    //Upload object stores post message, timestamp and likes      
    mUploads = new ArrayList<>();

    mFireStoreOrdered = mFireStoreInst.collection("posts").orderBy("time");
    mFireStoreOrdered
            .get()
            .addOnCompleteListener(new OnCompleteListener<QuerySnapshot>() {
                @Override
                public void onComplete(@NonNull Task<QuerySnapshot> task) {
                    if (task.isSuccessful()) {
                        for (DocumentSnapshot doc : task.getResult()) {

                            //For each document get the ID
                            String postID = doc.getId();

                            // Upload object stores post message, timestamp and likes
                            Upload upload = doc.toObject(Upload.class).withId(postID);

                            mUploads.add(upload);
                        }

                        Collections.reverse(mUploads);

                        //Populate Recyclerview
                        mAdapter = new UploadAdapter(MainActivity.this, mUploads);

                        mContentView.setAdapter(mAdapter);


                    } else {
                        //...
                    }
                }
            });

When trying to implement the "like"-functionality for these posts I got to the limits of Firestore, which can only handle one document update per second.

Reading this article convinced me of using the Firebase Realtime Database to store the likes by using transaction operations instead of using distributed counters. I do not want to display the likes in real-time, I only want to use the RTDB to handle multiple likes/dislikes per second.

When additionally using the Firebase RTDB for likes, I would add data to a path /posts/postID/likes.

How can I get the post messages from Firestore and add the corresponding likes from the RTDB to mUploads before passing it to the adapter. Specificially, is it possible to ensure that I set the correct like value to its corresponding post, without querying for each postID.

P. Wulf
  • 18
  • 7

1 Answers1

0

This is a very common practice when it comes to Firestore, to store the number of likes in the Firebase Realtime database, otherwise you'll be charged for every read/write operation as explained in my answer from this post. So using Firebase Realtime database you can host the number of likes at no cost.

So, how can be done? First of all, you are guessing right. The number of likes should be added beneath the postId like this:

Firebase-root
   |
   --- likes
         |
         --- postIdOne: numberOfLikes //must be stored as an integer
         |
         --- postIdOTwo: numberOfLikes //must be stored as an integer
         |
         --- //and so on

To achive what you want, you need to follow the next steps.

  1. Every time you you add a new post, add the corresponding post id in Firebase Realtime database like above by setting the value of that particular post id to 0.
  2. Every time you get a new like increase the value of that postId by one. Every time a user retracts a like, decrease the value of that postId by one. To achieve this and also to have consistent data, I recommend you use Firebase Transactions.
  3. Then in your adapter class, where you are displaying data from Firestore, when you want to set the number of likes to a view, just attach a listener on that particular post id node and get the number of likes. Inside the onDataChange() set that number to a TextView like this:

    DatabaseReference rootRef = FirebaseDatabase.getInstance().getReference();
    DatabaseReference noOfLikesRef = rootRef.child("likes").child(postId);
    ValueEventListener valueEventListener = new ValueEventListener() {
        @Override
        public void onDataChange(DataSnapshot dataSnapshot) {
            String numberOfLikes = "(" + dataSnapshot.getValue() + ")";
            numberOfLikesTextView.setText(numberOfLikes);
        }
    
        @Override
        public void onCancelled(DatabaseError databaseError) {}
    };
    noOfLikesRef.addListenerForSingleValueEvent(valueEventListener);
    

That's it!

Alex Mamo
  • 130,605
  • 17
  • 163
  • 193
  • Thank you very much @Alex Mamo! I accepted this as the answer since it solves the stated problem. However I have a comment and a question on this statement: "using Firebase Realtime database you can host the number of likes at no cost." When using the onDataChange() in the Adapter (in onBindViewHolder to be more specific) I generate a lot of Download in Firebase. Since I only load tiny payloads this seems to be due to a huge overhead (by high frequent reads?), which seems to boost the download statistics of Firebase and the prize. Did I misunderstand your answer or is there a workaround :)? – P. Wulf Apr 05 '18 at 12:00
  • When I said at no cost, it means that you are not charged for every like that you get. In Cloud Firestore, for 100 likes you are charged with 100 write operations. In Firebase realtime database, there is no such plan. As you can see, I have used `addListenerForSingleValueEvent` which will download the data only once. – Alex Mamo Apr 05 '18 at 12:08
  • I understand. Although it is a listener for a single value event, it is used very frequently in my case. Always when a View is bound (which happens quite often for me) a request to the database is done. When scrolling through my cards I get a highly frequent read request. I do not know much about overhead and whether my assumption is true, all I can see is that I get ~2 MB of downloads with only 1 user for each hour of normal operation, when storing "only" 10 posts and 10 like values respectively. – P. Wulf Apr 05 '18 at 12:35
  • Yes, a request to the database is done. And this is because you are reading the number of likes for each view But remember, you don't need to this in your `onBindViewHolder` method, you need to do it in your holder class, where you are are setting the data to the coresponding TextView, otherwise when scrolling, the `onBindViewHolder` is triggered each time. – Alex Mamo Apr 05 '18 at 12:45
  • Word! I should keep it out of my onBindViewHolder. Thanks, I will do so and give an update on the usage numbers for clarification! – P. Wulf Apr 05 '18 at 12:50
  • That's perfect. Thanks! – Alex Mamo Apr 05 '18 at 12:51
  • Inside the `onBindViewHolder` method, when you are calling the method that actually is seeting the data in class, pass the object of the class or only the id as an argument, so it can be used in holder class. Having that id, you can use it in a reference or in any place is needed. – Alex Mamo Apr 05 '18 at 13:48
  • Sorry, this seems to get a longer discussion than intended. First of all, thanks for your help! When passing the ID as a String and starting the valueEventListener in the ViewHolder class I get a NullPointerException on the String ID. I guess this is a problem due to synchronization? It calls the valueEventListener on the postID before it is assigned? – P. Wulf Apr 05 '18 at 14:04
  • You need to get that id that is passed as an argument from the `onBindViewHolder `. It should be something like this: `viewHolder.setText(upload.getId())`. – Alex Mamo Apr 05 '18 at 14:17
  • Yup, in my `ViewHolder` class I have `private String postID` which I set in the `onBindViewHolder` by `viewHolder.postID = getId(position)`. And just underneath my `onClickListeners` in the ViewHolder I have added the `valueEventListener`. But I get the NullPointerException on the String postID. – P. Wulf Apr 05 '18 at 14:28
  • There has nothing to do with the onClickListeners. You just need to pass that id as an argument to a method beneath the holderclass, so you can simply use it in holder class. – Alex Mamo Apr 05 '18 at 14:29
  • I know, the information about putting it underneath the onClickListeners was just not necessary. I did the setup as described. I will open a new Thread on Stackoverflow since this seems to be a different question and leave a link here :). – P. Wulf Apr 05 '18 at 14:34
  • Yes, that's better. Cheers! – Alex Mamo Apr 05 '18 at 14:36
  • https://stackoverflow.com/questions/49675314/passing-variableposition-from-onbindviewholder-to-viewholder-class – P. Wulf Apr 05 '18 at 14:49