I created the following interface:
private interface AllPicksFirebaseCallback {
void onSuccess(DataSnapshot dataSnapshot);
void onStart();
void onFailure();
}
and created the following readData function:
private void readData(Query query, SurvivorAllPicksActivity.AllPicksFirebaseCallback 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();
}
});
}
My Activity class has the following:
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
playerList = new ArrayList<>();
playerInPoolReference = mFirebaseDatabase.getReference("POOLPLAYERS");
playerInPoolQuery = playerInPoolReference.child(currPool).orderByValue().equalTo(true);
readData(playerInPoolQuery, new SurvivorAllPicksActivity.AllPicksFirebaseCallback() {
@Override
public void onSuccess(DataSnapshot dataSnapshot) {
int counter = 0;
for (DataSnapshot snapshot : dataSnapshot.getChildren()) {
String userId = snapshot.getKey();
setPlayerUsername(userId, counter, dataSnapshot.getChildrenCount());
counter = counter + 1;
}
}
@Override
public void onStart() {
Log.d("ONSTART", "Started");
}
@Override
public void onFailure() {
Log.d("onFailure", "Failed");
}
});
}
public void setPlayerUsername(String userId, final int position, long totalCount) {
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();
SurvivorAllPicks result = new SurvivorAllPicks(username);
//
playerList.add(result);
survivorPicksReference = mFirebaseDatabase.getReference("SURVIVORPICKS");
survivorPicksQuery = survivorPicksReference.child(currPool).child(userId);
// Completion Handler for Player Picks Lookups
readPlayerSurvivorPicks(survivorPicksQuery, new SurvivorAllPicksActivity.PlayerPicksCallback() {
@Override
public void onSuccess(DataSnapshot dataSnapshot) {
int pickIndex = 0;
for (DataSnapshot allPicksSnapshot : dataSnapshot.getChildren()) {
// *** NEED COMPLETION HANDLER ***
// Set the Players Picks Info here
playerList.get(position).setSurvivorAllPicks(allPicksSnapshot);
// Call the rest of the lookups based on the playerList info populated just above
setGameStatus(playerList, position, pickIndex, totalCount);
pickIndex = pickIndex + 1;
// *** NEED COMPLETION HANDLER ***
}
}
@Override
public void onStart() {
Log.d("ONSTART", "Started");
}
@Override
public void onFailure() {
Log.d("onFailure", "Failed");
}
});
} else {
// dataSnapshot doesn't exist
}
}
@Override
public void onCancelled(DatabaseError databaseError) {
Log.w("SurvivorAllPicksActivity", "onCancelled", databaseError.toException());
}
});
}
With the interface for the above code:
private interface PlayerPicksCallback {
void onSuccess(DataSnapshot dataSnapshot);
void onStart();
void onFailure();
}
private void readPlayerSurvivorPicks(Query query, SurvivorAllPicksActivity.PlayerPicksCallback 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();
}
});
}
public void setGameStatus(List<SurvivorAllPicks> playerList, final int position, int index, long totalCount) {
gameReference = mFirebaseDatabase.getReference("Games");
gameQuery = gameReference.child(playerList.get(position).getGameId()).child("isPlayed");
// Add handle for listener
gameQuery.addListenerForSingleValueEvent(new ValueEventListener() {
@Override
public void onDataChange(DataSnapshot dataSnapshot) {
if (dataSnapshot.exists()) {
Boolean gameStatus = (Boolean) dataSnapshot.getValue();
setPickInfo(playerList, gameStatus, position, index, totalCount);
}
}
@Override
public void onCancelled(DatabaseError databaseError) {
Log.w("SurvivorAllPicksActivity", "onCancelled", databaseError.toException());
}
});
}
public void setPickInfo(List<SurvivorAllPicks> playerList, Boolean gameStatus, final int position, int index, long totalCount) {
teamReference = mFirebaseDatabase.getReference("Teams");
teamQuery = teamReference.child(playerList.get(position).getTeamId());
// Add handle for listener
teamQuery.addListenerForSingleValueEvent(new ValueEventListener() {
@Override
public void onDataChange(DataSnapshot dataSnapshot) {
if (dataSnapshot.exists()) {
String teamLogo = dataSnapshot.child("Logo").getValue().toString();
String teamInit = dataSnapshot.child("Init").getValue().toString();
// Set the PicksDetails here!!!
playerList.get(position).setSurvivorAllPicksDetails(teamLogo, teamInit, index, gameStatus, playerList.get(position).getPoolStatus());
}
if (position == (totalCount - 1)) {
setContentView(new SurvivorAllPickLayout(getApplicationContext(), playerList));
}
}
@Override
public void onCancelled(DatabaseError databaseError) {
Log.w("SurvivorAllPicksActivity", "onCancelled", databaseError.toException());
}
});
}
My issue is the need for a completion handler; I marked the code where i think i need it.
What is happening is that the code keeps moving along and by the time the realtime database lookups are executed the lookup for the second iteration is already executing and it replicates the result for the array list.
How can i alter my code to stop that from happening? I need to make the lookup complete first for the first lookup before it moves forward for the second lookup and so on!?
The problem with your current workflow is that you are assuming that the firebase callbacks return in the order you call them, and that you can access data in an array based on that order, but that may not always be the case. As I understand your workflow you have several dependent queries:
I have included an example workflow below using a map of UserId -> Player that works more robustly with many chained queries like this.
To make this sort of thing easier to test, I have included a mock Firebase class that can help test out async workflows by initiating callbacks with random delays and returning some expected data you provide.
Because data is being returned in potentially non-deterministic order, your Player class needs to know when it is completely loaded. I added an
isLoaded()method to the dummy Player class I used here that checks if its fields are all non-empty.Using these components, I set up a workflow that mimics your example using a ViewModel and LiveData:
To robustly handle failures, you will need to set a failure flag on the Player when one of the queries fails and check for that in checkLoaded as well (either count is as being "loaded" or decrement totalPlayers and remove it from the playerData map).
The activity would initiate loading by calling
load()on the ViewModel, and observe the list of Players to see when they are all loaded.The mock firebase class is defined below. Putting your firebase calls behind an interface that you could mock like this can help with testing too.
And for completeness, the dummy Player class I used for this: