Android - Firebase - Issue with getChildrenCount() method - android

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??

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:
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.
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.

Related

Android Studio/Firebase: Trying to retrieve all data but only getting the last item

I am trying to make an app that displays jobs added by users on a map. In order to do this, I am trying to create a realtime Firebase Database that stores all jobs:
In the onCreate method, markers(jobs) will be added from the list of all current jobs in the Firebase database.
Here is my code so far:
DatabaseReference ref1 = database.getReference();
DatabaseReference jobsReference = ref1.child("Jobs");
jobsReference.addListenerForSingleValueEvent(new ValueEventListener() {
#Override
public void onDataChange(#NonNull DataSnapshot dataSnapshot) {
jobs = new ArrayList<>();
// AddJobHandler is the object I use to store the jobs
for(DataSnapshot jobSnapShot : dataSnapshot.getChildren()) {
AddJobHandler newJob = jobSnapShot.getValue(AddJobHandler.class);
jobs.add(newJob);
}
for(int i = 0; i < jobs.size(); i++) {
Toast.makeText(map.this, jobs.get(i).getDesc(), Toast.LENGTH_SHORT).show();
}
}
#Override
public void onCancelled(#NonNull DatabaseError databaseError) {
}
});
When I run the app, I get 3 toasts, but titles that are of the most recent job added. How can I add all of the jobs to the jobs ArrayList from the database?
TL; DR: The jobs ArrayList only seems to add the most recent job. How can I get the ArrayList to add all of the jobs that are in the Firebase Realtime Database
Seems like there is only one change needed in your code, Please check this,
jobs = new ArrayList<>(); //write this in onCreate(); or while declaration in global scope.
Your code Below
DatabaseReference ref1 = database.getReference();
DatabaseReference jobsReference = ref1.child("Jobs");
jobsReference.addListenerForSingleValueEvent(new ValueEventListener() {
#Override
public void onDataChange(#NonNull DataSnapshot dataSnapshot) {
// AddJobHandler is the object I use to store the jobs
for(DataSnapshot jobSnapShot : dataSnapshot.getChildren()) {
AddJobHandler newJob = jobSnapShot.getValue(AddJobHandler.class);
jobs.add(newJob);
}
for(int i = 0; i < jobs.size(); i++) {
Toast.makeText(map.this, jobs.get(i).getDesc(), Toast.LENGTH_SHORT).show();
}
}
#Override
public void onCancelled(#NonNull DatabaseError databaseError) {
}
});
Every time that you get an event in dataSnapshot you create new ArrayList, so you have just last. try to do jobs = new ArrayList<>(); in constructor of class or in some another place. it must be fired once.
Also it good to check if (dataSnapshot.exist) before some actions with it.

Is it possible to sort the Firebase data and apply it to the Real Time DB on Android Studio?

I am trying to sort my Firebase DB data on the Android Studio side and apply it in real-time. My codes successfully updates the view immediately when I make a manual change on my DB, but I wonder if I can sort by name using the code. I thought orderByChild was the function to do that, but it does not change the database.
Function that fetches data from firebase and displays:
private void fetchRepoData(String profileUserID) {
profileUserDatabase = repoReference.child(profileUserID);
//DatabaseReference reposReference = profileUserDatabase.child("Repos");
profileUserDatabase.addValueEventListener(new ValueEventListener() {
#Override
public void onDataChange(DataSnapshot dataSnapshot) {
repoListItems.clear();
repoNameList.clear();
userNameList.clear();
descriptionList.clear();
Toast.makeText(getActivity(), "changed", Toast.LENGTH_SHORT).show();
for(DataSnapshot snapshot : dataSnapshot.getChildren()) {
repoNameList.add(snapshot.getValue(RepoInfo.class).getRepoName());
userNameList.add(snapshot.getValue(RepoInfo.class).getOwner());
descriptionList.add(snapshot.getValue(RepoInfo.class).getDescription());
}
for(int i = 0; i < repoNameList.size(); i++) {
repoListItems.add(new RepositoryItem(i, repoNameList.get(i), userNameList.get(i), descriptionList.get(i)));
}
adapter.notifyDataSetChanged();
}
#Override
public void onCancelled(DatabaseError databaseError) {
}
});
}
Radio button for sorting by name
sortByNameRadio.setOnClickListener(
new View.OnClickListener() {
#Override
public void onClick(View v) {
repoReference.child("subchild").orderByChild("name");
adapter.notifyDataSetChanged();
}
}
);
When you call orderByChild() it returns a new Query object. You'll need to store a reference and attach a listener that query object.
profileUserDatabase = repoReference.child(profileUserID);
Query query = repoReference.child("subchild").orderByChild("name");
query.addValueEventListener(new ValueEventListener() {
for(DataSnapshot snapshot: dataSnapshot.getChildren()) {
System.out.println(snapshot.getKey());
...

How to synchronise methods of Firebase

In my Firebase database, I have two children let Child1, Child2. Child1 contains profile information of users as Root/Child1/Uid-User(x)/User_details. Child2 contains transactions done by users as Root/Child2/Uid-User(x)/Transaction_details. I want to retrieve user profile info of one user then transaction of the same user then store it in some object. Then profile info of another user, his transaction and store it in another object and so on.
How can I do that? as Firebase methods run asynchronously so if I create one method to retrieve from Child1 and another for Child2 then they execute asynchronously and create the problem.
You can retrieve it Initially by putting a listener to the userDetail node once this is done ensure retrieved data should not be null after again you make another request to retrive his transction data
Or You Retrieve the both separately and put the result into a 2 HashMap once the data retrieved from both listeners you can merge them based on the key (Ensure that the length of both map should be same)
To ensure Both the listeners are invoked you make like this
Initially put two variables
userProfileHit = 0;
userTransctionHit = 0;
privte void userList () {
userProfileHit ++;
private ValueEventListener userListener = new ValueEventListener() {
#Override
public void onDataChange(DataSnapshot dataSnapshot) {
//retriveData
userProfileHit --;
doMergingTask ();
}
#Override
public void onCancelled(DatabaseError databaseError) {
userProfileHit --;
doMergingTask ()
}
};
}
private void transctionList () {
userTransctionHit ++;
private ValueEventListener transctionListener = new ValueEventListener() {
#Override
public void onDataChange(DataSnapshot dataSnapshot) {
userTransctioneHit --;
doMergingTask ()
}
#Override
public void onCancelled(DatabaseError databaseError) {
userTransctionHit --;
doMergingTask ()
}
};
}
private void doMergingTask () {
if (userTransctionHit == 0 && userProfileHit == 0) {
// Check Datas are valid and merge it
}
}
My suggestion is that you put the method for retrieving the users transactions inside the onDataChanged method for retrieving profile information. So that, when you get the users profile information, you use it to get users transaction information and create the object. You can nest the firebase event listeners to produce the any outcome you want.
For Example:
final FirebaseDatabase database = FirebaseDatabase.getInstance();
final DatabaseReference ref = database.child("profile");
// Attach the first listener to read the data at our profile reference
ref.addValueEventListener(new ValueEventListener() {
#Override
public void onDataChange(DataSnapshot dataSnapshot) {
final Profile profile = dataSnapshot.getValue(Profile.class);
DatabaseReference transactionRef = database.child("transaction");
// Attach a listener to read the data at our transactions reference
transactionRef.addValueEventListener(new ValueEventListener() {
#Override
public void onDataChange(DataSnapshot dataSnapshot) {
Transaction transaction = dataSnapshot.getValue(Transaction.class);
//
//Do stuff with the Transaction object and Profile object here.
//
}
#Override
public void onCancelled(DatabaseError databaseError) {
System.out.println("The read failed: " + databaseError.getCode());
}
});
}
#Override
public void onCancelled(DatabaseError databaseError) {
System.out.println("The read failed: " + databaseError.getCode());
}
});

Firebase query n items and listen to new only

I am trying to query last 5 items and then listen only to new childs. However,
when I add new child, childeventlistener returns the last child from thos 5 i queried earlier.
query = myRef.orderByKey().limitToLast(5);
query.addListenerForSingleValueEvent(new ValueEventListener() {
#Override public void onDataChange(DataSnapshot dataSnapshot) {
if (dataSnapshot.exists()) {
// dataSnapshot is the "issue" node with all children with id 0
int oldSize = mData.size();
List<ChatMessageEntity> data = new ArrayList<>();
for (DataSnapshot issue : dataSnapshot.getChildren()) {
// do something with the individual "issues"
ChatMessageEntity chat = issue.getValue(ChatMessageEntity.class);
data.add(0,chat);
}
mData.addAll(data);
adapter.notifyItemRangeInserted(oldSize, mData.size());
}
query.addChildEventListener(newMessageListener);
}
#Override public void onCancelled(DatabaseError databaseError) {
}
});
With this type of scenario you're better off using a ChildEventListener. This has a method onChildAdded that will be called for (up to) five children initially and then for each new child added later.
query = myRef.orderByKey().limitToLast(5);
query.addChildEventListener(new ChildEventListener() {
#Override
public void onChildAdded(DataSnapshot dataSnapshot, String previousChildName) {
ChatMessageEntity chat = issue.getValue(ChatMessageEntity.class);
data.add(0,chat);
adapter.notifyItemInserted(mData.size()-1);
}
...
#Override public void onCancelled(DatabaseError databaseError) {
throw databaseError.toException(); // don't ignore these
}
});
The above code works, but is missing handling of previousChildName (as children that arrive later might not be added to the end of the list), onChildChanged, onChildMoved and onChildRemoved. Adding handling for all of these is a fun exercise, but quite a bit of work and a bit tricky to get right. If you want to complete the work, I recommend taking a look at the RecyclerViewAdapter in FirebaseUI and its work horse FirebaseArray - which are nicely battle tested.
For more information see the Firebase documentation on listening for child event.
The best i make up is to load required number of items, save key of the newst one. Remove item with that key from list. Create new query which starts from key of the newest and add childEventListener to it.
query = myRef.orderByKey().limitToLast(20);
query.addListenerForSingleValueEvent(new ValueEventListener() {
#Override public void onDataChange(DataSnapshot dataSnapshot) {
if (dataSnapshot.exists()) {
// dataSnapshot is the "issue" node with all children with id 0
int oldSize = mData.size();
//List<ChatMessageEntity> data = new ArrayList<>();
for (DataSnapshot issue : dataSnapshot.getChildren()) {
// do something with the individual "issues"
ChatMessageEntity chat = issue.getValue(ChatMessageEntity.class);
chat.key = issue.getKey();
mData.add(chat);
//data.add(0, chat);
}
//mData.addAll(data);
}
Log.e("mcheck", "onDataChange: " + mData.get(0).message);
Query query;
if (mData.size() > 0) {
query = myRef.orderByKey().startAt(mData.get(mData.size() - 1).key);
olderstKey = mData.get(0).key;
mData.remove(mData.size() - 1);
adapter.notifyItemRemoved(mData.size()-1);
adapter.notifyItemRangeInserted(0, mData.size());
//adapter.notifyDataSetChanged();
query.addChildEventListener(newMessageListener);
} else {
myRef.addChildEventListener(newMessageListener);
}
}
#Override public void onCancelled(DatabaseError databaseError) {
}
});

