Recycler View using Firestore goes to top on data changed - android

I have used a recyclerView with Firestore Database.
Whenever I press the like button on the feed post, the recycler view tends to go to the first post. The clicking on the like button involves 1 write and 1 update operation on a FeedPost object.
#Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
// Inflate the layout for this fragment
View fragView = inflater.inflate(R.layout.fragment_feed, container, false);
db = FirebaseFirestore.getInstance();
feedRecyclerView = (RecyclerView)fragView.findViewById(R.id.feed_recycler_view);
setUpFeedRecyclerView();
return fragView;
}
private void setUpFeedRecyclerView() {
feedRecyclerView.setLayoutManager(new LinearLayoutManager(getContext()));
getFeed();
}
private void getFeed() {
db.collection("XYZ")
.orderBy("time_posted", Query.Direction.DESCENDING).limit(100)
.addSnapshotListener(new EventListener<QuerySnapshot>() {
#Override
public void onEvent(QuerySnapshot snapshots, FirebaseFirestoreException e) {
if (e != null) {
return;
}
List<FeedPost> feedList = new ArrayList<>();
for (QueryDocumentSnapshot doc : snapshots) {
feedList.add(doc.toObject(FeedPost.class));
}
feedAdapter = new FeedAdapter(feedList);
feedRecyclerView.setAdapter(feedAdapter);
feedAdapter.notifyDataSetChanged();
}
});
}
I want to know how I can prevent the scrolling to the top of the recycler view, whenever the like button is clicked.

Avoid setting the adapter inside the event callback. Set the adapter when you're setting up the view and just update the data inside (and preferably use the correct notifyItemInserted/Removed/etc methods).

The best way to handle this is to use FirestoreRecyclerAdapter and let it handle all these use cases.
https://github.com/firebase/FirebaseUI-Android/tree/master/firestore#using-the-firestorerecycleradapter

Related

Android ViewModel slow navigation on already loaded data

Im working on loading data from Firebase Realtime Database by ViewModel. In my layout I have progress bar and recyclerview. Things work perfecly when i need to load my data first, when I click on button fragment opens instantly, progress bar is running and when data loads it stop running and recyclerview shows up. But, when i go into that fragment again (Data is already loaded), no progress bar is shown (which is okay), but it takes about second to comming that switch, which is significantly slower than first behaviour that i described.
So, I am wondering what is making it to wait that long in second scenario and how can I override it, so my fragment shows up first and then shows up recyclerview?
I have already tried using viewstub and dummy views but nothing seems to work..
My CategoryFragment
#Override
public void onCreate(#Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
homeViewModel = new ViewModelProvider(requireActivity()).get(MenuViewModel.class);
}
#Override
public View onCreateView(#NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
if(root == null || fragmentState == STATE_STARTUP)
{
binding = FragmentCategoryRecyclerBinding.inflate(inflater, container, false);
root = binding.getRoot();
binding.categoryRecycler.setHasFixedSize(true);
binding.categoryRecycler.setLayoutManager(new LinearLayoutManager(getContext(), RecyclerView.VERTICAL, false));
layoutAnimationController = AnimationUtils.loadLayoutAnimation(getContext(), R.anim.layout_item_fade_scale);
myFoodListAdapter = new MyFoodListAdapter(getContext(), foodModelList, String.valueOf(menuIndex), String.valueOf(categoryIndex));
binding.categoryRecycler.setAdapter(myFoodListAdapter);
}
else
binding = FragmentCategoryRecyclerBinding.bind(root);
return root;
}
#Override
public void onViewCreated(#NonNull View view, #Nullable Bundle savedInstanceState) {
homeViewModel.getMessageError().observe(getViewLifecycleOwner(), s -> Toast.makeText(getContext(), "" + s, Toast.LENGTH_SHORT).show());
homeViewModel.getMenuList(restaurantId).observe(getViewLifecycleOwner(), menuModels -> {
if(fragmentState == STATE_STARTUP || fragmentState == STATE_SEARCHING)
{
binding.categoryRecycler.setLayoutAnimation(layoutAnimationController);
myFoodListAdapter.setFoodModelList(menuModels.get(menuIndex).getCategories().get(categoryIndex).getItems());
binding.progressBar.hide();
binding.categoryRecycler.setVisibility(View.VISIBLE);
fragmentState = STATE_INITIALIZED;
if(foodModelList.isEmpty())
foodModelList = menuModels.get(menuIndex).getCategories().get(categoryIndex).getItems();
myFoodListAdapter.notifyDataSetChanged();
}
});
}
#Override
public void onDestroyView() {
binding = null;
super.onDestroyView();
}
Sorry for the late reply
Use viewmodel or androidviewmodel in onstart instead on create

