getContactsFromFirebase() method return an empty list

Solution 1:

Data is loaded from Firebase asynchronously. Since it may take some time to get the data from the server, the main Android code continues and Firebase calls your onDataChange when the data is available.

This means that by the time you return mContactsFromFirebase it is still empty. The easiest way to see this is by placing a few log statements:

System.out.println("Before attaching listener");
FirebaseDatabase.getInstance().getReference().child("Users")
    .addListenerForSingleValueEvent(new ValueEventListener() {
      @Override
      public void onDataChange(DataSnapshot dataSnapshot) {
        System.out.println("In onDataChange");
      }
      @Override
      public void onCancelled(DatabaseError databaseError) {
        throw databaseError.toException(); // don't ignore errors
      }
    });
System.out.println("After attaching listener");

When you run this code, it will print:

Before attaching listener

After attaching listener

In onDataChange

That is probably not the order that you expected the output in. As you can see the line after the callback gets called before onDataChange. That explains why the list you return is empty, or (more correctly) it is empty when you return it and only gets filled later.

There are a few ways of dealing with this asynchronous loading.

The simplest to explain is to put all code that returns the list into the onDataChange method. That means that this code is only execute after the data has been loaded. In its simplest form:

public void onDataChange(DataSnapshot dataSnapshot) {
    for (DataSnapshot snapshot : dataSnapshot.getChildren()) {
        Users user = snapshot.getValue(Users.class);
        assert user != null;
        String contact_found = user.getPhone_number();
        mContactsFromFirebase.add(contact_found);
        System.out.println("Loaded "+mContactsFromFirebase.size()+" contacts");
    }
}

But there are more approaches including using a custom callback (similar to Firebase's own ValueEventListener):

Java:

public interface UserListCallback {
  void onCallback(List<Users> value);
}

Kotlin:

interface UserListCallback {
  fun onCallback(value:List<Users>)
}

Now you can pass in an implementation of this interface to your getContactsFromFirebase method:

Java:

public void getContactsFromFirebase(final UserListCallback myCallback) {
  databaseReference.child(String.format("users/%s/name", uid)).addListenerForSingleValueEvent(new ValueEventListener() {
    @Override
    public void onDataChange(DataSnapshot dataSnapshot) {
      for (DataSnapshot snapshot : dataSnapshot.getChildren()) {
        Users user = snapshot.getValue(Users.class);
        assert user != null;
        String contact_found = user.getPhone_number();
        mContactsFromFirebase.add(contact_found);
        System.out.println("Loaded "+mContactsFromFirebase.size()+" contacts");
      }
      myCallback.onCallback(mContactsFromFirebase);
    }

    @Override
    public void onCancelled(DatabaseError databaseError) {
      throw databaseError.toException();
    }
  });
}

Kotlin:

fun getContactsFromFirebase(myCallback:UserListCallback) {
  databaseReference.child(String.format("users/%s/name", uid)).addListenerForSingleValueEvent(object:ValueEventListener() {
    fun onDataChange(dataSnapshot:DataSnapshot) {
      for (snapshot in dataSnapshot.getChildren())
      {
        val user = snapshot.getValue(Users::class.java)
        assert(user != null)
        val contact_found = user.getPhone_number()
        mContactsFromFirebase.add(contact_found)
        System.out.println("Loaded " + mContactsFromFirebase.size() + " contacts")
      }
      myCallback.onCallback(mContactsFromFirebase)
    }
    fun onCancelled(databaseError:DatabaseError) {
      throw databaseError.toException()
    }
  })

And then call it like this:

Java:

getContactsFromFirebase(new UserListCallback() {
  @Override
  public void onCallback(List<Users> users) {
    System.out.println("Loaded "+users.size()+" contacts")
  }
});

Kotlin:

getContactsFromFirebase(object:UserListCallback() {
  fun onCallback(users:List<Users>) {
    System.out.println("Loaded " + users.size() + " contacts")
  }
})

It's not as simple as when data is loaded synchronously, but this has the advantage that it runs without blocking your main thread.

This topic has been discussed a lot before, so I recommend you check out some of these questions too:

  • this blog post from Doug
  • Setting Singleton property value in Firebase Listener (where I explained how in some cases you can get synchronous data loading, but usually can't)
  • return an object Android (the first time I used the log statements to explain what's going on)
  • Is it possible to synchronously load data from Firebase?
  • https://stackoverflow.com/a/38188683 (where Doug shows a cool-but-complex way of using the Task API with Firebase Database)
  • How to return DataSnapshot value as a result of a method? (from where I borrowed some of the callback syntax)