Recycler view List Adapter updates UI incorrectly - android

I have created a recycler view adapter. Based on the the item in the adapter I am trying to change the UI for that row. When the screen is first loaded the UI is rendered correctly . But once I scroll through, the screen gets updated even for the rows where it should not hav.
Here is the code :
#Override
public void onBindViewHolder(ViewHolder viewHolder, int i) {
final OrderItem order = mItems.get(i);
viewHolder.tvName.setText(getCustomers().get(order.getmConsumerId()).getProfile().getFirstName().toUpperCase() );
viewHolder.tvDelType.setText(getDeliveryType(order.getmDeliveryType()));
viewHolder.tvTime.setText(formatTime(order.getmETATime()));//format time before present
viewHolder.tvAmt.setText("Rs " + order.getmAmount());
viewHolder.rlvName.setTitleText(order.getmConsumerId().substring(0, 1));
viewHolder.rlvName.setTitleSize(82f);
viewHolder.tvDetails.setOnClickListener(new OnClickListener() {
#Override
public void onClick(View view) {
Intent intent = new Intent(context, OrderDetailsActivity.class);
Gson gson = new Gson();
String ord = gson.toJson(order);
String user = gson.toJson(getCustomers().get(order.getmConsumerId()));
intent.putExtra("orderItem", ord);
intent.putExtra("user", user);
context.startActivity(intent);
}
});
setListeners(viewHolder, order, i);
//setStatus(viewHolder,order,i);
if(!order.getmOrderStatus().equals(Constants.ENUM_ORDERS_STATUS_PENDING))
{
viewHolder.tvOrderStatus.setText((order.getmOrderStatus().toUpperCase()));
viewHolder.tvOrderStatus.setVisibility(View.VISIBLE);
viewHolder.llButtons.setVisibility(View.GONE);
}
//viewHolder.rlvName.setBackgroundColor(Color.parseColor("FF0000"));
}
So based on the value my order has, I need to change the UI of the row . After scrolling, it changes the values of the row where it should not have.
Please suggest.

Because of recyler views RAM Management,the recyler view try to load cards from cache and because of your recyler view's cache, the items loads incorrectly.
for preventing this you must add a else to your if like this:
if(!order.getmOrderStatus().equals(Constants.ENUM_ORDERS_STATUS_PENDING))
{
viewHolder.tvOrderStatus.setText((order.getmOrderStatus().toUpperCase()));
viewHolder.tvOrderStatus.setVisibility(View.VISIBLE);
viewHolder.llButtons.setVisibility(View.GONE);
}
else
{
//The Default Item View Settings Here...
}

Related

How do I get a list in an android RecyclerView to update after an activity that modifies what it was showing?