FirebaseUI Recycler View for Chat 2 Different Holder Not Applied Correctly AFTER Scrolling

I'm making a chat app. There are 2 views same viewholder.
The problem is, when I scroll to the top, the image, name not correctly inserted as suppose.
I'm using Firebase Ui Recycler View for Realtime Database.
firebaseRecyclerAdapter = new FirebaseRecyclerAdapter<AdminMessage, AdminChatViewHolder>(firebaseRecyclerOptions) {
#Override
protected void onBindViewHolder(#NonNull final AdminChatViewHolder adminChatViewHolder, int i, #NonNull AdminMessage adminMessage) {
adminChatViewHolder.getTvMessage().setText(adminMessage.getMessage());
//We retrieve the userUid to get real time name
FirebaseDatabase.getInstance().getReference().child("admin").child(adminMessage.getSenderUid()).addListenerForSingleValueEvent(new ValueEventListener() {
#Override
public void onDataChange(#NonNull DataSnapshot dataSnapshot) {
if (dataSnapshot.exists()) {
Admin admin = dataSnapshot.getValue(Admin.class);
if (admin != null) {
final String nameAdmin = admin.getFullName();
final String profileUrl = admin.getProfileUrl();
//And then display
adminChatViewHolder.getTvName().setText(nameAdmin);
//Check for image if null or not (profileUrl)
if (profileUrl != null) {
Glide.with(getApplicationContext())
.load(profileUrl)
.into(adminChatViewHolder.getImageView());
}
}
}
}
#Override
public void onCancelled(#NonNull DatabaseError databaseError) {
}
});
}
#NonNull
#Override
public AdminChatViewHolder onCreateViewHolder(#NonNull ViewGroup parent, int viewType) {
View view;
if (viewType == VT_SENDER) {
view = LayoutInflater.from(parent.getContext()).inflate(R.layout.item_admin_message_sender, parent, false);
} else {
view = LayoutInflater.from(parent.getContext()).inflate(R.layout.item_admin_message_receiver, parent, false);
}
return new AdminChatViewHolder(view);
}
#Override
public void onDataChanged() {
progressBar.setVisibility(View.GONE);
}
#Override
public int getItemViewType(int position) {
if (getItem(position).getSenderUid().equals(userUid)) {
return VT_SENDER;
}
return VT_RECEIVER;
}
};
Alright, I remember I had the same issue before. I will try to remember all I can and update this answer.
Always empty the image view or hide it when there isn't an image or url fails:
if (profileUrl != null) {
Glide.with(getApplicationContext())
.load(profileUrl)
.into(adminChatViewHolder.getImageView());
}
else
{
//TODO: set an image place holder or hide adminChatViewHolder.getImageView()
}
I like to empty the imageView while waiting for the new one to load
and do the same for all views not just imageView (i.e. textview)
What if admin is null? what happens?
Don't forget to call super.onDataChanged();
#NonNull
#Override
public AdminChatViewHolder onCreateViewHolder(#NonNull ViewGroup parent, int viewType) {
View view;
if (viewType == VT_SENDER) {
View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.item_admin_message_sender, parent, false);
return new AdminChatViewHolder(view);
} else {
View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.item_admin_message_receiver, parent, false);
return new AdminChatViewHolder(view);
}
}
if you are query is right and your retrieving data is correct then you need to need new view every time ti display
I think the problem could be the async query from Firebase data. You put the update view logic in the onDataChange. However, if you scroll very fast to the top, the row is already recycled and reused. In this case, if the onDataChange triggered after the row is recycled, the content could set to the wrong target view.
What you can do is to move the data retrieval logic back to the FirebaseRecyclerOptions by setting a proper query to retrieve the Admin object or use the AdminMessage passed into the onBindViewHolder function.
You can refer to step 12 in this tutorial to see what onBindViewHolder suppose to do.
Your code is pretty straight forword, and I see you are trying to fetch some detail of the UID you provided. I did create a chat app available here and something similar in it. I encountered this issue. The issue is related to the image caching Glide uses. Make use of Glide Cache. Also, you should use placeholder and error while loading images.
Reset your VH initially onbind and make sure you cancel requests with Glide :)
It happens because you attach firebase listener in the onBindViewHolder - when you scroll, new listener is attached every time the method is called, which may result into a corrupted data in items if you will scroll fast, because the old async result may come into a new listener. It is quite common lifecycle error and it appears within lists, recycler views and even fragments and activities directly if the async logic handling is flawed.
To fix this in you case, use a method that was specifically designed for such cases in the RecyclerViewAdapter - onViewRecycled
It should look like this.
Firstly you need to define listener along with a database reference as a separate global variables in the adapter or viewholder class like this
DatabaseRefference ref;
ValueEventListener listener;
then in the onBindViewHolder
#Override
protected void onBindViewHolder(...){
...
listener = = new ValueEventListener() {
#Override
public void onDataChange(#NonNull DataSnapshot dataSnapshot) {
if (dataSnapshot.exists()) {
Admin admin = dataSnapshot.getValue(Admin.class);
if (admin != null) {
final String nameAdmin = admin.getFullName();
final String profileUrl = admin.getProfileUrl();
//And then display
adminChatViewHolder.getTvName().setText(nameAdmin);
//Check for image if null or not (profileUrl)
if (profileUrl != null) {
Glide.with(getApplicationContext())
.load(profileUrl)
.into(adminChatViewHolder.getImageView());
}
}
}
}
#Override
public void onCancelled(#NonNull DatabaseError databaseError) {
}
};
ref = FirebaseDatabase.getInstance().getReference().child("admin").child(adminMessage.getSenderUid());
ref.addListenerForSingleValueEvent(listener);
...
}
and then detach listener on view recycled event
override fun onViewRecycled(holder: StopsViewHolder) {
super.onViewRecycled(holder)
if (ref != null && listener != null)
ref.removeEventListener(listener)
}
It may require some additional work since I haven't tested it in the IDE. Hope it helps.

