RecyclerView notifyDataSetChanged() not working - android

I'm using firebase for chat app and I'm implementing to fetch messages from firebase database and update recycler view.
When I click the "send" button, it acts like below.
button.setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View view) {
final ChatModel.Comment comment = new ChatModel.Comment();
comment.uid = uid;
comment.message = editText.getText().toString();
FirebaseDatabase.getInstance().getReference().child("chatrooms")
.child(chatroomUid).push().setValue(comment).addOnSuccessListener(new OnSuccessListener<Void>() {
#Override
public void onSuccess(Void aVoid) {
FirebaseDatabase.getInstance().getReference().child("chatrooms")
.child(chatroomUid).addValueEventListener(new ValueEventListener() {
#Override
public void onDataChange(#NonNull DataSnapshot dataSnapshot) {
comments.clear();
for(DataSnapshot item : dataSnapshot.getChildren()) {
if(item.getKey().compareTo("users")!=0) {
comments.add(item.getValue(ChatModel.Comment.class));
}
}
adapter.notifyDataSetChanged();
}
#Override
public void onCancelled(#NonNull DatabaseError databaseError) {
}
});
}
});
}
});
Process is like this.
Store message data into firebase database using "Comment" object form.
If stage 1 is succeeded, fetch that message and call notifyDataSetChanged()
But it is not working, I have global adpater object and above method is called by this variable.
Adapter code is like below.
private class messageRecyclerveiwAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> {
#NonNull
#Override
public RecyclerView.ViewHolder onCreateViewHolder(#NonNull ViewGroup viewGroup, int i) {
View view = LayoutInflater.from(viewGroup.getContext()).inflate(R.layout.item_message, viewGroup, false);
return new MessageViewHolder(view);
}
#Override
public void onBindViewHolder(#NonNull RecyclerView.ViewHolder viewHolder, int i) {
((MessageViewHolder)viewHolder).textView.setText(comments.get(i).message);
}
#Override
public int getItemCount() {
return comments.size();
}
}
How can I fix it?

You need to create an object of your Adapter in the same class as you have the setOnClickListener:
Something like this:
final MessageRecyclerViewAdapter adapter = new MessageRecyclerViewAdapter(getActivity(), messageArrayList);
Then you need to pass the adapter to the method you're using to get the messages. Right now, you're calling notifyDataSetChanged() in the adapter which is wrong. First you need a specific method to get those messages from Firebase. For example:
public void getLiveChatMessages(final ArrayList<ChatMessageClass> messageArrayList, final MessageRecyclerViewAdapter adapter) {
I'm guessing you have a class that returns different part of the messages.. That's what I've named "ChatMessageClass" here, but you may call it something else. Within this method you get the messages from Firebase, then afterward you call as the last thing in the method:
adapter.notifyDataSetChanged();}
within the getLiveChatMessages method. Here you also pass in the adapter so that you can use it. This is the way to call the notifyDataSetChanged() within the same method as where you get the messages.
Good luck!

Related

LiveData not refreshing RecyclerView after Firestore update