I've seen some RecyclerView examples that simply modify an item within the click listener. My problem is that I start an activity from a click on an item, and the activity can change or delete the clicked item from the list. So the view needs to be updated after the activity is finished.
This code mostly from another developer passes the serialized item and position of the item to the activity as extra data. The position was intended to use to update the view later.
However, I found these comments:
"Do not treat position as fixed; only use immediately and call holder.getAdapterPosition() to look it up later. RecyclerView will NOT call onBindViewHolder again when the position of the item changes in the data set unless the item itself is invalidated or the new position cannot be determined. For this reason, you should only use the position parameter while acquiring the related data item inside this method, and should NOT keep a copy of it. If you need the position of an item later (eg. in a click listener), use getAdapterPosition() which will have the updated adapter position."
In the Adapter:
#Override
public void onBindViewHolder (#NonNull ItemAdapter.MyViewHolder holder, final int position)
{
final Item item = items.get(position);
holder.itemTitle.setText(item.getTitle());
holder.lay_item.setOnClickListener (new View.OnClickListener () {
#Override
public void onClick(View v) {
Intent intent = new Intent(context, ItemDetailActivity.class);
intent.putExtra("Item", item);
intent.putExtra("position", position);
context.startActivity (intent);
// TODO is this the place to refresh the view?
//holder.getAdapterPosition();
}
});
}
and for the activity:
Item currentItem;
protected void onCreate (Bundle savedInstanceState)
{
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_item_detail);
currentItem = (Item)getIntent().getSerializableExtra("Item"));
position = getIntent().getExtras().getInt("position");
...
}
I realize that the object held by the view is serialized into the extra data, so currentObject in the activity is not the same object.
I don't know why they did it like that. If that is the norm, please tell me how the list view is updated for changes to the object in the activity. If that is not the norm, how should it be done?
Therein lies the basis of the question, stated in the title:
How do I get a list in an android RecyclerView to update after an activity that modifies what it was showing?
Within the Activity, there is this click listener for the "save" button to update the database:
btn_save.setOnClickListener (new View.OnClickListener () {
#Override
public void onClick(View v) {
// ... stuff that updates the item attributes from the view elements...
// Save it. It is this object that should then be in the list in the RecyclerView.
try
{
MyApp.getDatabase().updateItem(item);
// TODO: This might work if currentItem was actually the one in the list
//adapter.notifyDataSetChanged();
}
catch (PersistenceException ex)
{
// notify user of failure
Toast.makeText (EditItemActivity.this, getResources().getString(R.string.item_update_fail), Toast.LENGTH_LONG).show ();
finish();
return;
}
Toast.makeText(EditItemActivity.this, getResources().getString(R.string.item_update_success), Toast.LENGTH_LONG).show ();
finish ();
}
});
The adapter was created in the fragment like this (where "rv_items" is the RecyclerView):
adapter = new ItemAdapter(itemList, getActivity());
rv_items.setAdapter(adapter);
The Item class is declared as:
class Item implements Serializable
In general, any time you're talking about "modifying a RecyclerView", that's a hint that you're looking at things the wrong way. You should always think about modifying the data, and then realize that the RecyclerView is just one way to display that data. Of course, you'll need to call methods like notifyDataSetChanged() whenever you modify the data so that the RecyclerView can know to update its display to pick up the changes, but you should still always think about the data first.
That being said, the root of the problem here is that you need some way to uniquely identify your item in your list of data items. Generally, I'd lean towards using some sort of unique ID here (instead of position). Then, when your second activity finishes and returns its result, you can use the ID to update your data list and dispatch the changes to the RecyclerView.
With all that, you'd have something like this:
#Override
protected void onActivityResult(int requestCode, int resultCode, #Nullable Intent data) {
super.onActivityResult(requestCode, resultCode, data);
if (requestCode == MY_REQUEST) {
if (resultCode == Activity.RESULT_OK && data != null) {
String id = data.getStringExtra("id");
for (int i = 0; i < items.size(); ++i) {
if (items.get(i).getId().equals(id)) {
// the item at position i was updated.
// insert your code here...
// at the end, notify the adapter
adapter.notifyItemChanged(i);
}
}
}
}
}

Android - modify the adapter and items of a recyclerview in on click listener

My situation is this: I set a recyclerView on runtime in a retrofit method async request, called in a loop.
So one by one the items are added to the recyclerview.
#Override
public void onResponse(Call<DirectionResults> call, Response<DirectionResults> response) {
Legs legs = response.body().getRoutes().get(0).getLegses().get(0);
NearbyBusStation current = new NearbyBusStation(currentItem, legs.getDistance().getValue(), legs.getDuration().getText());
int i;
for(i = 0; i < nearbyBusStations.size();i++){
if (current.getDistance() < nearbyBusStations.get(i).getDistance())
break;
}
nearbyBusStations.add(i,current);
mAdapter.notifyItemInserted(nearbyBusStations.size() - 1);
}
In The adapter:
#Override
public void onBindViewHolder(ViewHolder holder, int position) {
// - get element from your dataset at this position
// - replace the contents of the view with that element
final NearbyBusStation currentItem = mDataset.get(position);
holder.name.setText(currentItem.getName().getName());
holder.distance.setText(currentItem.getDistance() +" m");
holder.time.setText(currentItem.getTime() + " a piedi");
holder.itemView.setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View v) {
//I suppose that code go here
}
I want that when an item of the recyclerview is clicked, it expand adding new element below or that the recyclerview was cleared and the new element are added, with a button to return to initial recyclerview.
(on the click on a bus stop, it will open the next buses which pass from it)
I've tried to use external libraries to create expandable recyclerview but they work very well with fixed item, in my case items are adding by retrofit at runtime.
If is correct to use the onclick inside onBindViewHolder, how? with another adapter?
I need to insert items one by one because I used this library for animations
compile 'jp.wasabeef:recyclerview-animators:2.2.4'
which work only inserting or deleting elements one by one, but if is it a problem, I can change library for animation without any problem.
Thank you in advance!

