Here's my onBindViewHolder method which runs for every row in a recyclerView. I have around 500 to 600 rows at once. What Im doing is
1 - Initializing my Parcelable POJO class object
2 - Passing this object as an Intent argument to start another activiy when onItemClick event occurs.
AFAIK the Java doesn't support true closure so, It forces us to make an object/variable final if you want to use it in an inner class. Java imitate closure behavior by storing constant values in hidden variables and refer those later when needed.
What I want to know,
Is It fine in terms of performance/memory to create an object for each row in onBinderViewHolder method ?
When these objects get garbage collected ?
#Override
public void onBindViewHolder(ViewHolder holder, int position) {
final Cursor cursor = getItem(position);
final FF_Task_List task = new FF_Task_List();
task.setSiteArea(cursor.getString(TaskListEntry.TaskListQuery.SITE_AREA));
task.setSiteCode(cursor.getString(TaskListEntry.TaskListQuery.SITE_CODE));
task.setValidOn(cursor.getString(TaskListEntry.TaskListQuery.VALID_ON));
task.setScore(cursor.getFloat(TaskListEntry.TaskListQuery.SCORE));
task.setSurveyStatusId(cursor.getInt(TaskListEntry.TaskListQuery.SURVEY_STATUS_ID));
task.setTokenId(cursor.getLong(TaskListEntry.TaskListQuery.TOKEN_ID));
task.setEmpId(cursor.getLong(TaskListEntry.TaskListQuery.EMP_ID));
task.setSurveyTypeId(cursor.getInt(TaskListEntry.TaskListQuery.SURVEY_TYPE_ID));
final int scoreTextLength = String.valueOf(task.getScore()).length();
final float score = task.getScore();
final int scoreColor = Utilities.getColorByStatus(mContext, task.getSurveyStatusId());
final int statusIcon = Utilities.getIconByStatus(task.getSurveyStatusId());
final String validOnDate = Utilities.formatDBDate(task.getValidOn(), Utilities.Dates.DISPLAY_DATE);
sb = new SpannableStringBuilder(task.getScore() + "%");
sb.setSpan(new StyleSpan(Typeface.NORMAL), scoreTextLength, scoreTextLength + 1, Spannable.SPAN_INCLUSIVE_INCLUSIVE);
holder.siteName.setText(task.getSiteArea());
holder.siteCode.setText(task.getSiteCode());
holder.siteInfo.setText(validOnDate);
holder.siteScore.setTextColor(scoreColor);
if(score != 0) {
holder.siteScore.setPadding(5,0,0,0);
holder.siteScore.setText(sb);
}else{
holder.siteScore.setText("--");
}
holder.siteScoreProgress.setPercent(score / 100);
holder.siteStatusIcon.setImageResource(statusIcon);
holder.taskItem.setOnClickListener(new View.OnClickListener(){
#Override
public void onClick(View v) {
//raise custom itemClick event
itemClickListener.itemClicked(task ,v);
}
});
}
Since you have your model stored in a list you will get the value every time you call getItem(position) inside the onBindViewHolder(ViewHolder holder, int position).
The RecyclerView controls what are kept in memory at any time and so when you scroll the RecyclerView things (views and models) will get garbage collected when it's time for it.
I don't know how many items Android will keep in memory for you at any given time, but it's no more than the items that visible on the screen - and then it might keep a few items outside of view created. On a phone this would usually be 10-15 items, depending on the screensize and item's size.
Then when you scroll back to the same views again, the model will be recreated again and the ViewHolder will be updated with the newly created values.
Unless your data model is huge, it shouldn't be a problem creating the objects inside the onBindViewHolder(ViewHolder holder, int position).
If you want to avoid creating objects inside the onBindViewHolder convert the Cursor to your FF_Task_List objects and set the list of FF_Task_List to your backing model of the RecyclerView.
Related
I have a RecyclerView with a Horizontal LinerLayout. It displays numbers from 10 to 1, that is used to rate something.
When I select 10 and scroll back to 1 and select 1. I have to update the UI to remove selection on 10 and update selection on 1. But, when I use findViewHolderForAdapterPosition() to remove the selection on 10 it gives me a NullPointerException
I am getting the position in the ViewHolder with getAdapterPosition().
Then, I use that position to get the ViewHolder by calling findViewHolderForAdapterPosition() on my recycler view object and update the UI to remove the selection from 10.
vh = (RatingRecyclerAdapter.ViewHolder)
mRecycler.findViewHolderForAdapterPosition(previousPosition);
vh.textRating.setBackgroundResource(R.drawable.rating_background_selected_orange);;
With some tests, I found out when I try to do the same thing without scrolling it works fine. However, only when I am scrolling it gives me a NullPointerException
How do I fix this?
As requested here is some important code from Adapter class.
#Override
public void onBindViewHolder(RatingRecyclerAdapter.ViewHolder holder, int position) {
String itemText = itemList.get(position);
holder.textRating.setText(itemText);
}
public class ViewHolder extends RecyclerView.ViewHolder {
TextView textRating;
public ViewHolder(View itemView) {
super(itemView);
textRating = (TextView) itemView.findViewById(R.id.text_rating);
textRating.setOnClickListener(ratingClickListener);
}
private final View.OnClickListener ratingClickListener = new View.OnClickListener() {
#Override
public void onClick(View v) {
int position = getAdapterPosition();
if (callback != null) {
callback.onClickRating(v, position);
}
}
};
}
Activity Class
#Override
public void onClickRating(View view, int position) {
RatingRecyclerAdapter.ViewHolder vh;
int color;
int previousPosition = mAdapter.getSelectedPosition(); //Get previously clicked postion if any.
if (previousPosition == Constants.NO_ITEM_SELECTED) {
// An item was selected first time
vh = (RatingRecyclerAdapter.ViewHolder)
mRecycler.findViewHolderForAdapterPosition(position);
mAdapter.setSelectedPosition(position); // Save new item selected position.
color = Utility.getItemColor(mAdapter.getSelectedRating());
mAdapter.setSelectedRatingResource(vh, color);
return;
}
if (position == previousPosition) // Same item was selected
return;
vh = (RatingRecyclerAdapter.ViewHolder)
mRecycler.findViewHolderForAdapterPosition(previousPosition);
color = Utility.getItemColor(mAdapter.getSelectedRating());
mAdapter.setUnselectedRatingResource(vh, color); // Remove the previous selected item drawables.
vh = (RatingRecyclerAdapter.ViewHolder)
mRecycler.findViewHolderForAdapterPosition(position);
mAdapter.setSelectedPosition(position); // Save new item selected position.
color = Utility.getItemColor(mAdapter.getSelectedRating());
mAdapter.setSelectedRatingResource(vh, color); // Set the new selected item drawables. Setting some background to indicate selection.
}
As Sevastyan has written in the comment, the RecyclerView immediately recycles the view as soon as the item is out of the screen. So if we call findViewHolderForAdapterPosition() for a view which is outside the screen we get a null value. (I am not confirming this is the actual case. But, this is what it seems to me.)
So I created a class that stores all the data about an item in the RecyclerView and stored all the colours and value of that item in the class. And when we are populating the view, set the all the colours based on data stored in that class.
PS: I THANK Sevastyan for not giving me the answer directly. But, only giving me the reason for getting that Exception.
If your view is out of the screen, it can be recycled OR cached.
In case it's recycled, you can handle in onViewRecycled() method or setup the view again inside onBind() when the view becomes visible (you can save the state on the object of your list if needed).
In case it's not recycled (onViewRecycled method not called for that position), it's probably cached. You can set the cache size to zero to prevent this state from happening.
recycler.setItemViewCacheSize(0)
Edit: if anyone is having the same problem in the future it was a pretty easy fix. I used clearView(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder) to handle when the entire drag was finished and had forgotten to call it's super-method. That's why it didn't update properly.
Original question:
Here's the entire code: https://github.com/vustav/Ppaaiinntt/tree/master/app/src/main/java/com/decent/rvtest
Everything works fine except when I add an element right after deleting another. It does exist though. If I add another there's a properly sized space between the old list and the new element. I use a string where they add their names before a print and it shows there, and if I drag to change positions of the elements it shows up properly.
My reputation doesn't allow me to post images so an imgur-album will have to do:
http://imgur.com/a/bmb17
On the first image there's three elements and the string is printed at the bottom.
The second image is after the swipe. Notice the string is updated.
The third is after adding another "111". The string is correct but it doesn't show up in the view.
The fourth is after adding another. The string is still correct and the new element shows up in the view.
The last image is after dragging to change the position of the last two elements. Now everything is fine again.
These are the relevant methods (I think):
protected void add(PictureElement pe){
chain.add(pe);
notifyItemInserted(chain.size()-1);
}
public void remove(int position) {
chain.remove(position);
notifyItemRemoved(position);
}
protected void swap(int from, int to){
chain.swap(from, to);
notifyItemMoved(from, to);
}
Edit: onBindViewHolder, getItemCount and the ViewHolder:
#Override
public int getItemCount() {
return chain.size();
}
#Override
public void onBindViewHolder(PEViewHolder PEViewHolder, int i) {
PictureElement pe = chain.get(i);
PEViewHolder.name.setText(pe.getName());
}
protected static class PEViewHolder extends RecyclerView.ViewHolder {
protected TextView name;
public PEViewHolder(View v) {
super(v);
name = (TextView) v.findViewById(R.id.txtName);
}
}
Interesting quote on the different notify methods you are using
public final void notifyItemRemoved (int position) Notify any registered observers that the item previously located at position has
been removed from the data set. The items previously located at and
after position may now be found at oldPosition - 1. This is a
structural change event. Representations of other existing items in
the data set are still considered up to date and will not be rebound,
though their positions may be altered. Parameters position : Position
of the item that has now been removed.
public final void notifyItemRangeChanged (int positionStart, int itemCount) Notify any registered observers that the itemCount items
starting at position positionStart have changed. Equivalent to calling
notifyItemRangeChanged(position, itemCount, null);. This is an item
change event, not a structural change event. It indicates that any
reflection of the data in the given position range is out of date and
should be updated. The items in the given range retain the same
identity.
Could you try and comment this block of code and check if it works
//Called by the ItemTouchHelper when the user interaction with an element is over and it also
// completed its animation
/*
#Override
public void clearView(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder) {
//update from where the action took place
mPEAdapter.updateChain(viewHolder.getLayoutPosition());
//clearView is called after onMove so any drags or swipes are complete
dragging = false;
mPEAdapter.setSwipe(false);
}
*/
I have a list with 13 items (although items may be added or removed), positions 0-12. When the fragment containing the RecyclerView is first shown, only positions 0 through 7 are visible to the user (position 7 being only half visible). In my adapter I Log every time a view holder is binded/bound (idk if grammar applies here) and record its position.
Adapter
#Override
public void onBindViewHolder(final ViewHolder holder, final int position) {
Log.d(TAG, "onBindViewHolder() position: " + position);
...
}
From my Log I see that positions 0-7 are bound:
I have a selectAll() method that gets each ViewHolder by adapter position. If the returned holder is NOT null I use the returned holder to update the view to show it's selected. If the returned holder IS null I call selectOnBind() a method that flags the view at that position update to show it's selected when it's binded rather than in real time since it's not currently shown:
public void selectAll() {
for (int i = 0; i < numberOfItemsInList; i++) {
MyAdapter.ViewHolder holder = (MyAdapter.ViewHolder)
mRecyclerView.findViewHolderForAdapterPosition(i);
Log.d(TAG, "holder at position " + i + " is " + holder);
if (holder != null) {
select(holder);
} else {
selectOnBind(i);
}
}
}
In this method I Log the holder along with its position:
So up to this point everything seems normal. We have positions 0-7 showing, and according to the Log these are the positions bound. When I hit selectAll() without changing the visible views (scrolling) I see that positions 0-7 are defined and 8-12 are null. So far so good.
Here's where it gets interesting. If after calling selectAll() I scroll further down the list positions 8 and 9 do not show they are selected.
When checking the Log I see that it's because they are never bound even though they were reported to be null:
Even more confusing is that this does not happen every time. If I first launch the app and test this it may work. But it seems to happen without fail afterwards. I'm guessing it has something to do with the views being recycled, but even so wouldn't they have to be bound?
EDIT (6-29-16)
After an AndroidStudio update I cannot seem to reproduce the bug. It works as I expected it to, binding the null views. If this problem should resurface, I will return to this post.
This is happening because:
The views are not added to the recyclerview (getChildAt will not work and will return null for that position)
They are cached also (onBind will not be called)
Calling recyclerView.setItemViewCacheSize(0) will fix this "problem".
Because the default value is 2 (private static final int DEFAULT_CACHE_SIZE = 2; in RecyclerView.Recycler), you'll always get 2 views that will not call onBind but that aren't added to the recycler
In your case views for positions 8 and 9 are not being recycled, they are being detached from the window and will be attached again. And for these detached view onBindViewHolder is not called, only onViewAttachedToWindow is called. If you override these function in your adapter, you can see what I am talking.
#Override
public void onViewRecycled(ViewHolder vh){
Log.wtf(TAG,"onViewRecycled "+vh);
}
#Override
public void onViewDetachedFromWindow(ViewHolder viewHolder){
Log.wtf(TAG,"onViewDetachedFromWindow "+viewHolder);
}
Now in order to solve your problem you need to keep track of the views which were supposed to recycled but get detached and then do your section process on
#Override
public void onViewAttachedToWindow(ViewHolder viewHolder){
Log.wtf(TAG,"onViewAttachedToWindow "+viewHolder);
}
The answers by Pedro Oliveira and Zartha are great for understanding the problem, but I don't see any solutions I'm happy with.
I believe you have 2 good options depending on what you're doing:
Option 1
If you want onBindViewHolder() to get called for an off-screen view regardless if it's cached/detached or not, then you can do:
RecyclerView.ViewHolder view_holder = recycler_view.findViewHolderForAdapterPosition( some_position );
if ( view_holder != null )
{
//manipulate the attached view
}
else //view is either non-existant or detached waiting to be reattached
notifyItemChanged( some_position );
The idea is that if the view is cached/detached, then notifyItemChanged() will tell the adapter that view is invalid, which will result in onBindViewHolder() getting called.
Option 2
If you only want to execute a partial change (and not everything inside onBindViewHolder()), then inside of onBindViewHolder( ViewHolder view_holder, int position ), you need to store the position in the view_holder, and execute the change you want in onViewAttachedToWindow( ViewHolder view_holder ).
I recommend option 1 for simplicity unless your onBindViewHolder() is doing something intensive like messing with Bitmaps.
When you have large number of items in the list you have passed to recyclerview adapter you will not encounter the issue of onBindViewHolder() not executing while scrolling.
But if the list has less items(I have checked on list size 5) you may encounter this issue.
Better solution is to check list size.
Please find sample code below.
private void setupAdapter(){
if (list.size() <= 10){
recycler.setItemViewCacheSize(0);
}
recycler.setAdapter(adapter);
recycler.setLayoutManager(linearLayoutManager);
}
I think playing with view is not a good idea in recyclerview. The approach I always use to follow to just introduce a flag to the model using for RecyclerView. Let assume your model is like -
class MyModel{
String name;
int age;
}
If you are tracking the view is selected or not then introduce one boolean to the model. Now it will look like -
class MyModel{
String name;
int age;
boolean isSelected;
}
Now your check box will be selected/un-selected on the basis of the new flag isSelected (in onBindViewHolder() ). On every selection on view will change the value of corresponding model selected value to true, and on unselected change it to false. In your case just run a loop to change all model's isSelected value to true and then call notifyDataSetChanged().
For Example, let assume your list is
ArrayList<MyModel> recyclerList;
private void selectAll(){
for(MyModel myModel:recyclerList)
myModel.isSelected = true;
notifyDataSetChanged();
}
My suggestion, while using recyclerView or ListView to less try to play with views.
So in your case -
#Override
public void onBindViewHolder(final ViewHolder holder, final int position) {
holder.clickableView.setTag(position);
holder.selectableView.setTag(position);
holder.checkedView.setChecked(recyclerList.get(position).isSelected);
Log.d(TAG, "onBindViewHolder() position: " + position);
...
}
#Override
public void onClick(View view){
int position = (int)view.getTag();
recyclerList.get(position).isSelected = !recyclerList.get(position).isSelected;
}
#Override
public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
int position = (int)buttonView.getTag();
recyclerList.get(position).isSelected = isChecked;
}
Hope it will help you, Please let me know if you need any further explanation :)
So I think you question is answered below by #Pedro Oliveira. The main sense of RecycleView, that he using special algorithms for caching ViewHolder in any time. So next onBindViewHolder(...) may not work, for ex. if view is static, or something else.
And about your question you think to use RecycleView for dynamic changed Views. DON'T DO IT! Because RecycleView invalidates views and has caching system, so you will have a lot of problems.
Use LinkedListView for this task!
I have a recyclerView in which i have a list of items displayed in a LinearLayout.There is a "increase" button in every list which will increment a quantity by "1".But when I click the button on the first list item to increment the number..the incremented value is displayed in the last list item of the recyclerView not in the desired position where i clicked.Can anyone help me find the solution?
#Override
public void onBindViewHolder(MyViewHolder holder, int position) {
myholder = holder;
holder.item_name_text.setText(data.get(position).getName());
holder.item_price_text.setText(data.get(position).getPrice().toString());
holder.item_quantity_text.setText("500");
//Adding item to the cart
holder.add_image.setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View v) {
if(totalItem>=0 && totalItem<10){
totalItem++;
myholder.item_totalquantity_text.setText(String.valueOf(totalItem));
}else{
Toast.makeText(myholder.itemView.getContext(),"Cannot add more item",Toast.LENGTH_SHORT).show();
}
}
});
So I always think of recyclerviews as the frontend that displays my data. If you would like to make changes to the actual data itself and make sure it gets reflected in the recyclerview, you will need to ensure the changes are made inside the arraylist of data objects.
You will need to add a field inside the data object and call it counter. Then you must increment the counter once the user clicks on the onClickListener.
myholder.item_totalquantity_text.setText(data.get(position).setCounter(data.get(position).getCounter())+1);
Do not set OnClickListener() in onBindViewHolder(). Instead do it in your ViewHolder class itself. And main ly as RecyclerView re-uses the holder objects to represent the new set of data that is becoming visible. So the listener on which yo click might be belonging to some other view holder so it appears there instead.
Have a quick read of this and this. These are not very relevant to you but it has info to understnad recycling concept so you can fix your stuff easily.
In my application I populate a GridView's adapter with random data from a file. The data is shown to the user as a TextView per item. If the user touch an item, the item changes the background colour.
The problem is that if the user touches an item and then rotates the device, the item returns to its original aspect (with the normal background colour)
I've tried different approaches:
Implementing my own adapter
Extending BaseAdapter
Using ArrayAdapter
Using selectors for the TextView
Extending the TextView item with custom styles (from here and here)
Disabling the View within the GridView's onItemClick(AdapterView<?> parent, View view, int position, long id)
What I want to do is keep the Views' colour/style/aspect when I rotate the device
Note:
Why I load data radomly from a file?
The file contains different words. Every time the player start the activity (it is a game) different words in random order are shown inside the GridView. The user hast to point to the right word. If the user make a mistake, the word changes the colour (indeed, I prefer to disable the View). The process is repeated till the user makes the right choice.
You can save the selected states of your list using onSaveInstanceState.
As you click on an item in your list you can assign a state to a boolean array.
Implement the onSaveInstanceState method in your Fragment/Activity.
public void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
outState.putBooleanArray(BundleArgs.STATES, mAdapter.getStates());
}
And then in onCreateView you pass those values to your adapter.
if (savedInstanceState != null) {
states = savedInstanceState.getBooleanArray(BundleArgs.STATES);
//Declare adapter and pass states to it
myAdapter = new Adapter(context, values, states);
}
That's a mistake I've seen several times repeated on SO.
The data and the view that represents the data are totally different entities and they should be treated separately.
You need to keep the state of your data in another data element and preserve that data element during rotation. For example (it's just an example, there' several ways of doing it):
// possible states
private static final int NORMAL = 0;
private static final int RIGHT = 1;
private static final int WRONG = 2;
Map<String, Integer> states; // here you keep the states
then on every click, on the code that checks the answer and change color:
// process the click/state change
states.put(word, newState);
then on rotation:
public void onSaveInstanceState(Bundle outState) {
outState.putSerializable("states", states);
}
and on create
// onCreate
if (savedInstanceState != null) {
states = (Map<String, Integer>) savedInstanceState.getSerializable("states");
} else {
states = new HashMap<String, Integer>();
}
and then back on your custom adapter you have to check the state and modify the view accordingly.
// inside getView
int state = 0
if(states.containsKey(word)){
state = states.get(word).intValue();
}
switch(state){
// deal with the cases and set the color
}