I have an Android app which uses firestore as its database. I have followed this series of blog posts to set up my firestore database in my app : https://firebase.googleblog.com/2017/12/using-android-architecture-components.html and then followed this stackoverflow entry to change my code to work for firestore: Android Architecture Components with Firebase specifically Firestore.
After this I was successful to display the result of my query in a recycler view, however when I added the swap to update (I do soft delete by setting a isActive flag to false) action in my app, LiveData was inconsistent in refreshing the RecyclerView. Here is my code snippets:
MainActivity.java
TaskViewModel viewModel =
ViewModelProviders.of(this).get(TaskViewModel.class);
LiveData<LinkedList<TaskProperties>> liveData = viewModel.getTaskPropertiesLiveData();
final MainActivity mainActivityReference = this;
liveData.observe(this, new Observer<LinkedList<TaskProperties>>() {
#Override
public void onChanged(#Nullable LinkedList<TaskProperties> taskProperties) {
if (taskProperties != null) {
// Get a handle to the RecyclerView.
mRecyclerView = findViewById(R.id.recyclerview);
// Create an adapter and supply the data to be displayed.
mAdapter = new TaskListAdapter(mainActivityReference, taskProperties);
// Connect the adapter with the RecyclerView.
ItemTouchHelper.Callback callback = new SimpleItemTouchHelperCallback(mAdapter);
ItemTouchHelper touchHelper = new ItemTouchHelper(callback);
touchHelper.attachToRecyclerView(mRecyclerView);
mRecyclerView.setAdapter(mAdapter);
// Give the RecyclerView a default layout manager.
mRecyclerView.setLayoutManager(new LinearLayoutManager(mainActivityReference));
}
}
});
View Model:
public class TaskViewModel extends ViewModel {
private LinkedList<TaskProperties> taskProperties;
private static final Query PROJECT_REF = FirebaseFirestore.getInstance().collection("project").whereEqualTo("active", true);
private final FirebaseQueryLiveData liveData = new FirebaseQueryLiveData(PROJECT_REF);
public TaskViewModel() {
taskPropertiesLiveData.addSource(liveData, new Observer<QuerySnapshot>() {
#Override
public void onChanged(#Nullable final QuerySnapshot querySnapshot) {
if (querySnapshot != null) {
new Thread(new Runnable() {
#Override
public void run() {
taskProperties = new LinkedList<TaskProperties>();
for (DocumentSnapshot document : querySnapshot.getDocuments()) {
taskProperties.addLast(document.toObject(TaskProperties.class));
}
taskPropertiesLiveData.postValue(taskProperties);
}
}).start();
} else {
taskPropertiesLiveData.setValue(null);
}
}
});
}
#NonNull
public LiveData<LinkedList<TaskProperties>> getTaskPropertiesLiveData() {
return taskPropertiesLiveData;
}
}
Code in the callback class to remove :
public void onSwiped(RecyclerView.ViewHolder viewHolder, int direction) {
mAdapter.onItemDismiss(viewHolder.getAdapterPosition());
}
Constructor in Adapter:-
public TaskListAdapter(Context context,LinkedList<TaskProperties> taskList) {
mInflater = LayoutInflater.from(context);
this.taskList = taskList;
}
Code in Adapter to remove:-
public void onItemDismiss(int position) {
TaskDao taskDao = new TaskDao();
taskDao.softDeleteTaskInDB(taskList.get(position));
}
Code in DAO class to update( soft delete) :-
public void softDeleteTaskInDB(TaskProperties taskProperties){
taskProperties.setActive(false);
database.collection("project")
.document(taskProperties.getTask())
.set(taskProperties).
addOnSuccessListener(new OnSuccessListener<Void>() {
#Override
public void onSuccess(Void aVoid) {
Log.d(DEBUG_TAG, "DocumentSnapshot successfully written!");
}
})
.addOnFailureListener(new OnFailureListener() {
#Override
public void onFailure(#NonNull Exception e) {
Log.w(DEBUG_TAG, "Error writing document", e);
}
});
Log.i(DEBUG_TAG,taskProperties.getTask());
}
I have observed that LiveData was able to refresh the view when I was deleting one component from the end of the list, however when I deleted from the middle of the list the view sometimes does not refresh properly. From the logs I found that the position that is being passed into the adapter class is working fine, however the tasklist array does not have the most updated value.
For example if the task list contains :-
Cat
Dog
Mouse
Rabbit
Tiger
and if delete Mouse and then Rabbit in quick succession, the onItemDismiss in adapter class receives position 3 in both cases, but the taskList variable in the Adapter class still contains Mouse at position 3. This means the LiveData might not have refreshed the RecyclerView.
Can someone please tell me where am I going wrong?
Thanks,
Sangho

