1

I am creating a journal app. I am trying to fetch the current users journal entries and then convert that into a journal object which will be put into a journal list. Eventually, this journal list will be sent to the RecyclerView Adapter.

I have this piece of code in onCreate():

myCollection.whereEqualTo("userId", userId).addSnapshotListener(new EventListener<QuerySnapshot>() {
            @Override
            public void onEvent(@Nullable @org.jetbrains.annotations.Nullable QuerySnapshot value, @Nullable @org.jetbrains.annotations.Nullable FirebaseFirestoreException error) {
                for(QueryDocumentSnapshot snapshot : value){
                    Journal myJournal = snapshot.toObject(Journal.class);
                    journals.add(myJournal);

                     
                    RecyclerViewAdapter myAdapter = new RecyclerViewAdapter(journals, JournalList.this);
                    recyclerView.setAdapter(myAdapter);
                    recyclerView.setLayoutManager(new LinearLayoutManager(JournalList.this));
                    myAdapter.notifyDataSetChanged();

                }
            }
        });

If I move this part:

RecyclerViewAdapter myAdapter = new RecyclerViewAdapter(journals, JournalList.this);
                        recyclerView.setAdapter(myAdapter);
                        recyclerView.setLayoutManager(new LinearLayoutManager(JournalList.this));
                        myAdapter.notifyDataSetChanged();

out of the onEvent block(but still in onCreate() ), the journal is still sent to firebase, but it seems like the RecyclerViewAdapter isn't invoked until I add the second post.

My guess is that either Android Studio skips over the onEvent() block and continues on(possibly puts it in a queue considering it knows it will take time to execute), or it runs on a background thread in which the adapter part finishes first. Either way, an empty arrayList of journals is sent to Firestore.

However, I'm unsure which one of these scenarios is actually occurring. If someone could confirm, I would appreciate it. Thanks.

Update: Code that doesn't work:

myCollection.whereEqualTo("userId", userId).addSnapshotListener(new EventListener<QuerySnapshot>() {
            @Override
            public void onEvent(@Nullable @org.jetbrains.annotations.Nullable QuerySnapshot value, @Nullable @org.jetbrains.annotations.Nullable FirebaseFirestoreException error) {
                for(QueryDocumentSnapshot snapshot : value){
                    Journal myJournal = snapshot.toObject(Journal.class);
                    journals.add(myJournal);



                }
            }
        });

        
        RecyclerViewAdapter myAdapter = new RecyclerViewAdapter(journals, JournalList.this);
        recyclerView.setAdapter(myAdapter);
        recyclerView.setLayoutManager(new LinearLayoutManager(JournalList.this));
        myAdapter.notifyDataSetChanged();

Full code of JournalList.java(if needed):

import android.content.Intent;
import android.os.Bundle;
import android.view.Menu;
import android.view.MenuItem;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AppCompatActivity;
import androidx.appcompat.widget.Toolbar;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;

import com.google.firebase.auth.FirebaseAuth;
import com.google.firebase.auth.FirebaseUser;
import com.google.firebase.firestore.CollectionReference;
import com.google.firebase.firestore.EventListener;
import com.google.firebase.firestore.FirebaseFirestore;
import com.google.firebase.firestore.FirebaseFirestoreException;
import com.google.firebase.firestore.QueryDocumentSnapshot;
import com.google.firebase.firestore.QuerySnapshot;

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

public class JournalList extends AppCompatActivity {

