Kostas_Me_
Kostas_Me_

Reputation: 113

Android - Firebase - Issue with getChildrenCount() method

Currently, I'm building an Android App which is using a Firebase Database.

In my code, I'm trying to get the number of Children in a specific path of my Firebase Database. Having defined the reference of the Firebase specific path, I have made a Listener in order to get the DataSnapshot and then to call getChildrenCount().

After that, I want to use this result in a FOR-Loop. However, the code doesn't work.

It appears that it is executed first the FOR-Loop (I realised that because Log.v("NUM_OF_PRODUCTS",numOfProducts+"") outputs 0 insteed of 3) and the Listener is executed after, while in the code, listener code comes first and FOR-Loop code next.

Part of my code:

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

    selected_cat = getIntent().getExtras().getString("pass_selected_cat");
    listView = (ListView) findViewById(R.id.listView);
    final ArrayAdapter<String> adapter = new ArrayAdapter<>(this, android.R.layout.simple_list_item_1, arrayList);
    listView.setAdapter(adapter);
    fbdatabase = FirebaseDatabase.getInstance();
    fbref_products = fbdatabase.getReference("PRODUCTS/SM_0/" + selected_cat);

    //Listener for getting numOfProducts.
    fbref_products.addListenerForSingleValueEvent(new ValueEventListener() {
        @Override
        public void onDataChange(DataSnapshot dataSnapshot) {
            numOfProducts = (int) dataSnapshot.getChildrenCount();
        }
        @Override
        public void onCancelled(DatabaseError databaseError) {
        }
    });

    //Print numOfProducts before FOR-Loop being executed.
    Log.v("NUM_OF_PRODUCTS",numOfProducts+"");


    //FOR-Loop 
    for (int i = 0; i < numOfProducts; i++) {
        String str = i + "";
        DatabaseReference product_ref = fbref_products.child(str);

        product_ref.addListenerForSingleValueEvent(new ValueEventListener() {
            @Override
            public void onDataChange(DataSnapshot dataSnapshot) {
                Map<String, Object> map = (Map<String, Object>) dataSnapshot.getValue();
                String productName = (String) map.get("productName");

                arrayList.add(productName);
                adapter.notifyDataSetChanged();
            }

            @Override
            public void onCancelled(DatabaseError databaseError) {
                //textView.setText("The read failed: " + databaseError.getMessage());
            }
        });
    }

My code works fine if instead the variable numOfProducts, we had a specific number.

Could anyone help me??

Upvotes: 1

Views: 3029

Answers (1)

Frank van Puffelen
Frank van Puffelen

Reputation: 600131

Data from the Firebase Database is loaded asynchronously. By the time you run your for loop it hasn't been loaded yet.

It is easiest to see this if you place a few log statements in your code:

System.out.println("Before attaching listener");
fbref_products.addListenerForSingleValueEvent(new ValueEventListener() {
    @Override
    public void onDataChange(DataSnapshot dataSnapshot) {
        System.out.println("In onDataChange");

    }
    @Override
    public void onCancelled(DatabaseError databaseError) {
    }
});
System.out.println("After attaching listener");

When you run this, the output will be:

Before attaching listener

After attaching listener

In onDataChange

This is probably not the order that you expected. But it explains perfectly why numOfProducts is still 0 when you start the loop: the data simply hasn't been loaded from Firebase yet.

In initial reaction from pretty much every developer is "I don't want this. How do I make it execute in the right order?" This is a natural response, but one that you'll have to suppress to get anywhere when programming against web/cloud APIs. I also recommend reading my answer here: Setting Singleton property value in Firebase Listener

The solution is to reframe your solution from "first I'll get the number of products and then I'll loop over the products" to "whenever I get the products, I'll loop over them".

In code that is:

fbref_products.addListenerForSingleValueEvent(new ValueEventListener() {
    @Override
    public void onDataChange(DataSnapshot dataSnapshot) {
        numOfProducts = (int) dataSnapshot.getChildrenCount();
        for (int i = 0; i < numOfProducts; i++) {
            String str = i + "";
            DatabaseReference product_ref = fbref_products.child(str);
    
            product_ref.addListenerForSingleValueEvent(new ValueEventListener() {
                @Override
                public void onDataChange(DataSnapshot dataSnapshot) {
                    Map<String, Object> map = (Map<String, Object>) dataSnapshot.getValue();
                    String productName = (String) map.get("productName");
    
                    arrayList.add(productName);
                    adapter.notifyDataSetChanged();
                }
    
                @Override
                public void onCancelled(DatabaseError databaseError) {
                    //textView.setText("The read failed: " + databaseError.getMessage());
                }
            });
        }
    }
    @Override
    public void onCancelled(DatabaseError databaseError) {
    }
});

Aside from the whole dealing with asynchronous events, you can significantly simplify this code:

fbref_products.addListenerForSingleValueEvent(new ValueEventListener() {
    @Override
    public void onDataChange(DataSnapshot dataSnapshot) {
        for (DataSnapshot productSnapshot: dataSnapshot.getChildren()) {
            String productName = productSnapshot.child("productName").getValue(String.class);
            arrayList.add(productName);
            adapter.notifyDataSetChanged();
        }        
    }
    @Override
    public void onCancelled(DatabaseError databaseError) {
    }
});

Changes:

  1. this uses only a single listener. Your second listener is not needed, since retrieving a node already retrieves all data under that node as well. You can loop over the child nodes.
  2. this doesn't first extract a HashMap, but simply reads the data from the child snapshot directly.

One more change you should consider is to keep a separate list of product names. You now retrieve all product data to show a list of names, which is wasteful. If you keep a separate list of just the product names, you can load that instead. You'll find that a common theme when using Firebase (or almost any NoSQL database): model the data in your database the way you show it on the screen.

Upvotes: 2

Related Questions