RecyclerView row flashes/blinks when item inserted - java

Any time I insert an item into my Room database, my recycler view flashes the row containing the newly inserted view holder (the rest of the list does not flash). I am using LiveData to keep my list automatically updated by calling submitList(list) in onChanged() of the observed ViewModel method. My adapter extends ListAdapter and I am using DiffUtil to track changes in the list. That being said, I don't call notifyItemInserted(position) directly as DiffUtil should do this for me. There are 2 instances where an item gets inserted (1) a completely new item gets inserted at the end of the list (2) a deleted item gets reinserted into the list. Both cases the item will insert itself then flash. I have read numerous posts where people suggest disabling animations on the recycler view but this is not an option for me as I rely on animations elsewhere in my code. Any other suggestions would be appreciated. I'm trying to keep the posted code brief but I can post more if it would be helpful.
MainActivity.java
public class MainActivity extends AppCompatActivity implements View.OnClickListener {
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
initComponents();
initRecyclerView();
setListeners();
setListObserver();
createItemTouchHelper();
}
private void setListObserver() {
viewModel.getAllItems().observe(this, new Observer<List<ListItem>>() {
#Override
public void onChanged(List<ListItem> newList) {
adapterMain.submitList(newList);
}
});
}
...
// Inserts a new ListItem when MainActivity's EditText is used
public void onClick(View v) {
if (v.getId() == R.id.img_add_item_main) {
String itemName = String.valueOf(edtAddItem.getText());
if (!itemName.trim().equals("")) { // Insert new list item only if the EditText is not empty
ListItem item = new ListItem();
item.setItemName(itemName);
viewModel.insert(item);
}
...
// SnackBar to allow a user to undo a delete operation
public void showUndoSnackBar(ListItem deletedItem) {
Snackbar undoSnackBar = Snackbar.make(constraintLayout, "Undo deleted Item",
Snackbar.LENGTH_LONG).setAction("UNDO", new View.OnClickListener() {
#Override
public void onClick(View v) {
// Restore deleted item to its original position in the list if UNDO is clicked
viewModel.insert(deletedItem);
}
});
undoSnackBar.show();
}
RecyclerAdapterMain.java
public class RecyclerAdapterMain extends ListAdapter<ListItem, RecyclerAdapterMain.ListItemHolder> {
public RecyclerAdapterMain() {
super(DIFF_CALLBACK);
}
private static final DiffUtil.ItemCallback<ListItem> DIFF_CALLBACK = new DiffUtil.ItemCallback<ListItem>() {
#Override
public boolean areItemsTheSame(#NonNull ListItem oldItem, #NonNull ListItem newItem) {
return oldItem.getId() == newItem.getId();
}
#Override
public boolean areContentsTheSame(#NonNull ListItem oldItem, #NonNull ListItem newItem) {
return oldItem.equals(newItem);
}
#Override
public ListItemHolder onCreateViewHolder(#NonNull ViewGroup parent, int viewType) {
View itemView = LayoutInflater.from(parent.getContext())
.inflate(R.layout.recycler_item_layout_main, parent, false);
return new ListItemHolder(itemView);
}
#Override
public void onBindViewHolder(#NonNull ListItemHolder holder, int position) {
ListItem item = getItem(position);
holder.txtItemName.setText(item.getItemName());
holder.checkBox.setChecked(item.getIsChecked());
if(item.getIsChecked()) {
holder.txtItemName.setTextColor(Color.LTGRAY);
} else {
holder.txtItemName.setTextColor(Color.BLACK);
}
}
...
ListItem.java (POJO)
#Entity(tableName = "list_item_table")
public class ListItem {
#PrimaryKey(autoGenerate = true)
private long id;
private String itemName;
private boolean isChecked;
public long getId() {
return id;
}
public void setId(long id) {
this.id = id;
}
... other getters and setters
public boolean equals(#Nullable ListItem listItem) {
return this.itemName.equals(listItem.getItemName()) && this.isChecked == listItem.getIsChecked();
}
}

I determined that the DefaultItemAnimator class (which is the animator for RecyclerView at the time of this writing) has a method called animateAdd(final RecyclerView.ViewHolder holder) that sets the holder's alpha to 0 then animates it to 1 over time. I verified that this was the cause of the blinking by changing the default setting to 1. I solved the problem by a using a combination of the accepted answer from this StackOverflow post and the documentation for DefaultItemAnimator.
First, I created a new animator class that extends DefaultItemAnimator in order to override the animateAdd() method. Under the section for animateAdd(), the documentation states, "Called when an item is added to the RecyclerView. Implementors can choose whether and how to animate that change, but must always call dispatchAddFinished(RecyclerView.ViewHolder) when done, either immediately (if no animation will occur) or after the animation actually finishes." I called dispatchAddFinished() immediately to avoid the add animation. Instead of disabling animations altogether, all animations are present except for the problematic one.
MyRecyclerViewAnimator.java
public class MyRecyclerViewAnimator extends DefaultItemAnimator {
#Override
public boolean animateAdd(RecyclerView.ViewHolder holder) {
dispatchAddFinished(holder); // this is what bypasses the animation
return true;
}
/* this is the default implementation of animateAdd() in DefaultItemAnimator
#Override
public boolean animateAdd(final RecyclerView.ViewHolder holder) {
resetAnimation(holder);
holder.itemView.setAlpha(0); // this is what caused the flashing/blinking
mPendingAdditions.add(holder);
return true;
}
*/
}
MainActivity.java
...
recyclerMain.setItemAnimator(new MyRecyclerViewAnimator()); // set the default animator to your extended class
...

Related

Adding new button and spinner for each button click

In the activity when user click on add client button I want to add new view to the screen which contains a spinner with list of client names retrieved from api and a button that will do some action on click.
So I thought I would use a recycleview and adapter for this but I think I'm wrong
in the activity I have the adapter
private ClientAdapter clientAdapter;
When I retrieve clients name from API I set the adapter as
clientRecyclerView.setLayoutManager(new LinearLayoutManager(getApplicationContext()));
clientAdapter= new clientAdapter(clientList , this , this);
clientRecyclerView.setAdapter(podAdapter);
At this point I don't want the recycle view to render anything until user click on add new client button then I want to display one item that has spinner with client names and a button.
Then if he clicks again on add client button I want to show another spinner and button and so on.
However now I'm having 3 clients so recycleview render 3 view items which make sense.
But what the trick that I should do to achieve my goal?
Here's my adapter
public class ClintsAdapter extends RecyclerView.Adapter<ClintsAdapter.ViewHolder> {
private List<Clients> clientsList;
private EventListener listener;
public ClintsAdapter(List<Clients> clientsList, EventListener listener , Context context) {
this.clientsList = clientsList;
this.EventListener = listener;
}
#NonNull
#Override // To inflate view
public ClintsAdapter.ViewHolder onCreateViewHolder(#NonNull ViewGroup parent, int viewType) {
View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.listitem_client, parent, false);
ViewHolder viewHolder = new ViewHolder(view, listener);
return viewHolder;
}
#Override
public void onBindViewHolder(#NonNull ClintsAdapter.ViewHolder holder, int position) {
ClintsAdapter = new ArrayAdapter<Client>(context, R.layout.spinner_text_view, clientsList);
ClintsAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
holder.clientSpinner.setAdapter(ClintsAdapter);
holder.clientSpinner.setTitle("Choose client");
}
#Override
public int getItemCount() {
if (clientsList == null)
return 0;
return clientsList.size();
}
public interface PODListener {
void onClick(int position);
}
public class ViewHolder extends RecyclerView.ViewHolder implements View.OnClickListener {
private SearchableSpinner clientSpinner , collectMethodSpinner;
EventListener listener;
public ViewHolder(View itemView, final EventListener listener) {
super(itemView);
this.listener = podListener;
clientSpinner = itemView.findViewById(R.id.spinner_client);
btnComment = itemView.findViewById(R.id.btn_comment);
btnComment.setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View v) {
if ( listener != null ) {
int position = getAdapterPosition();
if (position != RecyclerView.NO_POSITION){
listener.onClick(position);
}
}
}
});
}
#Override
public void onClick(View v) {
}
}
}
and here's my list item
From comments:
The problem is I'm passing list of clients to the adapter (size of 3) then the adapter render 3 items. I don't want this behavior I want to have 0 item if user click on add I will render one item and so on
You are using a single ArrayList<Client> for two different purposes:
The list of clients to choose from in the spinner
The number of spinners to display in the RecyclerView.
These are two separate things, so you need two separate lists.
You can do that with just adding integer value for your ClientsAdapter. Set its default value as 0 and create a method for changing it's value. When you want to add new item (new Spinner and Button) use that method and notify your adapter.
Add a new field called count for your ClientsAdapter.
private int count;
Inside constructor assign its value to 0. So on start its value will be 0 and RecyclerView will show nothing.
public ClintsAdapter(List<Clients> clientsList, EventListener listener , Context context){
this.clientsList = clientsList;
this.EventListener = listener;
count = 0;
}
Change getItemCount method's return value. According to your code getItemCount returns size of your List. That List is for Spinner and has no relation with this method. Instead of returning your List's size return count.
#Override
public int getItemCount() {
return count;
}
Create a method for changing count's value. count starts with 0 (assigned it 0 inside constructor) and when you click Button (add new Spinner and Button) this method will change its value.
public void addItem(int count) {
this.count = count;
}
Whenever you click Button simply call addItem method and pass new count value and notify your clientAdapter.
addClient.setOnClickListener(v -> {
int count = clientRecyclerView.getChildCount();
clientAdapter.addItem(count+1);
clientAdapter.notifyItemInserted(count);
});
NOTE: I don't get it why you're setting podAdapter for RecyclerView.
clientRecyclerView.setLayoutManager(new LinearLayoutManager(getApplicationContext()));
clientAdapter= new clientAdapter(clientList , this , this);
clientRecyclerView.setAdapter(podAdapter);
You're creating clientAdapter reference for your ClientsAdapter but while setting adapter for RecyclerView, you're using different reference (podAdapter).
Full code for ClientsAdapter:
public class ClintsAdapter extends RecyclerView.Adapter<ClintsAdapter.ViewHolder> {
private List<Clients> clientsList;
private EventListener listener;
private int count;
public ClintsAdapter(List<Clients> clientsList, EventListener listener , Context context) {
this.clientsList = clientsList;
this.EventListener = listener;
count = 0;
}
#NonNull
#Override // To inflate view
public ClintsAdapter.ViewHolder onCreateViewHolder(#NonNull ViewGroup parent, int viewType) {
View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.listitem_client, parent, false);
ViewHolder viewHolder = new ViewHolder(view, listener);
return viewHolder;
}
#Override
public void onBindViewHolder(#NonNull ClintsAdapter.ViewHolder holder, int position) {
ClintsAdapter = new ArrayAdapter<Client>(context, R.layout.spinner_text_view, clientsList);
ClintsAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
holder.clientSpinner.setAdapter(ClintsAdapter);
holder.clientSpinner.setTitle("Choose client");
}
public void addItem(int count) {
this.count = count;
}
#Override
public int getItemCount() {
return count;
}
public interface PODListener {
void onClick(int position);
}
public class ViewHolder extends RecyclerView.ViewHolder implements View.OnClickListener {
private SearchableSpinner clientSpinner , collectMethodSpinner;
EventListener listener;
public ViewHolder(View itemView, final EventListener listener) {
super(itemView);
this.listener = podListener;
clientSpinner = itemView.findViewById(R.id.spinner_client);
btnComment = itemView.findViewById(R.id.btn_comment);
btnComment.setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View v) {
if ( listener != null ) {
int position = getAdapterPosition();
if (position != RecyclerView.NO_POSITION){
listener.onClick(position);
}
}
}
});
}
#Override
public void onClick(View v) {
}
}
}