How to build an Arraylist of objects from toggle OnClickListeners inside RecyclerView Adapter's items

I'm building an Android app of media, and trying to add a Playlist feature to it, the user will be able to create a playlist of his own and modify it.
I'm using a RecyclerView to show the user list of songs which he can choose from.
The problem is I don't understand how to pass the Arraylist of chosen songs from the adapter to the fragment.
I've tried to use the Observer pattern but the don't know how to use that information.
This is my Fragment for creating the playlist:
public class CreatePlaylistFragment extends Fragment implements PlaylistAdapterInterface {
#Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
// Inflate the layout for this fragment
return inflater.inflate(R.layout.fragment_create_playlist, container, false);
}
#Override
public void onViewCreated(#NonNull View view, #Nullable Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
ArrayList<ProgramsData> dataArrayList = ProgramsReceiver.getPrograms();
ArrayList<ProgramsData> sortedList = new ArrayList<>(dataArrayList);
adapter = new CreatePlaylistAdapter(dataArrayList, view.getContext(), this);
adapter.adapterInterface = this;
ivCreatePlaylist.setOnClickListener(v -> {
Toast.makeText(v.getContext(), "Creating Playlist!", Toast.LENGTH_SHORT).show();
new PlaylistsJsonWriter(playlistArrayList,getContext()).execute();
});
}
#Override
public void OnItemClicked(ArrayList<ProgramsData> programs) {
programsToCreate = programs;
String s = etListName.getText().toString();
playlistArrayList.add(new Playlist(s, programsToCreate));
}
}
This is the Recycler Adapter with ViewHolder as inner class:
public class CreatePlaylistAdapter extends RecyclerView.Adapter<CreatePlaylistViewHolder> {
List<ProgramsData> programsDataList;
Context context;
public PlaylistAdapterInterface adapterInterface = null;
public CreatePlaylistAdapter(List<ProgramsData> programsDataList, Context context , PlaylistAdapterInterface adapterInterface) {
this.programsDataList = programsDataList;
this.context = context;
this.adapterInterface = adapterInterface;
}
#NonNull
#Override
public CreatePlaylistViewHolder onCreateViewHolder(#NonNull ViewGroup viewGroup, int i) {
LayoutInflater inflater = LayoutInflater.from(context);
View view = inflater.inflate(R.layout.chose_program_to_playlist_item, viewGroup, false);
return new CreatePlaylistViewHolder(view);
}
#Override
public void onBindViewHolder(#NonNull CreatePlaylistViewHolder holder, int i) {
ProgramsData programsData = programsDataList.get(i);
holder.tvProgramName.setText(programsData.getProgramName());
if (programsData.getStudentName() != null)
holder.tvStudentName.setText(programsData.getStudentName());
else holder.tvLine.setText(""); //if there is no student the line won't be printed
holder.ivProfilePic.setImageResource(programsData.getProfilePic());
holder.programsData = programsData;
// holder.mAdapterInterface = adapterInterface;
adapterInterface.OnItemClicked(holder.programs);
}
#Override
public int getItemCount() {
return programsDataList.size();
}
}
class CreatePlaylistViewHolder extends RecyclerView.ViewHolder {
TextView tvProgramName;
TextView tvStudentName;
TextView tvLine;
CircleImageView ivProfilePic;
ToggleButton tbCheck;
ProgramsData programsData;
ArrayList<ProgramsData> programs;
PlaylistAdapterInterface mAdapterInterface;
public CreatePlaylistViewHolder(#NonNull View itemView) {
super(itemView);
tvProgramName = itemView.findViewById(R.id.tvProgramName);
tvStudentName = itemView.findViewById(R.id.tvStudentName);
ivProfilePic = itemView.findViewById(R.id.ivProfilePic);
tvLine = itemView.findViewById(R.id.tvLine);
tbCheck = itemView.findViewById(R.id.tbCheck);
programs= new ArrayList<>();
tbCheck.setOnClickListener(v -> {
if (tbCheck.isChecked()) {
tbCheck.setBackgroundResource(R.drawable.ic_radio_button_checked);
programs.add(programsData);
} else if (!tbCheck.isChecked()) {
tbCheck.setBackgroundResource(R.drawable.ic_check);
programs.remove(programsData);
}
});
}
}
And this is the interface for the Observer Pattern:
public interface PlaylistAdapterInterface {
void OnItemClicked(ArrayList<ProgramsData> programs);
}
I know it's a lot of code, but I just don't understand how to pass the data from the adapter back to the fragment...
I don't understand exactly what are you trying to do.
The code contains several errors that I'll try to explain.
A clear error that you have made stays in onBindViewholder where you call the listener at the creation of every item instead than after clicking on it.
You have simply add an onClickListener in the viewHolder.getItemView() or in a specific view of the viewholder and then perform the operation you need to do once an item is clicked.
If you set a listener inside onBindViewHolder, you also have a method called
holder.getAdapterPosition() that you can use to understand which item are you clicking on.
The viewholder should be used only to setup the views accordingly to the data you are binding and nothing else. For this reason, you should not pass any object or listener to it and instead use the approach above.
If you have just to retrieve the selected songs after an user confirms it's playlist you can just add a public method on your adapter
public List<ProgramsData> getSelectedSongs()
that you can call from your fragment when an user click a confirm button.
In order to have a list of all selected song, you can have another list
ArrayList<ProgramsData> selectedPrograms;
that you are going to fill after the click.
The content of the listener inside the onBindViewHolder could be
ProgramsData currentProgram = programs.get(holder.getAdapterPosition());
if(selectedPrograms.contains(currentProgram){
selectedPrograms.remove(currentProgram);
}else{
selectedPrograms.add(currentProgram);
}
notifyItemChanged(holder.getAdapterPosition); //You can use this to update the view of the selected item
Then inside the onBindViewHolderMethod you can check whether the items you are binding are part of the selectedList and update the views accordingly.
You can use callback method. Maintain list of selected items in array list and send back to fragment when done button is clicked or any other button you have placed for complete action.
Follow these steps
-Create an Interface with list parameter.
-Fragment should implement this interface.
-Then when you initialize Recyclerview adapter pass this interface object.
-When done is clicked call overridden method of this interface and send selected songs list as argument.

Implementing recycler view using sugar ORM

i am trying to display data from a textview on an activity onto a recycler view on a fragment. I have an adapter class to implement the adapter methods. however when i click the fragment the app closes and the .count method remains unable to be resolved. the code is attached below;
public class NotesXFragment extends Fragment {
#Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
}
#Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
View noteview = inflater.inflate(R.layout.fragment_notes, container, false);
note_fab.setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View v) {
startActivity(new Intent(getActivity(), AddNote.class));
}
});
RecyclerView recyclerView = (RecyclerView) noteview.findViewById(R.id.note_rv);
recyclerView.setLayoutManager(new LinearLayoutManager(getActivity()));
// notesDbAdapter = new NotesDbAdapter(getActivity(),notes);
// recyclerView.setAdapter(notesDbAdapter);
notecount = Note.count(Note.class);
if(notecount>=0){
notes = Note.listAll(Note.class);
notesDbAdapter = new NotesDbAdapter(getActivity(),notes);
recyclerView.setAdapter(notesDbAdapter);
}
return noteview }
}
Please check if you're correctly importing the Note class. Moreover, you can try to get the count using the SugarRecord class, i find it to work better than using the normal classes.
notecount = SugarRecord.count(Note.class);
Please check if you're using the most recent version of the library.
Additionally, whenever you update your schema, please increment your DB version in your AndroidManifest.xml file.
<meta-data android:name="VERSION" android:value="2" />

