I'm using Firebase Cloud Firestore with Firestore UI RecyclerView to display items on MainActivity. Everything works fine except that, when I uninstall and re-install the app, query does not fetch previously added items and only an empty list appears. When I add a new item to Firestore after re-install, only that item is fetched and still no previous data. However, I can see both previously added data and the new item on Firebase Console.
Has anyone experienced a similar issue or any idea what can cause this?
My function that sets up the RecyclerView is as follows. I call this function and then call adapter.startListening() in onStart and adapter.stopListening() in onStop.
private void setupRecyclerView() {
if(shouldStartSignIn()) return;
if(!PermissionUtils.requestPermission(this, RC_STORAGE_PERMISSION, android.Manifest.permission.WRITE_EXTERNAL_STORAGE)) {
Log.e(TAG, "Permission not granted, don't continue");
return;
}
LinearLayoutManager layoutManager = new LinearLayoutManager(this);
pagesRecyclerView.setLayoutManager(layoutManager);
Query query = FirebaseFirestore.getInstance().collection(Page.COLLECTION).whereEqualTo(Page.FIELD_USER_ID, FirebaseAuth.getInstance().getCurrentUser().getUid()).orderBy(Page.FIELD_TIMESTAMP).limit(50);
FirestoreRecyclerOptions<Page> options = new FirestoreRecyclerOptions.Builder<Page>().setQuery(query, Page.class).build();
adapter = new PagesAdapter(options, this);
pagesRecyclerView.setAdapter(adapter);
}
You have initialized yourquery but the query is not attached to any listener which actually pulls the data. So your adapter is empty. Basically you need to do something like this:
query.addSnapshotListener(new EventListener<QuerySnapshot>() {
#Override
public void onEvent(#Nullable QuerySnapshot snapshot,
#Nullable FirebaseFirestoreException e) {
if (e != null) {
// Handle error
//...
return;
}
// Convert query snapshot to a list of chats
List<Chat> chats = snapshot.toObjects(Chat.class);
// Update UI
// ...
}
});
You can read more about it here: https://github.com/firebase/FirebaseUI-Android/tree/master/firestore#querying
Related
Hello guys and thanks you for your time.
So in my app, I'm trying to fetch data in real time from my firestore database with a SnapshotListener and then save it in a global variable.
The fetching itself works fine, but when I try to save it into an array it fails. It says the array is empty afterwards.
Can you guys explain me what's wrong since I'm still starting on this firestore thing?
Here's the part of the code I think is helpfull:
// Get barbershops nearby from google firestore
MainActivity.db.collection("barbershops")
.addSnapshotListener(new EventListener<QuerySnapshot>() {
#Override
public void onEvent(#Nullable QuerySnapshot value,
#Nullable FirebaseFirestoreException e) {
if (e != null) {
Log.w(TAG, "Listen failed.", e);
return;
}
for (QueryDocumentSnapshot doc : value) {
Log.d(TAG, doc.getId() + " => " + doc.getData());
Barbershop bbs = doc.toObject(Barbershop.class);
barbershops.add(bbs);
}
}
});
progressBar.setVisibility(View.GONE);
// Set up the recycler view
RecyclerView recyclerView = view.findViewById(R.id.recyclerView);
// use a linear layout manager
RecyclerView.LayoutManager layoutManager = new LinearLayoutManager(getActivity());
recyclerView.setLayoutManager(layoutManager);
mAdapter = new MyAdapter(barbershops, getContext(), this);
recyclerView.setAdapter(mAdapter);
return view;
and then I also have a global variable:
private ArrayList<Barbershop> barbershops;
Thanks!
addSnapshotListener is asynchronous and returns immediately. The callback is invoked some time later with the results of the query. That means barbershops won't be populated right away. It's not clear from your question what "the array is empty afterwards" actually means, but if you try to use barbershops before the callback has been invoked, you will not see the data from those documents. You should only use that list after the callback is invoked.
I am fetching JSON array of user ids from a server (not Firebase server).
I also store images of each user in Firebase storage. I have a dataset of Users, that contain user id and user image url. The JSON response is constantly updating, so every call I receive new response from the server with new list of user ids. The only solution I came up with, is to:
Clear dataset > Loop through the JSON Array to add all users to the empty dataset > notify dataset changed.
The problem with this is that it's not efficient: I notify data set changed on each iteration, and also since I clear the dataset every new response (from the remote server), the list refreshes, instead of simply adding / removing the necessary users.
This is how the code looks:
#Override
public void onResponse(JSONArray response) { // the JSON ARRAY response of user ids ["uid1", "uid334", "uid1123"]
myDataset.clear(); // clear dataset to prevent duplicates
for (int i = 0; i < response.length(); i++) {
try {
String userKey = response.get(i).toString(); // the currently iterated user id
final DatabaseReference rootRef = FirebaseDatabase.getInstance().getReference();
DatabaseReference userKeyRef = rootRef.child("users").child(userKey); // reference to currently iterated user
ValueEventListener listener = new ValueEventListener() {
#Override
public void onDataChange(DataSnapshot dataSnapshot) {
myDataset.add(new User(dataSnapshot.getKey(), dataSnapshot.child("imageUrl").getValue().toString())); //add new user: id and image url
mAdapter.notifyDataSetChanged(); // notify data set changed after adding each user (Not very efficient, huh?)
}
#Override
public void onCancelled(#NonNull DatabaseError databaseError) {
Log.d(TAG, databaseError.getMessage());
}
};
userKeyRef.addListenerForSingleValueEvent(listener);
}
catch (JSONException e) { Log.d(TAG, "message " + e); }
}
You might be interested in DiffUtil.
It uses an efficient algorithm to calculate the difference between your lists. And the cherry on the top is that this can be run on a background thread.
It is an alternative to notifyDataSetChanged() and is sort of an industry standard way for updating your RecyclerView
You can use Firebase Cloud functions.Pass the JSON Array to cloud function and retrieve the updated dataset in one go and notify the recycle view Here is link
I have a chat activity that loads the 5 most recent messages of a chat room in descending order from the bottom to the top of a reversed RecyclerView using a Firebase query. Messages sent to the Firebase collection after those messages are loaded are displayed at the bottom of the screen by changing the position new list items are inserted into the list after the onEvent is called. Here is my code:
private void initRecyclerView() {
//initializes and sets adapter/resets variables
mAdapter = new ChatRecyclerViewAdapter(mMessages);
mRecyclerView.setAdapter(mAdapter);
firstEventListenerCalled = false;
final LinearLayoutManager linearLayoutManager = new LinearLayoutManager(this);
linearLayoutManager.setReverseLayout(true);
mRecyclerView.setLayoutManager(linearLayoutManager);
Query query = mCollection.orderBy("timestamp", Query.Direction.DESCENDING).limit(5;
firstRegistration = query.addSnapshotListener(new EventListener<QuerySnapshot>() {
#Override
public void onEvent(#Nullable QuerySnapshot queryDocumentSnapshots, #Nullable FirebaseFirestoreException e) {
Log.d("First SnapshotListener", "Called");
for (DocumentChange documentChange : queryDocumentSnapshots.getDocumentChanges()) {
switch (documentChange.getType()) {
case ADDED:
//Creates listener for queried messages that puts messages retrieved during initial query at end of message list,
// then another one after one runs its course that puts new ones at the beginning of the list
if (!firstEventListenerCalled) {
Message message = documentChange.getDocument().toObject(Message.class);
mMessages.add(message);
mAdapter.notifyDataSetChanged();
} else if (firstEventListenerCalled) {
Message message = documentChange.getDocument().toObject(Message.class);
mMessages.add(0, message);
mAdapter.notifyDataSetChanged();
Log.d(TAG, "newMessageAdded " + message.getMessage());
}
}
}
firstEventListenerCalled = true;
}
});
I am attempting to remove the ListenerRegistration in the activity's onStop() method, clear the contents of the RecyclerView in the onRestart() method, and then re-call the method above in onStart(), so that no messages come in when the activity is stopped and that any messages sent during that time appear among the 5 queried by the initRecyclerView() method when the activity is restarted:
#Override
protected void onRestart() {
super.onRestart();
mMessages.clear();
mAdapter.notifyItemRangeRemoved(0, mMessages.size());
}
#Override
protected void onStart() {
super.onStart();
Log.d(TAG, "onStart Called");
initRecyclerView();
}
#Override
protected void onStop() {
super.onStop();
Log.d(TAG, "onStop Called");
firstRegistration.remove();
if (refreshRegistraton != null) {
refreshRegistraton.remove();
}
}
#Override
public void onBackPressed() {
finish();
}
However, the following happens when the activity is restarted using this strategy:
The 5 queried messages appear in the proper order
Any messages sent while the activity was stopped appear in addition to those 5 messages and in reverse order:
Old message 1
Old message 2
Old message 3
Old message 4
Old message 5
Message sent while stopped 2
Message sent while stopped 1
The new messages appear to be getting loaded in the order described for when firstEventListenerCalled == true, but it is made false at the beginning of the initRecyclerView() method every time it is called. They should not be getting loaded separately at all since I removed the listeners and totally recreated the RecyclerView. What am I missing?
It appears that the disk persistance that is enabled by default for Android was responsible for this behavior. Turning it off got me the result I wanted.
There's an example provided in NetworkBoundResource, but when I tried it out, if database returns a result, it does not fetch from network anymore. What I want is display data from database and trigger network at the same time and eventually replace data in database after network completes. Example codes will be much appreciated.
I would use room database to save your items in a table. Then you use Paging list to observe to that table. The first time you observe to that table, also do a request to network. When you receive the response delete all items from the table and insert the new ones (all this in a repository class, and in a background thread). This last step will update your paging list automatically as your paging list is observing to that room table.
Guess this article could be helpful:
https://proandroiddev.com/the-missing-google-sample-of-android-architecture-components-guide-c7d6e7306b8f
and the GitHub repo for this article:
https://github.com/PhilippeBoisney/GithubArchitectureComponents
The heart of this solution is UserRepository:
public LiveData<User> getUser(String userLogin) {
refreshUser(userLogin); // try to refresh data if possible from Github Api
return userDao.load(userLogin); // return a LiveData directly from the database.
}
// ---
private void refreshUser(final String userLogin) {
executor.execute(() -> {
// Check if user was fetched recently
boolean userExists = (userDao.hasUser(userLogin, getMaxRefreshTime(new Date())) != null);
// If user have to be updated
if (!userExists) {
webservice.getUser(userLogin).enqueue(new Callback<User>() {
#Override
public void onResponse(Call<User> call, Response<User> response) {
Toast.makeText(App.context, "Data refreshed from network !", Toast.LENGTH_LONG).show();
executor.execute(() -> {
User user = response.body();
user.setLastRefresh(new Date());
userDao.save(user);
});
}
#Override
public void onFailure(Call<User> call, Throwable t) { }
});
}
});
}
I am have the follow code:
public synchronized void next(final RoomListQueryResultHandler handler) {
this.setLoading(true);
roomList = new ArrayList<Room>();
this.database.child("members").child(this.mUser.getUid()).child("rooms")
.limitToFirst(this.mLimit)
.startAt(this.currentPage * this.mLimit)
.addListenerForSingleValueEvent(new ValueEventListener() {
#Override
public void onDataChange(DataSnapshot dataSnapshot) {
RoomListQuery.this.setLoading(false);
//mListAdapter.setLoading(false);
if (!dataSnapshot.hasChildren()) {
RoomListQuery.this.currentPage--;
}
for (DataSnapshot ds : dataSnapshot.getChildren()) {
Room room = ds.getValue(Room.class);
//roomList.add(Room.upsert(room));
Room.getRoom(room.getId(), new Room.RoomGetHandler() {
#Override
public void onResult(Room room, customException e) {
if (e != null) {
// Error!
e.printStackTrace();
return;
}
roomList.add(room);
}
});
handler.onResult(roomList, (customException) null);
}
}
#Override
public void onCancelled(DatabaseError databaseError) {
handler.onResult((List) null, new customException(databaseError.toString()));
}
});
}
}
If they are see, I have two Handlers, at first I call a list of "rooms" from Firebase, and then for each one I get the detail in other query.
The problem is that the response is a empty list, since the function not wait for all query details to be executed for the rooms, so the variable roomList always returns empty.
Any idea what I can implement, or what other methodology to use to solve it?
Thank you very much!
Greetings.
Depending on how your application is structured, you might want to change the database design so that there is no need to perform an additional Firebase query for each room retrieved from the first query.
//mListAdapter.setLoading(false);
If you're creating a list view where each row is from the /members/<user_id>/rooms Firebase node, what are the minimum room attributes necessary to display that list? If it's just a few things like room name, photo url, owner, room_id, etc you might be better off duplicating those from the original source. Then clicking one of those rows can trigger the original additional Firebase query you had as part of Room.getRoom(room.getId(), new Room.RoomGetHandler() { ... });, to navigate to a new screen / display a modal with the full room details once retrieved.
Update
To address your comment about requiring the extra data, in that case, as part of the Room class I would include an extra boolean value _loadedDetails set initially to false. So that for rendering a room within the list, when _loadedDetails is currently false just display a loading spinner. That way you can still perform those additional queries and when completed, update the appropriate Room object within roomList based on the index. Something like this:
#Override
public void onDataChange(DataSnapshot dataSnapshot) {
RoomListQuery.this.setLoading(false);
//mListAdapter.setLoading(false);
if (!dataSnapshot.hasChildren()) {
RoomListQuery.this.currentPage--;
}
int i = 0;
for (DataSnapshot ds : dataSnapshot.getChildren()) {
Room room = ds.getValue(Room.class);
roomList.add(room); // here instead
updateRoom(room, i);
i++;
}
handler.onResult(roomList, (customException) null);
}
...
// outside of the ValueEventListener
public void updateRoom(room, index) {
Room.getRoom(room.getId(), new Room.RoomGetHandler() {
#Override
public void onResult(Room room, customException e) {
if (e != null) {
// Error!
e.printStackTrace();
return;
}
room._loadedDetails = true; // make that publicly accessible boolean, or include a setter method instead
roomList.set(index, room);
}
});
}