How to store checkbox statuses after I exit app?

I have tab layout (3 fragments) with recyclerview and checkboxes for every fragment. I set up onClickListener, but after I exit my app, checkbox statuses are reseted. I tried some solutions I found online some solutions (about shared preferences) but neither work for me. Maybe I implement it wrong. Can you help me ?
Also if need, I can share with you my Fragment file.
recyclerview java:
public class RecyclerViewAdapter extends RecyclerView.Adapter <RecyclerViewAdapter.MyViewHolder>{
Context mContext;
List<RecTab1> mData;
public RecyclerViewAdapter(Context mContext, List<RecTab1> mData) {
this.mContext = mContext;
this.mData = mData;
}
#NonNull
#Override
public MyViewHolder onCreateViewHolder(#NonNull ViewGroup parent, int viewType) {
View v ;
v = LayoutInflater.from(mContext).inflate(R.layout.item_tab1,parent,false);
MyViewHolder vHolder = new MyViewHolder(v);
return vHolder;
}
#Override
public void onBindViewHolder(#NonNull final MyViewHolder holder, int position) {
holder.tv_name.setText(mData.get(position).getName());
holder.tv_subName.setText(mData.get(position).getSubName());
holder.cb_checkbox.setChecked(mData.get(position).getSelected());
holder.cb_checkbox.setTag(position);
holder.cb_checkbox.setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View v) {
Integer pos = (Integer) holder.cb_checkbox.getTag();
Toast.makeText(mContext, mData.get(pos).getName() + " clicked!", Toast.LENGTH_LONG).show();
if (mData.get(pos).getSelected()) {
mData.get(pos).setSelected(false);
} else {
mData.get(pos).setSelected(true);
}
}
});
}
#Override
public int getItemCount() {
return mData.size();
}
public static class MyViewHolder extends RecyclerView.ViewHolder{
private TextView tv_name;
private TextView tv_subName;
private CheckBox cb_checkbox;
public MyViewHolder (View itemView){
super (itemView);
tv_name = (TextView) itemView.findViewById(R.id.Item1Name);
tv_subName = (TextView) itemView.findViewById(R.id.Item1SubName);
cb_checkbox = (CheckBox) itemView.findViewById(R.id.CheckboxID);
}
}
}
item java:
public class RecTab1 {
private String Name;
private String SubName;
private boolean isSelected;
public RecTab1(){
}
public RecTab1 (String name, String subName){
Name = name;
SubName = subName;
}
public String getName() {
return Name;
}
public String getSubName() {
return SubName;
}
public void setName(String name) {
Name = name;
}
public void setSubName(String subName) {
SubName = subName;
}
public boolean getSelected() {
return isSelected;
}
public void setSelected(boolean selected) {
isSelected = selected;
}
}
There are basically 2 kinds of persistence:
Temporary persistence (Scope: can be only used inside Activity/Fragment)
Long-term persistence (Scope: goes beyond the lifecycle of Activity/Fragment)
I think, you implemented first technique by creating Rect1 object somewhere(I assumed you did not save them in the database as I did not see those lines of code) and later trying to access the values when needed elsewhere. But, those variables will be destroyed and recreated later on destroying and recreating of the fragment when you would switch between tabs in your TabLayout.
What you need is the second one.
There are many versions of this kind of persistence as :
- SharedPreferences
- Sqlite Database
- Online Database such as Firebase
For eg: In your case, if you have few numbers of tabs which again has few numbers of information to persist then, you can use SharedPreferences. If there is larger number of tabs and in turn larger number of information to persist then, using Sqlite Database is better. See this link about using Room to save data in Sqlite
Now using SharedPreferences, you can save the checked state of the CheckBox into the preference file in the onCheckChangedListener method as:
public MyViewHolder onCreateViewHolder(#NonNull ViewGroup parent, int viewType) {
View v = LayoutInflater.from(mContext).inflate(R.layout.item_tab1, parent, false);
Checkbox cb_checkbox = v.findViewById(R.id.CheckboxID);
cb_checkbox.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() {
#Override
public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
mContext.getSharedPreferences("com.example.yourapp.SAVED_STATE", Context.MODE_PRIVATE)
.edit()
.putBoolean("cb_checkbox", isChecked)
.apply();
}
});
I set the onCheckChangedListener in the onCreateViewHolder because mContext was available. You could have also done in inside the MyViewHolder class passing the Context object in the constructor of MyViewHolder.It is NOT recommended to set listener in onBindViewHolder() because it is called multiple times during binding so, there would be recurrent setting of listener even for those cb_checkbox in the MyViewHolder object whose listeners are already set.
Now, to retrieve the checked state of the cb_checkbox, we will see in our SharedPreferences to find information if it is saved previously as :
#Override
public void onBindViewHolder(#NonNull RecyclerView.ViewHolder holder, int position) {
// your other binding code
// code for binding previous state of the checkbox if is saved otherwise false as a default
boolean checkedState = mContext.getSharedPreferences("com.example.yourapp.SAVED_STATE", MODE_PRIVATE)
.getBoolean("cb_checkbox", false);
holder.cb_checkbox.setChecked(checkedState);
}