    private RecyclerView recyclerView;
    private FirebaseFirestore db = FirebaseFirestore.getInstance();
    private CollectionReference myCollection = db.collection("Journal");
    private FirebaseAuth firebaseAuth;
    private FirebaseUser currentUser;
    private List<Journal> journals;
    private Toolbar myToolbar;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_journal_list);

        recyclerView = findViewById(R.id.recyclerView);

        firebaseAuth = FirebaseAuth.getInstance();
        currentUser = firebaseAuth.getCurrentUser();
        String userId = currentUser.getUid();
        myToolbar = findViewById(R.id.my_toolbar);
        journals = new ArrayList<>();

        setSupportActionBar(myToolbar);




        myCollection.whereEqualTo("userId", userId).addSnapshotListener(new EventListener<QuerySnapshot>() {
            @Override
            public void onEvent(@Nullable @org.jetbrains.annotations.Nullable QuerySnapshot value, @Nullable @org.jetbrains.annotations.Nullable FirebaseFirestoreException error) {
                for(QueryDocumentSnapshot snapshot : value){
                    Journal myJournal = snapshot.toObject(Journal.class);
                    journals.add(myJournal);



                }
            }
        });

        //my guess:
        RecyclerViewAdapter myAdapter = new RecyclerViewAdapter(journals, JournalList.this);
        recyclerView.setAdapter(myAdapter);
        recyclerView.setLayoutManager(new LinearLayoutManager(JournalList.this));
        myAdapter.notifyDataSetChanged();










    }

    @Override
    public boolean onCreateOptionsMenu(Menu menu) {

        getMenuInflater().inflate(R.menu.menu, menu);

        return super.onCreateOptionsMenu(menu);



    }

    @Override
    public boolean onOptionsItemSelected(@NonNull MenuItem item) {
        switch (item.getItemId()){
            case R.id.addNote:

                startActivity(new Intent(JournalList.this, PostJournalActivity.class));

                break;
            case R.id.sign_out:

                firebaseAuth.signOut();
                startActivity(new Intent(JournalList.this, MainActivity.class));

                break;
        }

        return super.onOptionsItemSelected(item);


    }

    @Override
    protected void onDestroy() {

        super.onDestroy();
    }
}
CodingChap
  • 1,088
  • 2
  • 10
  • 23

2 Answers2

1

The network request etc runs on a background thread, but the callback doesn't.

However I think your misunderstanding here comes from how callbacks work. When you pass in an EventListener here, you're passing in a class (in this case, an anonymous class) which overrides the onEvent method. There's nothing to "skip over" as you mentioned.

Let's consider an example. Say I defined an interface Callback:

interface Callback {

    void myMethod();
}

Then I can write a redundant method:

void doSomethingWith(Callback myCallback) {

    myCallback.myMethod();
}

Now here, when I call

doSomethingWith(new Callback() {
    @Override
    public void myMethod() {
        
        // ...
    }
});

doSomethingElse();

the implementation of myMethod runs before doSomethingElse.

However, it doesn't have to be this way. Say I instead defined doSomethingWith like this:

void doSomethingWith(Callback myCallback) {

    // Do nothing
}

Then the code in your implementation of myMethod will never get called. Just because you passed in a class that implemented a method, doesn't guarantee when it will get called or whether it will get called at all.

To bring this back to Firebase a bit, we can consider an example like this:

void doSomethingWith(Callback myCallback) {

    // Switch to a background thread,
    // wait for a network request then call myCallback.myMethod()
}

So here your callback will get called, but it will be at a later time, when a network request has completed, exactly the same scenario as with your Firebase listener.


Now let's actually address solving your problem. It would make the most sense (to me) to declare the adapter first, then when onEvent is called, update the data:

RecyclerViewAdapter myAdapter = new RecyclerViewAdapter(journals, JournalList.this);
recyclerView.setAdapter(myAdapter);
recyclerView.setLayoutManager(new LinearLayoutManager(JournalList.this));

myCollection...addSnapshotListener(new EventListener<QuerySnapshot>() {
    @Override
    public void onEvent(...) {

        myAdapter.data = ...
        myAdapter.notifyDataSetChanged();
    }
});

As a side note, Android Studio definitely doesn't play a part here, it's an IDE. Don't get Android confused with Android Studio.

Henry Twist
  • 5,666
  • 3
  • 19
  • 44
  • Hi, thank you for your response. I'm a little confused with the String example though and how it relates to this. Can you please rephrase? Thanks. – CodingChap May 12 '21 at 22:42
  • So is the reason the adapter block of code gets called first because they are both running simultaneously on background threads(and the adapter block finishes first)? Thanks. – CodingChap May 12 '21 at 22:44
  • In regards to my string example: the thing you've passed into `addSnapshotListener` is just a class which extends/implements `EventListener`. So say I created a class which extended a `String` and I override a method (say `substring`). Then I passed an instance of my custom string class into a method. `substring` wouldn't be called by that method, it doesn't make any sense. It would only be called if the method called it! So with this example, `onEvent` only gets called if `addSnapshotListener` calls it, which it does *after* it's finished fetching the data on a background thread. – Henry Twist May 12 '21 at 22:50
  • Sorry if my string example is convoluted, if it doesn't make sense feel free to ignore it. In regards to your second question, I'm not sure exactly what you mean, what's getting called when? If your adapter code is below the `myCollection.whereEqual(...)` line then it will probably get called before `onEvent` if that's what you mean? – Henry Twist May 12 '21 at 22:51
  • Hi, thanks! Even if it takes time for the snapshot listener to call `onEvent()` why would that result in problems with the journal's list(journals) for the RecyclerViewAdapter(if I call it outside of onEvent())? Thanks – CodingChap May 12 '21 at 23:01
  • I didn't intend to make it this long... But I have updated my answer so hopefully that addresses my overly-bizarre string example and your actual problem at hand. If it doesn't, let me know! – Henry Twist May 12 '21 at 23:11
  • Hi, firstly, thank you so much for taking the time to write a thorough explanation. I really appreciate it. So, basically in order for onEvent() to be called, we have to invoke a callback, which will call the snapshot listener? How do we call the snapshot listener though? – CodingChap May 12 '21 at 23:23
  • How does this relate to the Recycler View Adapter being inside of onEvent(in which case everything works fine) and being outside of onEvent(in which case it doesn't work? Thanks – CodingChap May 12 '21 at 23:32
  • No problem, I think it's important to realise the simple (or not so simple!) mechanics behind this and realise it's not any Android magic etc! Not quite, in my example, the callback *is* the `EventListener`, so `onEvent` (`myMethod` in the example) gets called by Firebase whenever it's finished its network request. By calling `addSnapshotListener` you're effectively asking Firebase to do the network request, then call `onEvent` from my `EventListener` when they're ready. – Henry Twist May 12 '21 at 23:34
  • Can you provide a full example of it not working with it being outside? The example I've provided with it outside should work just fine. – Henry Twist May 12 '21 at 23:38
  • Hi, if I do it that way though, won't the list journals be null since I haven't populated it with the journal entries from the user in the onEvent code? Thanks. The case where it doesn't work for me which I tried is when I put the Adapter code below the ending parentheses of the whereEqualTo code – CodingChap May 12 '21 at 23:41
  • Sorry, never mind. Your way seems to work as well. Weirdly, the only way that doesn't work is putting the RecyclerAdapter code below outside of the whereEqualTo block – CodingChap May 12 '21 at 23:46
  • If you edit your question to include your example that doesn't work then I can probably tell you why! – Henry Twist May 12 '21 at 23:55
  • The reason that doesn't work is because `onEvent` is called when the network request has completed, whereas `notifyDataSetChanged` is called immediately. So your list might be changing, but you're not notifying the adapter at the correct time. The order of execution would be something like this: `request data -> set adapter -> notify adapter of changes (which don't exist yet) -> ... -> data is ready (onEvent)`. Notifying the adapter obviously has to come *after* the changes come, not before. – Henry Twist May 13 '21 at 01:19
  • 1
    Got it. Thank you! – CodingChap May 13 '21 at 03:30
1

While @Henry Twist's solution will work, please also note that you also have an alternative solution, which is the Firebase-UI library. This library does all the heavy work for you behind the scenes. You only need to start/stop listening for changes, and that's much pretty it. Please see below a working solution:

Alex Mamo
  • 130,605
  • 17
  • 163
  • 193
  • Hi, I am sorry for the late reply. I saw this post, and told myself to get back to it, but completely forgot. This looks interesting, will definitely be sure to implement it in my future projects. Thank you! – CodingChap May 19 '21 at 04:42