recyclerView ClickListener return null after notifyDataSetChanged()

i am using a recyclerView and loading the data from database.The recyclerView item have 2 buttons. The recyclerView loads fine and clicks work fine for the following code.
allData=db.getCatData(CATEGORYLIST_CIDD,1);
textlistadapter = new TextListAdapter(getActivity(), allData);
recyclerView.setAdapter(textlistadapter);
later i rearrange the adapter data for displaying the data in descending order by using this code
db=new DatabaseHandler(getActivity().getApplicationContext());
allData=db.getCatData(CATEGORYLIST_CIDD,order_id);
textlistadapter = new TextListAdapter(getActivity(), allData);
recyclerView.setAdapter(textlistadapter);
textlistadapter.notifyDataSetChanged();
Now the items are rearranged on the recyclerView, but the onShareButtonClickListener returns null, here is the code of the adapter.
private OnShareButtonClickListener onShareButtonClickListener;
public interface OnShareButtonClickListener {
void onItemClick(View view, Pojo obj, int position);
}
public void setOnShareButtonClickListener(final OnShareButtonClickListener onShareButtonClickListener) {
this.onShareButtonClickListener = onShareButtonClickListener;
}
the click listener code is here
holder.bt_share.setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View view) {
//Snackbar.make(view, "Share Clicked", Snackbar.LENGTH_SHORT).show();
if (onShareButtonClickListener != null) {
onShareButtonClickListener.onItemClick(view, c, position);
}
}
});
The click does not work since the onShareButtonClickListener is null.
The same code works well for the first time and once i change the order then the click events are not working, all other functions are working well.
you should only need to do this once:
textlistadapter = new TextListAdapter(getActivity(), allData);
recyclerView.setAdapter(textlistadapter);
later on, if you want to update/change the data, then you should do something like this:
textlistadapter.setListItems(allData);
textlistadapter.notifyDataSetChanged();
Where setListItems() is a public method in your adapter, an example like this:
public void setListItems(List<> allData) {
this.data.clear();
this.data.addAll(allData);
}
It's easy to understand what you are doing wrong. You populate an array which will feed your adapter with data, that meaning that, after setting the adapter, if you want it's data to change, you only need to change the array you populated the data with, and notify the adapter for the data change.
For example:
myData = db.getData();
myAdapter = new MyAdapter(this,myData);
recyvlerview.setAdapter(myAdapter);
Now every time you want to change the data, you only need to change your myData and notifity the adapter
myAdapter.notifyDataSetChange();
Basically instead of doing this:
db=new DatabaseHandler(getActivity().getApplicationContext());
allData=db.getCatData(CATEGORYLIST_CIDD,order_id);
textlistadapter = new TextListAdapter(getActivity(), allData);
recyclerView.setAdapter(textlistadapter);
textlistadapter.notifyDataSetChanged();
This should suffice
db=new DatabaseHandler(getActivity().getApplicationContext());
allData=db.getCatData(CATEGORYLIST_CIDD,order_id);
textlistadapter.notifyDataSetChanged();

RecyclerView.Adapter onBindViewHolder() gets wrong position