executing async method in onBindViewHolder

I have a recyclerView which items of recyclerView has a textView sometimes with links in it. I use richLinkPreview for previewing my link inside each item has a link. But when I scroll the recyclerView I face a problem that is every time I view an item with link, it goes to bring link data and show it and sometimes it shows wrong link preview(shows link preview of item A in item B). I think my problem is calling richLinkPreview inside onBindViewHolder method but I have not any option in my knowledge. Could you help me with that?
Try this:
public class Adapter extends RecyclerView.Adapter<Adapter.ViewHolder> {
private final SparseArray<MetaData> mFetchedMetadata = new SparseArray<>();
...
#Override
public void onBindViewHolder(final #NonNull Adapter.ViewHolder holder,
final int position) {
...
final MetaData metadata = mFetchedMetadata.get(position);
if (metadata != null) {
holder.richLinkView.setLinkFromMeta(metadata);
} else {
holder.richLinkView.setLink("url", new ViewListener() {
#Override
public void onSuccess(boolean status) {
mFetchedMetadata.put(position, holder.richLinkView.getMetaData());
}
#Override
public void onError(Exception e) {
}
});
}
...
}
...
}

How to retrieve data from firebase database without listening for events [duplicate]

This question already has answers here:
How to return DataSnapshot value as a result of a method?
(6 answers)
Closed 4 years ago.
Hello I am using Firebase database for my android app.I have my data stored on firebase database and I want to get it whenever the fragment gets created but when I start my app the method(which gets data from firebase database) returns null
and whenever I resume the fragment with the android device the recyclerView gets populated with the data from firebase.
My question is why the list returns null first time. and How can I get data whenever the fragment gets created
OffersFragment
#Nullable
#Override
public View onCreateView(#NonNull LayoutInflater inflater, #Nullable ViewGroup container, #Nullable Bundle savedInstanceState) {
View offersView = inflater.inflate(R.layout.fragment_offers, container, false);
mOffersRecyclerView = offersView.findViewById(R.id.rv_offers);
mAddCard = offersView.findViewById(R.id.fab_add_card);
setUp();
return offersView;
}
#Override
public void setPresenter(OffersContract.Presenter presenter) {
mPresenter = presenter;
}
#Override
public void setUp() {
mOffersList = mPresenter.getOffersList();
mOffersAdapter = new OffersAdapter(getActivity(), mOffersList);
mOffersRecyclerView.setAdapter(mOffersAdapter);
RecyclerView.LayoutManager layoutManager = new LinearLayoutManager(getActivity());
mOffersRecyclerView.setLayoutManager(layoutManager);
mOffersRecyclerView.setHasFixedSize(false);
performActions(mAddCard);
}
OffersPresenter
#Override
public List<Offers> getOffersList() {
return databaseHelper.getOffers();
}
DatabaseHelper -> getOffers()
public List<Offers> getOffers() {
DatabaseReference offerReference = getDatabase().child("offers");
offerReference.addValueEventListener(new ValueEventListener() {
#Override
public void onDataChange(DataSnapshot dataSnapshot) {
final Iterable<DataSnapshot> children = dataSnapshot.getChildren();
for (DataSnapshot d : children) {
offersList.add(d.getValue(Offers.class));
}
}
#Override
public void onCancelled(DatabaseError databaseError) {
}
});
return offersList;
}
The firebase callbacks are async so the method getOffers will return null before it gets updated by callback.
You have to pass your adapter instance and list instance to firebase callback so that it update from there
Try this mechanism:
In your activity initialize your list
#Override
public void setUp() {
mOffersList = new ArrayList<Offers> ();
mOffersAdapter = new OffersAdapter(getActivity(), mOffersList);
mOffersRecyclerView.setAdapter(mOffersAdapter);
RecyclerView.LayoutManager layoutManager = new LinearLayoutManager(getActivity());
mOffersRecyclerView.setLayoutManager(layoutManager);
mOffersRecyclerView.setHasFixedSize(false);
performActions(mAddCard);
}
Update your getter
#Override
public void getOffersList(List<Offers> offers, OffersAdapter adapter) {
return databaseHelper.getOffers(offers, adapter);
}
Helper will be like this:
public void getOffers(List<Offers> offers,OffersAdapter adapter) {
if (offers == null) offers = new ArrayList<Offers> ();
DatabaseReference offerReference = getDatabase().child("offers");
offerReference.addValueEventListener(new ValueEventListener() {
#Override
public void onDataChange(DataSnapshot dataSnapshot) {
final Iterable<DataSnapshot> children = dataSnapshot.getChildren();
for (DataSnapshot d : children) {
offers.add(d.getValue(Offers.class));
}
adapter.notifyDataSetChanged();
}
#Override
public void onCancelled(DatabaseError databaseError) {
}
});
}
Keep in mind you are using value event which will gets called if any data changes and as a result data will be added multiple times. Use single value event or Child event listener to solve the issue if you have any.
It returns null because you are placing your return offersList; at the end of public List<Offers> getOffers() Remove the return statement and make getOffers() void and add logic to Get/Update your data and/or commit() your Fragment inside of onDataChange(DataSnapshot dataSnapshot), so it will return your list when the data is fetched the first time.
What I usually do is to commit the fragment or set its arguments inside of
onDataChange(DataSnapshot dataSnapshot)