How can I retrieve some fields from my Firebase database before I push any changes to it?

Currently I create a Listing object and store a bunch of fields in there. Two of the fields I need to store are the current User's email and name. I am trying to get those two fields as follows
dbRef = database.getReference().child("Users").child(emailKey);
dbRef.addListenerForSingleValueEvent(new ValueEventListener() {
#Override
public void onDataChange(DataSnapshot dataSnapshot) {
UserInfo userIn = dataSnapshot.getValue(UserInfo.class);
email1 = userIn.email;
sellerName = userIn.username;
}
#Override
public void onCancelled(DatabaseError databaseError) {
}
});
After this bit of code I have the line
DatabaseReference temp = dbRefL.push();
temp.setValue(l);
All of this code is called by me pressing a button. The first time I press the button, the entire Listing object is pushed to Firebase just the way I want it EXCEPT the user email and username aren't there because they're blank. The second time I press the button the Strings are there how I want.
My guess is that this is because OnDataChange only executes after I push the Listing object. Is this true? How can I get OnDataChange to execute before I push the listing object?
The listener onDataChange() callbacks are asynchronous. The reason that your email and username is blank is because onDataChange hasn't been executed yet to make sure you push data after email and username are retrieve, put the code inside onDataChange()
dbRef = database.getReference().child("Users").child(emailKey);
dbRef.addListenerForSingleValueEvent(new ValueEventListener() {
#Override
public void onDataChange(DataSnapshot dataSnapshot) {
UserInfo userIn = dataSnapshot.getValue(UserInfo.class);
email1 = userIn.email;
sellerName = userIn.username;
//set up your l value
DatabaseReference temp = dbRefL.push();
temp.setValue(l);
}
#Override
public void onCancelled(DatabaseError databaseError) {
}
});
Once you press the button above code should be called to obtain email and username then push the data as you want.
User addValueEvenListener like :
DatabaseReference map = database.getReference("field_name");
map.addValueEventListener(new ValueEventListener() {
#Override
public void onDataChange(DataSnapshot dataSnapshot) {
String data = (String) dataSnapshot.getValue();
}
#Override
public void onCancelled(DatabaseError databaseError) {
}
});
****OR using****
database.getReference("field_name").addValueEventListener(new ValueEventListener() {
#Override
public void onDataChange(DataSnapshot dataSnapshot) {
Log.e(TAG, "Field data", dataSnapshot.getValue(String.class));
}
#Override
public void onCancelled(DatabaseError databaseError) {
// Failed to read value
Log.e(TAG, "Failed to read user", databaseError.toException());
}
});

Categories

Resources