I'll show the code and after the steps to get the problem.
I have a recyclerview inside a tabbed fragment that takes the dataset from a custom object:
mRecyclerView = (RecyclerView) v.findViewById(R.id.recyclerview);
mRecyclerView.setLayoutManager(mLayoutManager);
mRecyclerAdapter = new MyRecyclerAdapter(mMes.getListaItens(), this, getActivity());
mRecyclerView.setAdapter(mRecyclerAdapter);
I set the longclick behavior of the list items in onBindViewHolder() of the adapter:
#Override
public void onBindViewHolder(final ViewHolder holder, final int position) {
ItemMes item = mListaItens.get((position));
holder.descricao.setText(item.getDescrição());
holder.valor.setText(MainActivity.decimalFormatWithCod.format(item.getValor()));
...
holder.itemView.setOnLongClickListener(new View.OnLongClickListener() {
#Override
public boolean onLongClick(View v) {
new MaterialDialog.Builder(mContext)
.title(holder.descricao.getText().toString())
.items(R.array.opcoes_longclick_item)
.itemsCallbackSingleChoice(-1, new MaterialDialog.ListCallbackSingleChoice() {
#Override
public boolean onSelection(MaterialDialog dialog, View view, int which, CharSequence text) {
switch (which) {
case 0:
mParentFragment.showUpdateItemDialog(position);
return true;
case 1:
mParentFragment.showDeleteItemDialog(position);
return true;
}
return false;
}
})
.show();
return true;
}
});
}
Then, the methods in the fragment that take care of delete the item itself:
public void showDeleteItemDialog(int position) {
final ItemMes item = mMes.getListaItens().get(position);
new MaterialDialog.Builder(getActivity())
.title("Confirmar Remoção")
.content("Tem certeza que deseja remover " + item.getDescrição() + "?")
.positiveText("Sim")
.negativeText("Cancelar")
.onPositive(new MaterialDialog.SingleButtonCallback() {
#Override
public void onClick(#NonNull MaterialDialog dialog, #NonNull DialogAction which) {
deleteItem(item);
}
})
.show();
}
public void deleteItem(ItemMes item) {
getMainActivity().deleteItemFromDatabase(item.getID());
int position = mMes.getListaItens().indexOf(item);
mMes.getListaItens().remove(position);
mRecyclerAdapter.notifyItemRemoved(position);
atualizaFragment();
}
And finally the method in activity that do the DB operation:
public int deleteItemFromDatabase(long id) {
SQLiteDatabase db = dataBaseHelper.getWritableDatabase();
String where = DBHelper.COLUNA_ID + " = ?";
String[] args = {String.valueOf(id)};
int rowsAffected = db.delete(DBHelper.TABELA_ITEM, where, args);
db.close();
return rowsAffected;
}
Now i'll reproduce the steps:
I'm showing 3 itens in the listview. Then I try to remove the first:
1 - The longclick is intercepted passing the correct index:
2 - The item is correctly deleted from the database:
3 - After all this, as expected, the adapter is storing and showing 2 items...
SO, if I try to delete the first item of this 2 item list I get the wrong position (should be 0, is 1):
And also if I try to delete the last item of this 2 item list I get the wrong position (should be 1, is 2):
The question is: If I have a dataset of size 2 (and the adapter knows it), how can it call onBindViewHolder(ViewHolder holder, int [last index +1])?
I have no idea what could be wrong. So I ask help cause I'm thinking about give up this project cause I do everything right but always something dont works, and Im tired.
Thanks in advance.
I've noticed that in method onBindViewHolder(VH holder, int position) while the position was comming wrong, the holder.getAdapterPosition() gives me always the correct position.
So I changed my code from:
ItemMes item = mListaItens.get((position));
...
mParentFragment.showUpdateItemDialog(position);
...
mParentFragment.showDeleteItemDialog(position);
....
To:
ItemMes item = mListaItens.get((holder.getAdapterPosition()));
...
mParentFragment.showUpdateItemDialog(holder.getAdapterPosition());
...
mParentFragment.showDeleteItemDialog(holder.getAdapterPosition());
....
And everything works well. This is very strange but...
Thanks everybody.
Took a look at the adapter code you provided in the comment and it's pretty straightforward. Try this: rather than call notifyItemRemoved(), call notifyDataSetChanged(). This is rather expensive as it will cause your adapter to re-bind the data set (and re-create ViewHolders), but since you're using an ArrayList where you are removing an element, it's really the simplest way to do it. Otherwise you'll have to track the position of the items and when an item is removed it cannot change the position of other items - or handle the case where items shift their position in the data set.
Try this code in onBindViewHolder()
int adapterPos=holder.getAdapterPosition();
if (adapterPos<0){
adapterPos*=-1;
}
ItemMes item = mListaItens.get((adapterPos));
mParentFragment.showUpdateItemDialog(adapterPos);
Use adapterPos instead of position variable.
According to RecyclerView's getAdapterPosition documentation:
RecyclerView does not handle any adapter updates until the next layout traversal. This
may create temporary inconsistencies between what user sees on the screen and what
adapter contents have. This inconsistency is not important since it will be less than
16ms but it might be a problem if you want to use ViewHolder position to access the
adapter. Sometimes, you may need to get the exact adapter position to do
some actions in response to user events. In that case, you should use this method which
will calculate the Adapter position of the ViewHolder.
So in case of implementing user events, using getAdapterPosition is a recommended way to go.

