Search code examples
androidfirebase-realtime-databaselistener

Android: Get a Pojo List from Firebase during the first initialization from the Service Class


I try to get a List of all my "Example" Pojos from Firebase. My Service Class is a Singelton and in the Constructor I call the method initialize which calls the Method readAll(). So why is "readAll()" not working correctly ? It runns the onDataChange long after the return so it returns null instead of the Pojo List.

Service

public class Service {
    private static volatile Service instance = null;
    private List<Example> exampleList= new LinkedList<>();
    private ExampleDao exampleDao = new ExampleDao();


    public void initialize(){
       exampleList= exampleDao .readAll();
    }

   private Service(){
        initialize();
    }

    public static synchronized Service getInstance(){
        if(instance==null)
            instance = new Service();
        return instance;
    }
}

DAO

public class ExampleDao implements ExampleDao<Example> {
    private FirebaseDatabase database = FirebaseDatabase.getInstance();
    DatabaseReference myRef = database.getReference("example");
    private List<Example> pojoList=  null;


    @Override
    public List<Example> readAll() {
        // Read from the database

        myRef.addListenerForSingleValueEvent(new ValueEventListener() {
            @Override
            public void onDataChange(DataSnapshot dataSnapshot) {
                    pojoList= new LinkedList<>();
                for (DataSnapshot postSnapshot: dataSnapshot.getChildren()) {
                    pojoList.add(postSnapshot.getValue(Example.class));
                }




            }

            @Override
            public void onCancelled(DatabaseError error) {
                // Failed to read value
                Log.w(TAG, "Failed to read value.", error.toException());
            }
        });


        return pojoList;
    }
} 

Solution

  • Data is loaded from Firebase asynchronously.

    Since it may take quite some time for the data to come back from the server, and blocking the application during this time would lead to an "Application Not Responding" dialog, Firebase allows your application code to continue while it's loading the database. Then when the data comes back from the server, the Firebase client calls your onDataChange method with that data.

    The easiest way to see this is by adding a few log statements in your code:

    public List<Example> readAll() {
        Log.i(TAG, "Before attaching listener");
        myRef.addListenerForSingleValueEvent(new ValueEventListener() {
            @Override
            public void onDataChange(DataSnapshot dataSnapshot) {
                Log.i(TAG, "Inside onDataChange");
            }
    
            @Override
            public void onCancelled(DatabaseError error) {
                Log.w(TAG, "Failed to read value.", error.toException());
            }
        });
        Log.i(TAG, "After attaching listener");
    }
    

    If you run this code the output is:

    Before attaching listener

    After attaching listener

    Inside onDataChange

    That is probably not the order you expected. But it explains perfectly why the pojoList is empty when you return it: the data simply hasn't been loaded yet.

    There is no way to change this behavior: asynchronous APIs are inherent to programming apps against modern web APIs. The best way I've found to deal with this behavior is to reframe my solutions. Instead of thinking "first read all data, then do something with it", I think of it as "Start loading all data. Whenever the data is read, do something with it."

    In practice this means that all code that needs the data from the database needs to be (called from) inside the onDataChange method. So say that you want to print the loaded data, you'd do:

    public void readAll() {
        myRef.addListenerForSingleValueEvent(new ValueEventListener() {
            @Override
            public void onDataChange(DataSnapshot dataSnapshot) {
                pojoList= new LinkedList<>();
                for (DataSnapshot postSnapshot: dataSnapshot.getChildren()) {
                    pojoList.add(postSnapshot.getValue(Example.class));
                }
                Log.i(TAG, pojoList);
            }
    
            @Override
            public void onCancelled(DatabaseError error) {
                Log.w(TAG, "Failed to read value.", error.toException());
            }
        });
    }
    

    Now this makes your code a bit less flexible, since you're coding the logging into onDataChange. So you can also create your own callback interface, as shown in my answer here: getContactsFromFirebase() method return an empty list. This topic is confusing for a lot of developers new to Firebase. I highly recommend you check out some of the previous answers that are linked from there.