private variable inside fragment are all null, why?

I have of null object inside a fragment. The basic idea is that I have an activity that fetches a database asynchronously. However my recyclerview where I will populate the data lives into a fragment. The pseudo-code is more or less
ACTIVITY:
public void onCreate(Bundle savedInstanceState) {
//kicks off a query to the server
mData = new Gson().fromJson(getIntent().getExtras().getString(Constants.MYDATA), MyData.class);
if (mVenue == null) {
finish();
return;
}
// a bunch of stuff
// create a fragment
mMyFrag = new MyFrag();
}
public void CallBackWhenDone(final List<DataSet> dataset) {
// notify the frag that we are done
mMyFrag.notifyDataSetChanged(messages);
}
FRAGMENT:
private RecyclerView mRV;
private ParentActivity mActivity;
private ActivityAsynchData mAsynchData;
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.recycler, container, false);
mRV = (RecyclerView) view.findViewById(R.id.list);
mRV.setLayoutManager(new LinearLayoutManager(getActivity().getApplicationContext(), LinearLayoutManager.VERTICAL, false));
if (null != mActivity) {
mAsynchData = mActivity.GetAsynchData();
}
if (null != mAsynchData) {
mRV.setAdapter(new MyRecyclerAdapter(getActivity(), mAsynchData));
}
}
// mRV is null when the activity "CallBackWhenDone" calls the frag
// all private variables are gone! why?
public void notifyDataSetChanged(final List<Message> messages) {
MyRecyclerAdapter adapter = ((MyRecyclerAdapter) mRV.getAdapter());
adapter.setMessageList(messages);
adapter.notifyDataSetChanged();
}
I ended up hacking my recycler (mRV in this case) view to be static, but this looks super hacked.
any way around? In other words how can I fetch my "mRV" after the fragment has been created and the private vars are all gone.
What i could understand is, that you initialised the fragment and try to access the recycler view in that but its throwing you null. I am not surprised to see it being as null. The way you have called the method is not correct.You need to get the hosted and already running fragment, instance try doing this:
if you are using support fragment use getSupportFragmentManager instead of getFragmentManager.
MyFrag fragment = (MyFrag) getFragmentManager().findFragmentById(R.id.fragmentHolder);
fragment.<specific_function_name>();
Here , the R.id.fragmentHolder is the id of the frame layout or any layout that you are using to host your fragment inside the an activity.
<FrameLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="#+id/fragmentHolder"
android:layout_width="match_parent"
android:layout_height="match_parent"></FrameLayout>

Categories

Resources