Removing an item from a nested RecyclerView

I've been duelling with this problem for a good few hours now. I have a nested RecyclerView (i.e. a RecyclerView that encompasses an inner Recycler view). Both the parent and child recycler view's are dynamic. The problem I encounter is that I cannot find a way to correctly notify the child (inner) recycler view when a CRUD, in particular a delete, occurs. At first it works ok, but then I get all sorts of random errors from "You must be a direct descend view" or getAdapterPosition returning -1 or just simply incorrect positions. I think my implementation is pretty standard so I ask what is the correct way to notify the inner recycler view.
I am pretty close to returning to my former implementation which involved an array of fragments each containing a recycling view, but I question about the performance of such design. My code is as follows:
Parent RecyclerView
public class RecipeRecyclerAdapter extends RecyclerView.Adapter<RecipeRecyclerAdapter.ViewHolder>
{
public interface OnRecipeRecyclerListener
{
//--------------------------- Proxy methods for OnDishRecyclerListener -----------------
void renameDish(int DishPosition, int RecipePosition);
void deleteDish(int DishPosition, int RecipePosition);
//--------------------------- OnRecipeRecyclerListener methods ----------------------------
void deleteRecipe(int RecipePosition);
void renameRecipe(int RecipePosition);
}
//Recycler Pool and tools
private RecyclerView.RecycledViewPool viewPool = new RecyclerView.RecycledViewPool();
//Recycler Parameters
private ArrayList<Recipe> allRecipes;
private Context context;
//Listener
#Setter
private OnRecipeRecyclerListener onRecipeRecyclerListener;
public RecipeRecyclerAdapter(Context context, ArrayList<Recipe> allRecipes)
{
this.allRecipes = allRecipes;
this.context = context;
}
#NonNull
#Override
public ViewHolder onCreateViewHolder(#NonNull ViewGroup parent, int viewType)
{
View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.card_Recipe, parent, false);
return new RecipeRecyclerAdapter.ViewHolder(view, onRecipeRecyclerListener, context);
}
#Override
public void onBindViewHolder(#NonNull ViewHolder holder, int position)
{
Recipe Recipe = allRecipes.get(position);
holder.RecipeName.setText(Utils.colourFirstLetter(context, Recipe.getRecipeName(), R.color.progressFxBar));
holder.RecipeDate.setText(Utils.getDate(Recipe.getTimestamp()));
// Create layout manager with initial prefetch item count
LinearLayoutManager layoutManager = new LinearLayoutManager(
holder.DishsRecycler.getContext(),
LinearLayoutManager.VERTICAL,
false
);
layoutManager.setInitialPrefetchItemCount(Recipe.getDishs().size());
DishRecyclerAdapter DishsRecyclerAdapter = new DishRecyclerAdapter(Recipe.getDishs(), holder, context);
holder.DishsRecycler.setLayoutManager(layoutManager);
holder.DishsRecycler.setAdapter(DishsRecyclerAdapter);
holder.DishsRecycler.setRecycledViewPool(viewPool);
}
#Override
public int getItemCount()
{
return allRecipes.size();
}
static class ViewHolder extends RecyclerView.ViewHolder implements DishRecyclerAdapter.OnDishRecyclerListener
private OnRecipeRecyclerListener onRecipeRecyclerListener;
private Context context;
TextView RecipeName, RecipeDate;
ImageView addDish;
//The Dishs Recycler
RecyclerView DishsRecycler;
public ViewHolder(#NonNull View itemView, OnRecipeRecyclerListener onRecipeRecyclerListener, Context context)
{
super(itemView);
this.onRecipeRecyclerListener = onRecipeRecyclerListener;
this.context = context;
RecipeName = itemView.findViewById(R.id.RecipeName);
RecipeDate = itemView.findViewById(R.id.RecipeDate);
addDish = itemView.findViewById(R.id.addDish);
DishsRecycler = itemView.findViewById(R.id.DishsRecyclerView);
loadListeners(itemView);
}
private void loadListeners(#NonNull View initView)
{
RecipeName.setOnClickListener(v ->
{
PopupMenu popup = new PopupMenu(context, v);
MenuInflater inflater = popup.getMenuInflater();
inflater.inflate(R.menu.Recipe_floating_menu, popup.getMenu());
popup.show();
popup.setOnMenuItemClickListener(item ->
{
switch (item.getItemId())
{
case R.id.menuDeleteRecipe:
onRecipeRecyclerListener.deleteRecipe(getAdapterPosition());
return true;
case R.id.menuRenameRecipe:
onRecipeRecyclerListener.renameRecipe(getAdapterPosition());
return true;
case R.id.menuRecipeProps:
onRecipeRecyclerListener.RecipeProps(getAdapterPosition());
return true;
default:
return false;
}
});
});
addDish.setOnClickListener(v ->
{
onRecipeRecyclerListener.addDish(getAdapterPosition());
});
}
//******************************* OnDishRecyclerListener *******************************
#Override
public void renameDish(int position)
{
onRecipeRecyclerListener.renameDish(position, getAdapterPosition());
}
#Override
public void deleteDish(int position)
{
onRecipeRecyclerListener.deleteDish(position, getAdapterPosition());
}
}
}
Child (inner) RecyclerView
public class DishRecyclerAdapter extends RecyclerView.Adapter<DishRecyclerAdapter.ViewHolder>
{
public interface OnDishRecyclerListener
{
void renameDish(int position);
void deleteDish(int position);
}
private OnDishRecyclerListener onDishRecyclerListener;
private ArrayList<Dish> allDishs;
private Context context;
public DishRecyclerAdapter(ArrayList<Dish> allDishs, OnDishRecyclerListener onDishRecyclerListener, Context context)
{
this.onDishRecyclerListener = onDishRecyclerListener;
this.allDishs = allDishs;
this.context = context;
}
#NonNull
#Override
public ViewHolder onCreateViewHolder(#NonNull ViewGroup parent, int viewType)
{
View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.card_Dishs, parent, false);
return new ViewHolder(context, view, onDishRecyclerListener);
}
#Override
public void onBindViewHolder(#NonNull ViewHolder holder, int position)
{
Dish Dish = allDishs.get(position);
holder.DishName.setText(Dish.getDishName());
}
#Override
public int getItemCount()
{
return allDishs.size();
}
public class ViewHolder extends RecyclerView.ViewHolder
{
private Context context;
TextView DishName; //plus a bunch of other Views I just removed for the sake of simplicity
OnDishRecyclerListener onDishRecyclerListener;
public ViewHolder(Context context, #NonNull View itemView, OnDishRecyclerListener onDishRecyclerListener)
{
super(itemView);
this.context = context;
DishName = itemView.findViewById(R.id.DishName);
this.onDishRecyclerListener = onDishRecyclerListener;
loadListeners(itemView);
}
private void loadListeners(#NonNull View v)
{
//Rename an Dish
DishName.setOnClickListener(view ->
{
PopupMenu popup = new PopupMenu(context, v);
MenuInflater inflater = popup.getMenuInflater();
inflater.inflate(R.menu.Dish_floating_menu, popup.getMenu());
popup.show();
popup.setOnMenuItemClickListener(item ->
{
switch (item.getItemId())
{
case R.id.menuDeleteDish:
onDishRecyclerListener.deleteDish(getAdapterPosition());
return true;
case R.id.menuRenameDish:
onDishRecyclerListener.renameDish(getAdapterPosition());
return true;
case R.id.menuDishProps:
return true;
default:
return false;
}
});
});
}
}
}
An extraction of the fragment calling the parent recycler view:
#Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState)
{
View view = inflater.inflate(R.layout.fragment_Recipe_panel, container, false);
recyclerRecipe = view.findViewById(R.id.RecipeRecyclerView);
SimpleItemAnimator simpleItemAnimator = (SimpleItemAnimator) recyclerRecipe.getItemAnimator();
if(simpleItemAnimator !=null)
{
simpleItemAnimator.setSupportsChangeAnimations(true);
}
RecipeAdapter = new RecipeRecyclerAdapter(getContext(), allRecipes);
RecipeAdapter.setOnRecipeRecyclerListener(this);
//recyclerRecipe.setHasFixedSize(true);
recyclerRecipe.setLayoutManager(new LinearLayoutManager(getContext()));
recyclerRecipe.setAdapter(RecipeAdapter);
return view;
}
public void createRecipe(String RecipeName)
{
Recipe Recipe = new Recipe(RecipeName, getContext());
allRecipes.add(0,Recipe);
RecipeAdapter.notifyItemInserted(0);
}
#Override
public void deleteRecipe(int RecipePosition)
{
allRecipes.remove(RecipePosition);
RecipeAdapter.notifyItemRemoved(RecipePosition);
}
#Override
public void addDish(int RecipePosition)
{
allRecipes.get(RecipePosition).getDishs().add(new Dish(DishName));
RecipeAdapter.notifyItemChanged(RecipePosition);
}
#Override
public void deleteDish(int DishPosition, int RecipePosition)
{
Recipe Recipe = allRecipes.get(RecipePosition);
Dish Dish = Recipe.getDishs().get(DishPosition);
Dish.getTimer().destroyTimer();
Recipe.getDishs().remove(DishPosition);
RecipeAdapter.notifyItemChanged(RecipePosition);
}
I figured out what the problem was (after LOADS OF HOURS). I needed to notify first the parent recycler and then the child recycler in that order.
//adding an item to the inner list
recipeAdapter.notifyItemChanged(recipePosition);
dishsRecycler.getAdapter().notifyItemInserted(recipe.getDishs().size()-1);
//deleting an inner list item
recipeAdapter.notifyItemChanged(recipePosition);
dishsRecycler.getAdapter().notifyItemRemoved()
However the biggest culprit was having a common recyclerPool for all the inner recyclerviews, so removed this line from the code
//REMOVED THESE LINES
private RecyclerView.RecycledViewPool viewPool = new RecyclerView.RecycledViewPool();
holder.DishsRecycler.setRecycledViewPool(viewPool);
Also, I refrained from using notifyDataSet() as that for some reason throws NO_POSITION (-1).
I'm implementing a similar case.
I have 2 RecyclerViews, one nested. Where you can delete items either from nested or parent RecyclerView.
It guess you must update Recyclers every time an item changed or removed.
For comprehension I read this article first:
https://medium.com/android-news/recyclerview-optimisations-a4b141dd433d
And I agree answer by Ken John, when he said you need to notify RecyclerView updates first to parent then to nested; otherwise you get an error and your app will crash.
However, other important thing is how to do the notification updates.
For the nested RecyclerView, I used
// for items updated
notifyItemChanged(position);
// for items deleted
notifyItemRemoved(position);
but the mentioned above not working fine for parent RecyclerView, really I'm not sure why, but I solved as follow:
// for items updated
notifyItemChanged(position);
// for items deleted
notifyItemRemoved(position); // this line does not work for me
notifyDataSetChanged(); // it works fine
The last instruction spend a more bit of time, but works fine.
Note: I don't know yet why notifyItemRemoved(position) doesn't work for parent, and I have call notifyDataSetChanged()

Items in Recyclerview + Listadapter won't redraw on update

I'm setting up a RecyclerView that uses a ListAdapter to calculate animations on changes. The RecyclerView is receiving data through a ViewModel that fetches a list via Firebase. The items shown are of the Mensa kind. A Mensa item can change its visibility, its occupancy, or the distance displayed.
I want to implement two buttons that favorite/hide items, therefore changing their position in the list. Two buttons in every item allow the user to favorite or hide an item. This will move an item to the top / to the bottom of a list, in accordance with the sorting strategy, which places favorites first, defaults second, and hiddens last.
However, when I click on a button, the list will rearrange, but the item clicked won't rebind. Buttons retain their old state (and OnClickListeners), and only scrolling the list will call the onBind method. Is my problem with the DiffUtil.Callback? I really don't know what is wrong with my code.
I am already providing a new list in the submitList method of the adapter (this suggestion from another stackoverflow question enabled animations in my case), but the clicked item still won't redraw.
in MensaListActivity.java
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_mensa_list);
viewModel = ViewModelProviders.of(this).get(MensaListModel.class);
final RecyclerView recyclerView =findViewById(R.id.mensa_list_recyclerview);
final MensaListAdapter adapter = new MensaListAdapter(this, new MensaListAdapter.ItemButtonsListener() {
#Override
public void visibilityButtonClicked(Mensa mensa, VisibilityPreference newVisibility) {
viewModel.visibilityChanged(mensa, newVisibility);
}
});
recyclerView.setAdapter(adapter);
recyclerView.setHasFixedSize(false);
recyclerView.setLayoutManager(new GridLayoutManager(this, 1, RecyclerView.VERTICAL, false));
viewModel.getMensaData().observe(this, new Observer<LinkedList<Mensa>>() {
#Override
public void onChanged(LinkedList<Mensa> mensas) {
adapter.submitList(new LinkedList<>(mensas));
}
});
in MensaListModel.java
public LiveData<LinkedList<Mensa>> getMensaData() {
return mensaData;
}
// ...
public void visibilityChanged(Mensa changedItem, VisibilityPreference newVisibility) {
LinkedList<Mensa> newData = getMensaData().getValue();
int index = newData.indexOf(changedItem);
newData.remove(index);
newData.add(changedItem);
sortMensaData(newData);
// sortMensaData calls postValue method
MensaListAdapter.java
public class MensaListAdapter extends ListAdapter<Mensa, MensaListAdapter.MensaViewHolder> {
private final ItemButtonsListener listener;
private final Context context;
class MensaViewHolder extends RecyclerView.ViewHolder {
TextView nameLabel;
TextView addressLabel;
TextView restaurantTypeLabel;
TextView occupancyLabel;
TextView distanceLabel;
ImageButton favoriteButton;
ImageButton hideButton;
public MensaViewHolder(#NonNull View itemView) {
super(itemView);
// a bunch of assignments
}
public void bindData(final Mensa newMensa) {
nameLabel.setText(newMensa.getName());
addressLabel.setText(newMensa.getAddress());
restaurantTypeLabel.setText(newMensa.getType().toString());
String occText = "Occupancy: " + newMensa.getOccupancy().toInt();
occupancyLabel.setText(occText);
if (newMensa.getDistance() != -1) {
distanceLabel.setVisibility(View.VISIBLE);
distanceLabel.setText(Double.toString(newMensa.getDistance()));
} else {
distanceLabel.setVisibility(View.INVISIBLE);
}
switch(newMensa.getVisibility()){
case FAVORITE:
favoriteButton.setImageDrawable(ResourcesCompat.getDrawable(context.getResources(), R.drawable.favorite_active, null));
favoriteButton.setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View v) {
listener.visibilityButtonClicked(newMensa, VisibilityPreference.DEFAULT);
}
}); break;
case DEFAULT:
favoriteButton.setImageDrawable(ResourcesCompat.getDrawable(context.getResources(), R.drawable.favorite_inactive, null));
favoriteButton.setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View v) {
listener.visibilityButtonClicked(newMensa, VisibilityPreference.FAVORITE);
}
}); break;
case HIDDEN:
favoriteButton.setImageDrawable(ResourcesCompat.getDrawable(context.getResources(), R.drawable.favorite_inactive, null));
favoriteButton.setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View v) {
listener.visibilityButtonClicked(newMensa, VisibilityPreference.FAVORITE);
}
}); break;
// removed hidebutton assignments, as they're identical to the favoritebutton assignment
}
}
}
public MensaListAdapter(Context context, ItemButtonsListener listener) {
super(DIFF_CALLBACK);
this.context = context;
this.listener = listener;
}
private static final DiffUtil.ItemCallback<Mensa> DIFF_CALLBACK =
new DiffUtil.ItemCallback<Mensa>() {
#Override
public boolean areItemsTheSame(#NonNull Mensa oldItem, #NonNull Mensa newItem) {
return oldItem.equals(newItem);
}
#Override
public boolean areContentsTheSame(#NonNull Mensa oldItem, #NonNull Mensa newItem) {
return oldItem.getDistance() == newItem.getDistance()
&& oldItem.getOccupancy().equals(newItem.getOccupancy())
&& oldItem.getVisibility().equals(newItem.getVisibility());
}
};
#Override
public int getItemViewType(int position) {
return R.layout.mensa_list_item;
}
#NonNull
#Override
public MensaViewHolder onCreateViewHolder(#NonNull ViewGroup parent, int viewType) {
final View view = LayoutInflater.from(parent.getContext()).inflate(viewType, parent, false);
return new MensaViewHolder(view);
}
#Override
public void onBindViewHolder(#NonNull MensaViewHolder holder, int position) {
holder.bindData(getItem(position));
}
public interface ItemButtonsListener{
void visibilityButtonClicked(Mensa mensa, VisibilityPreference newVisibility);
}
}
Mensa.java
public class Mensa {
private String uID;
private String name;
private String address;
private Occupancy occupancy;
private RestaurantType type;
private VisibilityPreference visibility;
private double latitude;
private double longitude;
// distance is calculated lazily as soon as location coordinates are available, -1 means not calculated.
private double distance = -1;
public Mensa() {
}
// generated by android studio
#Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Mensa mensa = (Mensa) o;
return uID.equals(mensa.uID);
}
#Override
public int hashCode() {
return Objects.hash(uID);
}
// a bunch of getters and setters
}
The list before clicking the favorite button (heart). Relevant are the heart and eye buttons.
The list after favoriting the "Akademiestraße" item. It has changed positions, but the heart icon has not changed and the OnClickListeners are still the same.
The list after scrolling and returning to the top of the list. The heart is now filled, and OnClickListeners are changed.
It seems to me that your data is updating but the RecyclerView is only updating the order and not the item's view. Try calling your adapter's notifyDataSetChanged() after you update an item in your view.
Remember that your views are being recycled, meaning if you have for example a checkbox that was check in position 0. Scrolling will make that checkbox to be reuse in some item thus you may see other item that was checked as well even though you never check it. Always save the state of your view as it will be recycle. You can use your POJO/model to save the state with a boolean field.
Also when working with DiffUtil make sure it is a different instance of list not reusing the old one because it may not update your data.
You may also want to change this adapter.submitList(new LinkedList<>(mensas)); to just this adapter.submitList(mensas);