Update TextView from ListView android row

I am retrieving some data in an Async class called from a Custom ArrayAdapter. When i add a new comment, i update the comment text view and that works ok, but after i reload the entire list the updates don't appear anymore. I can see in the logcat that there is a new comment nr, but not on the UI.
Shouldn't this : answersListView.invalidateViews be enough? I am trying to update that single row from the listview, to escape the issue with not updating the comment nr after a while.
private void updateView(int index) {
System.out.println("index: " + index);
View v = answersListView.getChildAt(index - answersListView.getFirstVisiblePosition());
if (v == null)
return;
final TextView nrComments = (TextView) v.findViewById(com.dub.mobile.R.id.showCommentsTxt);
if (nrComments != null) {
if (nrComments.getText().toString().trim().length() == 0) {
// first comment
nrComments.setText("1 comments");
} else {
// comments exist already
int newNr = Integer.parseInt(nrComments.getText().toString().trim()
.substring(0, nrComments.getText().toString().trim().indexOf("comments")).trim()) + 1;
nrComments.setText(newNr + " comments");
}
System.out.println("final nr of comments: " + nrComments.getText());
nrComments.setVisibility(View.VISIBLE);
}
answersListView.getAdapter().getView(position, v, answersListView);
v.setBackgroundColor(Color.GREEN);
answersListView.invalidateViews();
}
And :
updateView(position);
adapter.notifyDataSetChanged();
The answer is pretty much what #Rami said in a comment to your question. You don't update directly the views, you just need to update the data. In your adapter, you override the getView method, in there is where you make all this changes, you don't need the updateViews method.
Let me try to explain how it works.
The listView uses an Adapter.
The List view ask the Adapter "give me the view in X position" with the getView method.
The adapter creates that view, is returned to the ListView, and that what is shown.
The Adapter itself, should contain a List with the data you want to show.
Those views (rows) are created and destroyed everytime one of those views become visible or invisible in the screen, or if you call the notifyDataSetChanged method.
So now, the thing is, if you change, let's say, the object in the position 5, of the adapter List for a different object with new data, then next time the ListView ask the Adapter to give me the view in the position 5, the adapter is going to create the View (row) with the new data. That's it.
you need to update the data in the UI thread.
if you have context in your Async class use
runOnUiThread(new Runnable() {
#Override
public void run() {
//update here
}
});
or
android.os.Handler handler = new android.os.Handler();
handler.post(new Runnable() {
#Override
public void run() {
//update here
}
});
That's not how you change data for an item in a list view. You must have used some array of strings to fill data in textview in the getView function of the adapter. You only need to change that array and then call notifydatasetchanged on adapter. Views are recycled by adapter so above does not make sense
getView (...)
{
TextView tv = new TextView(context);
tv.setText(myData[index]); // You need to change myData array contents
}

Categories

Resources