-1

I have the following Firebase DB node structure:

UserInGroup
    --- GroupID
      --- UserId : true/false  


Users
    --- UserId
      --- Username : String
      --- ...


GroupStatus
    --- GroupId
      --- UserId: true/false    

I need to pull for the first node to get all the users in the Group

Then use that info to get the users account info details

Finally check to see the users status in the Group

I cannot figure a way to implement the completionhandler in Java/Android ? I have done so for iOS with completionhandlers.

Can anyone assist with helping me implement the solution in Java?

---- UPDATE ----

I have done the following:

// Create an interface to init all the callback functions
private interface AllUsersCallback {

    void onSuccess(DataSnapshot dataSnapshot);

    void onStart();

    void onFailure();

}

private void readData(Query query, AllUsersActivity.AllUsersCallback listener) {

    listener.onStart();

    query.addListenerForSingleValueEvent(new ValueEventListener() {

        @Override
        public void onDataChange(DataSnapshot dataSnapshot) {

            if (dataSnapshot.exists()) {

                listener.onSuccess(dataSnapshot);

            } else { // dataSnapshot doesn't exist

            }

        }

        @Override
        public void onCancelled(DatabaseError databaseError) {

            Log.d(TAG, databaseError.getMessage());
            //
            listener.onFailure();

        }

    });

}

And lastly the Activity view:

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);

    // Init ArrayList
    userList = new ArrayList<>();

    userInGroupReference = mFirebaseDatabase.getReference("GroupUsers");

    userInGroupQuery = userInGroupReference.child(groupID).orderByValue().equalTo(true);

    // Completion Handler for Lookups
    readData(userInGroupQuery, new AllUsersActivity.AllUsersCallback() {

        @Override
        public void onSuccess(DataSnapshot dataSnapshot) {

            // Clear the List (remove dupes)
            userList.clear();

            for (DataSnapshot snapshot : dataSnapshot.getChildren()) {

                String userId = snapshot.getKey();
               
                // Call function to set usernames to the users
                setUsername(userId); 

            }

            /*
              THIS ALWAYS COMES OUT BLANK!? <--------
            */
            for (int i = 0; i < userList.size(); i++) {

                Log.e(TAG,"List element: " + userList.get(i).getUsername());

            }

        }

        @Override
        public void onStart() {

            // When starting
            Log.d("ONSTART", "Started");

        }

        @Override
        public void onFailure() {

            // If failed
            Log.d("onFailure", "Failed");

        }

    });

}

and the function used to set the users username to the userList:

public void setUsername(String userId) {

    userReference = mFirebaseDatabase.getReference("Users");

    userQuery = userReference.child(userId).child("username");

    // Add handle for listener
    userQuery.addListenerForSingleValueEvent(new ValueEventListener() {

        @Override
        public void onDataChange(DataSnapshot dataSnapshot) {

            if (dataSnapshot.exists()) {

                String username = dataSnapshot.getValue().toString();

                AllUsers result = new AllUsers(username);
                
                userList.add(result);

            }

        }

        @Override
        public void onCancelled(DatabaseError databaseError) {

        }

    });

}
Learn2Code
  • 1,974
  • 5
  • 24
  • 46
  • Call the subsequent queries from the `onComplete` (or whatever the callback is named) method from the first query. You need to show some code showing what you've tried to the question. – Tyler V Jun 11 '22 at 13:37
  • @TylerV i have added an update if you can assist – Learn2Code Jun 11 '22 at 14:23
  • So can't you just call the next query from the "do work" section? – Tyler V Jun 11 '22 at 14:27
  • Also, is this Firestore or Realtime database? – Tyler V Jun 11 '22 at 14:33
  • @TylerV Realtime Database – Learn2Code Jun 11 '22 at 14:33
  • Well the answer is the same, do the next call from inside the callback. – Tyler V Jun 11 '22 at 14:35
  • @TylerV so how would i call the consecutive queries in my code? Knowing that the first query result(s)/output will be used to do the execute the next query? – Learn2Code Jun 11 '22 at 14:35
  • @TylerV which one of the two sample code should i use? the first one or the second one. I am confused on how to write the callback in Java for Android? – Learn2Code Jun 11 '22 at 14:37
  • Try adding actual complete code sections to your question, we don't know what `readData` is or what queries you want to make. In the spot where you wrote "do work", make the next query using the data from snap. – Tyler V Jun 11 '22 at 14:37
  • There are lots of examples in [the docs](https://firebase.google.com/docs/database/admin/retrieve-data), what have you tried? – Tyler V Jun 11 '22 at 14:39
  • Also [here](https://firebase.google.com/docs/database/android/read-and-write) – Tyler V Jun 11 '22 at 14:48
  • @TylerV i have added my attempt at the completion handler. Please note that there is an issue which I dont understand why the list is not populated correctly, see my code with the comments with issue. – Learn2Code Jun 11 '22 at 21:26
  • Please read [this answer](https://stackoverflow.com/questions/57330766/why-does-my-function-that-calls-an-api-return-an-empty-or-null-value/70178210#70178210) - these types of database calls are asynchronous, they don't run in the order you write them. The callback runs later in time once it actually gets the data - which is why the list is empty where you check it. Check the size of `userList` inside `onDataChange` in your `setUsername` function. – Tyler V Jun 11 '22 at 22:33
  • Your code overall is not wrong - you're just checking the size too early. The real question is what do you want to happen once the list is populated? – Tyler V Jun 11 '22 at 22:38
  • @TylerV how do I "wait" for the list to be fully populated?! That is what i thought the completion handler does (it works for iOS). How do i check when its done vs "too early" as you stated? Lastly, I want to take that list and do another firebase query inorder to get more information. – Learn2Code Jun 11 '22 at 22:50
  • @TylerV 1. I need to pull for the first node to get all the users in the Group 2. Then use that info to get the users account info details 3. Finally retrieve the user's status in the Group – Learn2Code Jun 11 '22 at 22:52
  • That's not what I was asking - I wanted to know what you want to do with the data once you get it all. I posted one option where you use a counter to check if you are all done getting the data. A cleaner option would be to do this in a ViewModel, post the updated data to LiveData, and react accordingly. – Tyler V Jun 11 '22 at 22:58

1 Answers1

0

These database calls are asynchronous - the callback code does not run immediately, it runs some time in the future when you actually get the data.

The easiest way to chain multiple dependent async queries is to put each query into its own function, and call it from the dependent query's callback. In your case, you could have multiple callbacks running at once, so as each one completes you can check for it to be done and check for them all to be done by comparing the size of the list with the number of queries launched.

For example:

private ArrayList<String> userList = new ArrayList<>();
private int numUsers = 0;

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    
    // other setup stuff
    startInitialQuery();
}

private void startInitialQuery() {
    // make your initial query
    
    query.addListenerForSingleValueEvent(new ValueEventListener() {

        @Override
        public void onDataChange(DataSnapshot dataSnapshot) {
            if (dataSnapshot.exists()) {
                userList.clear();
                numUsers = 0; // dataSnapshot.getChildren().size();
                
                // If the size() call above works, use that, otherwise
                // you can count the number of children this way.
                for(DataSnapshot snap : dataSnapshot.getChildren()) {
                    ++numUsers;
                }   
                
                for(DataSnapshot snap : dataSnapshot.getChildren()) {
                    String userId = snap.getKey();
                    readUser(userId);
                }
            }
        }

        @Override
        public void onCancelled(DatabaseError databaseError) {
            Log.d(TAG, databaseError.getMessage());
        }
    });
}

private void readUser(String userId) {

    // make userQuery using "userId" input
    
    userQuery.addListenerForSingleValueEvent(new ValueEventListener() {

        @Override
        public void onDataChange(DataSnapshot dataSnapshot) {
            if (dataSnapshot.exists()) {
                String username = dataSnapshot.getValue().toString();
                userList.add(username);
                checkLoaded();
            }
            else {
                --numUsers;
                checkLoaded();
            }
        }

        @Override
        public void onCancelled(DatabaseError databaseError) {
            Log.d(TAG, databaseError.getMessage());
            --numUsers;
            checkLoaded();
        }
    });
}

private void checkLoaded() {
    if( userList.size() == numUsers ) {
        // All done getting users! Show a toast, update a view, etc...
    }
}

Alternately, if you switch to using Kotlin and coroutines you can write this as a pretty simple linear suspend function where you can actually make the different tasks wait.

A cleaner, but more invasive change, would be to move this all to a ViewModel that contains LiveData of each of these steps. As data is received, you post it to the LiveData and the UI can observe that and react accordingly (e.g update views, trigger the next call, etc).

Update

Here is an example showing how to do this with a ViewModel and LiveData


public class MainViewModel extends ViewModel {
    private final MutableLiveData<List<String>> users = new MutableLiveData<>();
    LiveData<List<String>> getUsers() {
        return users;
    }

    private final ArrayList<String> userList = new ArrayList<>();

    void startFetchingData() {
        // build query
        query.addListenerForSingleValueEvent(new ValueEventListener() {

            @Override
            public void onDataChange(DataSnapshot dataSnapshot) {
                if (dataSnapshot.exists()) {
                    userList.clear();

                    for(DataSnapshot snap : dataSnapshot.getChildren()) {
                        String userId = snap.getKey();
                        readUser(userId);
                    }
                }
            }

            @Override
            public void onCancelled(DatabaseError databaseError) {
                Log.d(TAG, databaseError.getMessage());
            }
        });
    }


    private void readUser(String userId) {
        // build userQuery
        userQuery.addListenerForSingleValueEvent(new ValueEventListener() {

            @Override
            public void onDataChange(DataSnapshot dataSnapshot) {
                if (dataSnapshot.exists()) {
                    String username = dataSnapshot.getValue().toString();
                    userList.add(username);
                    users.postValue(userList);
                }
            }

            @Override
            public void onCancelled(DatabaseError databaseError) {
                Log.d(TAG, databaseError.getMessage());
            }
        });
    }
}

and in the activity you set an observer for the LiveData that is notified any time the data changes.

model = new ViewModelProvider(this).get(MainViewModel.class);

final Observer<List<String>> userObserver = userList -> {
    // Update the UI, or call something else
    // this will get called every time the list of users is
    // updated in the ViewModel
    System.out.println("TEST: got data " + userList);
};

// Observe the LiveData, passing in this activity as the LifecycleOwner and the observer.
model.getUsers().observe(this, userObserver);

model.startFetchingData();
Tyler V
  • 9,694
  • 3
  • 26
  • 52
  • Switching to Kotlin gonna be tough since i have all the rest of the code in java. Do you have an example of a ViewModel that contains LiveData? – Learn2Code Jun 11 '22 at 23:10
  • I can add one - you don't have to switch the entire app to Kotlin, you can mix and match. – Tyler V Jun 11 '22 at 23:11
  • Really!? U can mix and match java and Kotlin!? So can I have the Activity in Kotlin and the Layout (View) in Java? – Learn2Code Jun 11 '22 at 23:14
  • Yes, you could write a ViewModel in Kotlin but keep other stuff in Java. I gradually migrated my stuff to kotlin one file at a time - keeping it working for the intermediate steps. Android Studio has a "convert this java file to Kotlin" feature that works reasonably well. – Tyler V Jun 11 '22 at 23:16
  • That is awesome to know!! Im new to Android and more accustom to iOS. – Learn2Code Jun 11 '22 at 23:18