Dynamic Spinners - if an item is selected from one spinner, hide it from other spinners - Android

How to hide an item from other spinners that is currently selected in one spinner?
I've tried removing the items via ArrayList of strings and ArrayAdapters, but I've noticed as soon as it's removed from the list, the selection is no longer referenced to the list item (because it does not exist anymore).
Now suppose, I have 4 spinner that are created dynamically and they all have the same ArrayList as their resource and now i would like to use this adapter to fetch the position of the selected item from 1 spinner and then hide it from 3 other spinners.
for (int i = 0; i < numberOfStops; i++) {
AddStopView stopView = new AddStopView(getActivity());
stopView.setCallback(BaseBookingFragment.this);
stopView.setPassengerNames(extraPassengerNames);
stopViews.add(stopView);
parent.addView(stopView, viewPosition);
}
In above code i am creating Stop Views dynamically and each Stop View having Passenger Name spinner. And these all spinners have the same ArrayList as their resource.
piece of code from AddStopView.java
public AddStopView(Context context) {
super(context);
initialize();
}
public void setCallback(StopViewCallback callback) {
this.callback = callback;
}
public void setPassengerNames(List<String> passengerNames) {
this.passengerNames = passengerNames;
passengerAdapter.setNames(passengerNames);
}
private void initialize() {
inflate(getContext(), R.layout.view_stop, this);
passengerAdapter = new ExtraPassengerAdapter(getContext());
passengerAdapter.setNames(passengerNames);
nameSpinner.setAdapter(passengerAdapter);
nameSpinner.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
#Override
public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {
if (view == null) {
return;
}
passengerName = (String) view.getTag();
if (position != 0)
callback.updatePassengerList(AddStopView.this, (position - 1));
}
#Override
public void onNothingSelected(AdapterView<?> parent) {
}
});
}
code of call back of nameSpinner.setOnItemSelectedListener
#Override
public void updatePassengerList(AddStopView addStopView, int position) {
for (String passName : extraPassengerNames) {
if (addStopView.getPassengerName().equals(passName)) {
extraPassengerNames.remove(passName);
break;
}
}
for (AddStopView stopView : stopViews) {
if (!stopView.equals(addStopView))
stopView.setPassengerNames(extraPassengerNames);
}
}
code from ExtraPassengerAdapter.java
public class ExtraPassengerAdapter extends BaseAdapter {
private List<String> names = new ArrayList<>();
private Context context;
public ExtraPassengerAdapter(Context context) {
this.context = context;
names.add(get0Position());
}
public void setNames(List<String> names) {
this.names.clear();
this.names.add(get0Position());
this.names.addAll(names);
notifyDataSetChanged();
}
#Override
public int getCount() {
return names.size();
}
#Override
public String getItem(int position) {
return names.get(position);
}
#Override
public long getItemId(int position) {
return 0;
}
#Override
public View getView(int position, View convertView, ViewGroup parent) {
TextView textView = (TextView) LayoutInflater.from(parent.getContext()).inflate(R.layout.adapter_stop, parent, false);
String name = getItem(position);
textView.setText(name);
textView.setTag(name);
return textView;
}
private String get0Position() {
return context.getString(R.string.passenger_name);
}
}
I think the right way to solve you problem is to create one list with all possible options and 4 lists with 4 adapters for each spinner. When something selected you update each list according to logic you described and call adapter.notifyDataSetChanged() for each adapter.

Categories