FirebaseRecyclerAdapter with 2 different database references - negative impact on scrolling

Simple thing I would like to do (see in the picture)
Display a view with info coming from 2 different places in Firebase so that it behaves in a professional way scrolling UP and DOWN
I have a list of movies and on each of them I would like the user to specify a rating and see it
In DB I created 2 structures to have the list of movies on one side and the ratings per user on the other
Problem using FirebaseRecyclerAdapter
My problem is that scrolling fast up and down the list, the visualization of the information coming from the second reference (the rating) is loaded on a different time (asynchronous call) and this is not acceptable to see this (little) delay building the view. Is this a limitation of FirebaseRecyclerView?
Because viewHolders are reused in the recycleView I reset and reload each time in populateView() the rating values and this doesn't help. Once retrieved I'm oblidged to get them again if the user scroll the view (see the setOnlistener in populateView()
Setting a listener in populateView cause also to have as many listener as the number of times populateView() is executed (if you scroll UP and DOWN it's many times).
Solutions / Workaround ?
Is there a correct way to do it preventing the problem? Or is it a limitation?
What about performance with my implementation where the listener is inside populateView() and there are MANY listener created?
Below some things I'm thinking on:
Prevent viewHolders to be recycled and just load once?
Override some other methods of RecyclerView? I tried with parseSnapshot() but it's the same problem...
Change the DB structure to have all the info in one list (I don't think it's the good one because it means adding rating information of each user to movie list)
Add a loading spinner on the rating part so that the rating is displayed only when the asyncrhonous call to firebase is completed (don't like it) without the today effect of: "changing star color in front of the user".
My Implementation
From FirebaseRecyclerAdapter
#Override
protected void populateViewHolder(final MovieViewHolder viewHolder, final Movie movie, final int position) {
String movieId = this.getRef(position).getKey();
// Oblidged to show no rating at the beginning because otherwise
// if a viewHolder is reused it has the values from another movie
viewHolder.showNoRating();
//---------------------------------------------
// Set values in the viewHolder from the model
//---------------------------------------------
viewHolder.movieTitle.setText(movie.getTitle());
viewHolder.movieDescription.setText(movie.getDescription());
//-----------------------------------------------------
// Ratings info are in another DB location... get them
// but call is asynchronous so PROBLEM when SCROLLING!
//-----------------------------------------------------
DatabaseReference ratingMovieRef = mDbRef.child(Constants.FIREBASE_LOCATION_RATINGS).child(currentUserId).child(movieId);
ratingQuoteRef.addListenerForSingleValueEvent(new ValueEventListener() {
#Override
public void onDataChange(DataSnapshot dataSnapshot) {
RatingMovie ratingMovie = dataSnapshot.getValue(RatingMovie.class);
Rating rating = Rating.NO_RATING;
if (ratingMovie != null) {
rating = Rating.valueOf(ratingMovie.getRating());
}
// Set the rating in the viewholder (through anhelper method)
viewHolder.showActiveRating(rating);
}
#Override
public void onCancelled(DatabaseError databaseError) {
}
});
}
from MovieViewHolder
public class QuoteViewHolder extends RecyclerView.ViewHolder {
public CardView cardView;
public TextView movieTitle;
public TextView movieDescription;
public ImageView ratingOneStar;
public ImageView ratingTwoStar;
public ImageView ratingThreeStar;
public QuoteViewHolder(View itemView) {
super(itemView);
movieTitle = (TextView)itemView.findViewById(R.id.movie_title);
movieDescription = (TextView)itemView.findViewById(R.id.movie_descr);
// rating
ratingOneStar = (ImageView)itemView.findViewById(R.id.rating_one);
ratingTwoStar = (ImageView)itemView.findViewById(R.id.rating_two);
ratingThreeStar = (ImageView)itemView.findViewById(R.id.rating_three);
}
/**
* Helper to show the color on stars depending on rating value
*/
public void showActiveRating(Rating rating){
if (rating.equals(Rating.ONE)) {
// just set the good color on ratingOneStar and the others
...
}
else if (rating.equals(Rating.TWO)) {
// just set the good color
...
} else if (rating.equals(Rating.THREE)) {
// just set the good color
...
}
/**
* Initialize the rating icons to unselected.
* Important because the view holder can be reused and if not initalised values from other moviecan be seen
*/
public void initialiseNoRating(){
ratingOneStar.setColorFilter(ContextCompat.getColor(itemView.getContext(), R.color.light_grey));
ratingTwoStar.setColorFilter(....
ratingThreeStar.SetColorFilter(...
}
You can sort of cache the ratings using a ChildEventListener. Basically just create a separat one just for the Ratings node, and have it store the ratings in a Map. Then using the RecyclerAdapter you will retrieve from the Map if the rating is available, if it is not, have the rating listener update the recyclerview as soon as is has downloaded the rating. This is one strategy you could go about, doing it, you will have to manually copy/paste some classes from the FirebaseUI library and set some fields public for this to work.
Usage would be something like this
private MovieRatingConnection ratingConnection;
// inside onCreate
ratingConnection = new MovieRatingConnection(userId, new MovieRatingConnection.RatingChangeListener() {
#Override
public void onRatingChanged(DataSnapshot dataSnapshot) {
if (recyclerAdapter != null) {
if (dataSnapshot != null) {
int index = recyclerAdapter.snapshots.getIndexForKey(dataSnapshot.getKey());
recyclerAdapter.notifyItemChanged(index);
}
}
}
});
Query movieQuery = FirebaseDatabase.getInstance().getReference().child("Movies");
recyclerAdapter = new FirebaseRecyclerAdapter(movieQuery...) {
#Override
public void populateViewHolder(RecyclerView.ViewHolder viewHolder, Object model, int position) {
//...
final String key = getRef(position).getKey();
viewHolder.showActiveRating(ratingConnection.getRating(key));
}
};
and MovieRatingConnection would be a class like this
public class MovieRatingConnection {
private MovieRatingListener listener;
public MovieRatingConnection(String userId, RatingChangeListener changeListener) {
Query query = FirebaseDatabase.getInstance().getReference().child("MovieRatings").child(userId);
listener = new MovieRatingListener(query, changeListener);
}
public Rating getRating(String key) {
return listener.getRating(key);
}
public void cleanup() {
if (listener != null) {
listener.unregister();
}
}
public static class MovieRatingListener implements ChildEventListener {
public interface RatingChangeListener {
public void onRatingChanged(DataSnapshot snapshot);
}
private Query query;
private HashMap<String, Rating> ratingMap = new HashMap<>();
private RatingChangeListener changeListener;
public MovieRatingListener(Query query, RatingChangeListener changeListener) {
this.query = query;
this.changeListener = changeListener;
query.addChildEventListener(this);
}
#Override
public void onChildAdded(DataSnapshot dataSnapshot, String s) {
if (dataSnapshot != null) {
ratingMap.put(dataSnapshot.getKey(), dataSnapshot.getValue(Rating.class));
changeListener.onRatingChanged(dataSnapshot);
}
}
#Override
public void onChildChanged(DataSnapshot dataSnapshot, String s) {
if (dataSnapshot != null) {
ratingMap.put(dataSnapshot.getKey(), dataSnapshot.getValue(Rating.class));
changeListener.onRatingChanged(dataSnapshot);
}
}
#Override
public void onChildRemoved(DataSnapshot dataSnapshot) {
ratingMap.remove(dataSnapshot.getKey());
changeListener.onRatingChanged(null);
}
#Override
public void onChildMoved(DataSnapshot dataSnapshot, String s) {
}
#Override
public void onCancelled(DatabaseError databaseError) {
}
public Rating getRating(String key) {
if (ratingMap.get(key) != null) {
return ratingMap.get(key);
} else {
return new Rating(); // default value/null object
}
}
public void unregister() {
query.removeEventListener(this);
}
}
}

Dynamically update getChildCount() from Firebase?

I'm trying to update a RecyclerView with information from Firebase. I've successfully been able to update the TextViews in my view from Firebase but, in doing so, my current code won't update the getChildCount() of the RecyclerView with the amount of children from the database without infinitely looping.
My current code: (CardAdapter.java)
public int getChildCount() {
mDatabase = FirebaseDatabase.getInstance();
mReference = mDatabase.getReference();
final String userID = FirebaseAuth.getInstance().getCurrentUser().getUid();
ValueEventListener listener = new ValueEventListener() {
#Override
public void onDataChange(DataSnapshot dataSnapshot) {
for (DataSnapshot snap :
dataSnapshot.child("pet").child("users").child(userID).getChildren()) {
mChildCount = (int) snap.getChildrenCount();
}
notifyDataSetChanged();
}
#Override
public void onCancelled(DatabaseError databaseError) {
}
};
mReference.addListenerForSingleValueEvent(listener);
return mChildCount;
}
This code works, but it infinitely loops due to the notifyDataSetChanged(); method that updates the RecyclerView.
What can I do to make it so the RecyclerView getChildCount() is dynamically updated from Firebase without infinitely looping?
You should not place your code to acquire data inside getChildrenCount(). I myself, usually place that in my Activity or inside constructor of Adapter. Like this:
public class YourAdapter extends RecyclerView.Adapter<YourAdapter.ViewHolder> {
private List<YourObject> yourObjectList;
public YourAdapter() {
ValueEventListener listener = new ValueEventListener() {
#Override
public void onDataChange(DataSnapshot dataSnapshot) {
for (DataSnapshot snap : dataSnapshot.child("pet").child("users").child(userID).getChildren()) {
...
// add to list
yourObjectList.add(snap.getValue(YourObject));
...
}
}
#Override
public void onCancelled(DatabaseError databaseError) {
}
};
mReference.addListenerForSingleValueEvent(listener);
...
}
#Override
public void onBindViewHolder(YourAdapter.ViewHolder holder, int position) {
YourObject yourObject = yourObjectList.get(position);
...
}
#Override
public int getItemCount() {
return yourObjectList.size();
}
}
Hope this help.

Categories

Resources