IndexOutOfBoundsException after refilling RecyclerView with AsyncTask and notifyItemInserted() - java

I have a RecyclerView that I fill with data using an AsyncTask. When I clear the List with clear() and mAdapter.notifyDataSetChanged() and then try to fill it again with the AsyncTask, I get this exception:
java.lang.IndexOutOfBoundsException: Inconsistency detected. Invalid view holder adapter position
Replacing notifyItemInserted() with notifyDataSetChanged() inside my AsyncTask solves the problem, but I don't think that's a good solution, and I'd like to understand why the first method doesn't work.
My AsyncTask doInBackground() method:
#Override
protected Void doInBackground(Void... voids) {
for (int i = 0; i < 100; i++) {
mDataContainer.addItem(i);
publishProgress(i);
}
return null;
}
and my AsyncTask onProgressUpdate() method:
#Override
protected void onProgressUpdate(Integer... values) {
mAdapter.notifyItemInserted(values[0]);
super.onProgressUpdate(values);
}
I hope someone can help me with this. Thanks in advance.
Edit:
Here is the adapter:
private class MyAdapter extends RecyclerView.Adapter<MyAdapter.MyViewHolder> {
private List<Item> mItems;
private int selectedPos = RecyclerView.NO_POSITION;
private class MyViewHolder extends RecyclerView.ViewHolder implements View.OnClickListener {
private Item mItem;
private TextView mNameTextView;
private TextView mMembersTextView;
MyViewHolder(View view) {
super(view);
itemView.setOnClickListener(this);
mNameTextView = itemView.findViewById(R.id.item_name);
mMembersTextView = itemView.findViewById(R.id.item_members);
}
#Override
public void onClick(View view) {
notifyItemChanged(selectedPos);
selectedPos = getLayoutPosition();
notifyItemChanged(selectedPos);
mOnItemSelectedListener.onItemSelected(mItem);
}
}
MyAdapter(List<Item> items) {
mItems = items;
}
#NonNull
#Override
public MyViewHolder onCreateViewHolder(#NonNull ViewGroup parent, int viewType) {
View view = LayoutInflater.from(getActivity())
.inflate(R.layout.list_item, parent, false);
return new MyViewHolder(view);
}
#Override
public void onBindViewHolder(MyViewHolder myViewHolder, int position) {
Item item = mItems.get(position);
myViewHolder.mItem = item;
myView.mNameTextView.setText(item.getName());
myView.mMembersTextView.setText(String.format(Locale.US,"%d/50", item.getMembers()));
if (!mIsInit) {
// select item that was selected before orientation change
if (selectedPos != RecyclerView.NO_POSITION) {
Item selectedItem = mItems.get(selectedPos);
mOnItemSelectedListener.onItemSelected(selectedItem);
// else select item 0 as default on landscape mode
} else if (getResources().getConfiguration().orientation == Configuration.ORIENTATION_LANDSCAPE){
selectedPos = 0;
mOnItemSelectedListener.onItemSelected(MyViewHolder.mItem);
}
mIsInit = true;
}
if (getResources().getConfiguration().orientation == Configuration.ORIENTATION_LANDSCAPE) {
myViewHolder.itemView.setSelected(selectedPos == position);
}
}
#Override
public int getItemCount() {
return mItems.size();
}
}

You should only be adding items to the adapter (and notifying) from the UI thread. Changing the adapter's backing data from the background and then notifying on the UI thread later via onProgressUpdate is quite likely to result in race conditions. In this case, mDataContainer.addItem(i); should be moved to onProgressUpdate before you notify the adapter.
It's hard to confirm if that's the only issue without seeing the definition of mDataContainer, but fixing the synchronization here would definitely be the first step toward fixing this.

I think it is better if you add all value in background and notify adapter after all data inserted"
#Override
protected Void doInBackground(Void... voids) {
for (int i = 0; i < 100; i++) {
mDataContainer.addItem(i);
}
return null;
}
#Override
protected void onPostExecute(Long result) {
mAdapter.notifyItemRangeInserted(0, 100);
}
I also suggest you should not use AsyncTask because it is deprecated on Android 11

Related

Why are we using interface to set onClick for View Holders for recycler view in the MainActivity

I am going through a tutorial which is using the Recycler View to display a list of weather for each day for a week.
There are two classes which I am confused in:
ForecastAdapter and MainActivity
Here is the code for the above two classes:
ForecastAdapter.java
public class ForecastAdapter extends RecyclerView.Adapter<ForecastAdapter.ForecastAdapterViewHolder> {
private String[] mWeatherData;
final private ForecastAdapterOnClickListener mClickHandler;
//Why do we need to create an interface here.
public interface ForecastAdapterOnClickListener {
void onClick(String weatherForDay);
}
public ForecastAdapter(ForecastAdapterOnClickListener forecastAdapterOnClickListener) {
mClickHandler = forecastAdapterOnClickListener;
}
public class ForecastAdapterViewHolder extends RecyclerView.ViewHolder implements View.OnClickListener{
public final TextView mWeatherTextView;
public ForecastAdapterViewHolder(View view) {
super(view);
mWeatherTextView = (TextView) view.findViewById(R.id.tv_weather_data);
view.setOnClickListener(this);
}
#Override
public void onClick(View v) {
int adapterPosition = getAdapterPosition();
String weatherForDay = mWeatherData[adapterPosition];
//Why are we calling onClick from mClickHandler here. Why can't we just display Toast here.
mClickHandler.onClick(weatherForDay);
/*Why can't we just display the Toast from here like this:
Toast.makeText(v.getContext(), weatherForDay, Toast.LENGTH_SHORT).show()
*/
}
}
#Override
public ForecastAdapterViewHolder onCreateViewHolder(ViewGroup viewGroup, int viewType) {
Context context = viewGroup.getContext();
int layoutIdForListItem = R.layout.forecast_list_item;
LayoutInflater inflater = LayoutInflater.from(context);
boolean shouldAttachToParentImmediately = false;
View view = inflater.inflate(layoutIdForListItem, viewGroup, shouldAttachToParentImmediately);
return new ForecastAdapterViewHolder(view);
}
#Override
public void onBindViewHolder(ForecastAdapterViewHolder forecastAdapterViewHolder, int position) {
String weatherForThisDay = mWeatherData[position];
forecastAdapterViewHolder.mWeatherTextView.setText(weatherForThisDay);
}
#Override
public int getItemCount() {
if (null == mWeatherData) return 0;
return mWeatherData.length;
}
public void setWeatherData(String[] weatherData) {
mWeatherData = weatherData;
notifyDataSetChanged();
}
}
MainActivity.java
//Why are implementing ForecastAdapterOnClickListener here?
public class MainActivity extends AppCompatActivity implements ForecastAdapter.ForecastAdapterOnClickListener{
private RecyclerView mRecyclerView;
private ForecastAdapter mForecastAdapter;
private TextView mErrorMessageDisplay;
private ProgressBar mLoadingIndicator;
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_forecast);
mRecyclerView = (RecyclerView) findViewById(R.id.recyclerview_forecast);
mErrorMessageDisplay = (TextView) findViewById(R.id.tv_error_message_display);
LinearLayoutManager layoutManager
= new LinearLayoutManager(this, LinearLayoutManager.VERTICAL, false);
mRecyclerView.setLayoutManager(layoutManager);
mRecyclerView.setHasFixedSize(true);
mForecastAdapter = new ForecastAdapter(this);
mRecyclerView.setAdapter(mForecastAdapter);
mLoadingIndicator = (ProgressBar) findViewById(R.id.pb_loading_indicator);
loadWeatherData();
}
private void loadWeatherData() {
showWeatherDataView();
String location = SunshinePreferences.getPreferredWeatherLocation(this);
new FetchWeatherTask().execute(location);
}
#Override
public void onClick(String weatherForDay) {
Context context = this;
Toast.makeText(context, weatherForDay, Toast.LENGTH_SHORT)
.show();
}
private void showWeatherDataView() {
mErrorMessageDisplay.setVisibility(View.INVISIBLE);
mRecyclerView.setVisibility(View.VISIBLE);
}
private void showErrorMessage() {
mRecyclerView.setVisibility(View.INVISIBLE);
mErrorMessageDisplay.setVisibility(View.VISIBLE);
}
public class FetchWeatherTask extends AsyncTask<String, Void, String[]> {
#Override
protected void onPreExecute() {
super.onPreExecute();
mLoadingIndicator.setVisibility(View.VISIBLE);
}
#Override
protected String[] doInBackground(String... params) {
if (params.length == 0) {
return null;
}
String location = params[0];
URL weatherRequestUrl = NetworkUtils.buildUrl(location);
try {
String jsonWeatherResponse = NetworkUtils
.getResponseFromHttpUrl(weatherRequestUrl);
String[] simpleJsonWeatherData = OpenWeatherJsonUtils
.getSimpleWeatherStringsFromJson(MainActivity.this, jsonWeatherResponse);
return simpleJsonWeatherData;
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
#Override
protected void onPostExecute(String[] weatherData) {
mLoadingIndicator.setVisibility(View.INVISIBLE);
if (weatherData != null) {
showWeatherDataView();
mForecastAdapter.setWeatherData(weatherData);
} else {
showErrorMessage();
}
}
}
#Override
public boolean onCreateOptionsMenu(Menu menu) {
MenuInflater inflater = getMenuInflater();
inflater.inflate(R.menu.forecast, menu);
return true;
}
#Override
public boolean onOptionsItemSelected(MenuItem item) {
int id = item.getItemId();
if (id == R.id.action_refresh) {
mForecastAdapter.setWeatherData(null);
loadWeatherData();
return true;
}
return super.onOptionsItemSelected(item);
}
}
The adapter, view holder and recycler view is working as expected. We are now supposed to implement Click Handling on the rows of the recycler view. Whenever a particular row is clicked, we are supposed to display a toast.
As you can see, we are implementing OnClickListener in the ForecastAdapterViewHolder and in the onClick function we are calling the onClick of the interface "ForecastAdapterOnClickListener".
In the MainActivity.java, we are implementing this "ForecastAdapterOnClickListener" and then displaying the toast.
Why can't we just display the toast in the onClick that is defined for the "ForecastAdapterViewHolder" class. I have tried it and it works. What is the point of doing what is being done in the code?
Is there some advantage in setting the click listener like that?
Because you'll have to display information afterwards and isn't role of ViewHolder neither Adapter. Activity/fragment must do that.
It's to keep your code organized.

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.

Find how many RecyclerView items fit screen before creation

Basically, I'm trying to programatically find out how many items are going to fit in a RecyclerView (and be visible to user, of course) in order to determine how many of them to fetch from cache.
I'm using a LinearLayoutManager.
Also, I'm aware of the LinearLayoutManager method findLastVisibleItemPosition, but obviously it's useless in this case since we're talking on before-initialization time, not after (so it returns -1).
Tried reading the docs or thinking on a creative but efficient idea, but I got nothing on my mind.
Any ideas?
This sounds actually pretty interesting, but only works if your height (vertical scroll) or width (horizontal scroll) is fixed, meaning no wrap_content.
No sample code and nothing tested here:
Create an Adapter with a setter for getCount that gets returned in case your data-source is null/empty
in getCount return at least 1 if your data is empty/null
make sure onBindViewHolder() can handle empty/non-existent data
add a OnChildAttachStateChangeListener to your RecyclerView, everytime the listener gets called, use the view to view.post(new Runnable() {...increase adapters getCount...adapter.notifyItemInserted()} (that runnable is necessary to avoid crash+burn)
OnChildAttachStateChangeListener gets called again >>> compare getCount and findLastVisibleItemPosition. If getCount > findLastVisibleItemPosition + 1 remove that listener. The number of fixed-size views fitting into ListView is findLastVisibleItemPosition + 1
Get your data and set it into you adapter, call notifyDataSetChanged
make sure getCount returns the data-source length from now on.
you could hide the listview behind a loadingscreen, or you can set the child views to invisible in onBindViewHolder
EDIT:
Create an Adapter which returns a ridiculous high count when no data is set and make sure it handles missing data correctly in onBindViewHolder
Extend LinearLayoutManager and Override onLayoutChildren() after the super call if getItemCount() > getChildCount() getChildCount() is the number of Views that would be visible in your RecyclerView
MainActivity.class
public class MainActivity extends AppCompatActivity {
private PreCountingAdapter mAdapter;
private RecyclerView mRecyclerView;
private PreCountLinearLayoutManager mPreCountLayoutManager;
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mRecyclerView = (RecyclerView) findViewById(R.id.recycler_view);
mPreCountLayoutManager = new PreCountLinearLayoutManager(this,
LinearLayoutManager.VERTICAL, false);
mPreCountLayoutManager.setListener(new PreCountLinearLayoutManager.OnPreCountedListener() {
#Override
public void onPreCounted(int count) {
mPreCountLayoutManager.setListener(null);
loadData(count);
}
});
mRecyclerView.setLayoutManager(mPreCountLayoutManager);
mAdapter = new PreCountingAdapter();
mRecyclerView.setAdapter(mAdapter);
}
private void loadData(final int visibleItemCount) {
// load data here, probably asynchronously,
// for simplicity just an String Array with size visibleItemCount
final List<String> data = new ArrayList<>();
for (int i = 0; i < visibleItemCount; i++) {
data.add(String.format("child number #%d", i));
}
mRecyclerView.post(new Runnable() {
#Override
public void run() {
mAdapter.swapData(data);
}
});
}
#Override
protected void onDestroy() {
mPreCountLayoutManager.setListener(null);
super.onDestroy();
}
}
PreCountLinearLayoutManager.class
public class PreCountLinearLayoutManager extends LinearLayoutManager {
private OnPreCountedListener mListener;
public interface OnPreCountedListener {
void onPreCounted(int count);
}
public PreCountLinearLayoutManager(Context context, int orientation, boolean reverseLayout) {
super(context, orientation, reverseLayout);
}
#Override
public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
super.onLayoutChildren(recycler, state);
if (getItemCount() > getChildCount()) {
if (mListener != null) {
mListener.onPreCounted(getChildCount());
}
}
}
public void setListener(OnPreCountedListener listener) {
mListener = listener;
}
}
PreCountingAdapter.class
public class PreCountingAdapter extends RecyclerView.Adapter<PreCountingAdapter.ViewHolder> {
private List<String> mData;
public void swapData(List<String> data) {
mData = data;
notifyDataSetChanged();
}
public class ViewHolder extends RecyclerView.ViewHolder {
View mItemView;
TextView mTextView;
public ViewHolder(View itemView) {
super(itemView);
mTextView = (TextView) itemView.findViewById(R.id.text_view);
mItemView = itemView;
}
}
#Override
public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
LayoutInflater inflater = LayoutInflater.from(parent.getContext());
return new ViewHolder(inflater.inflate(R.layout.recycler_child, parent, false));
}
#Override
public void onBindViewHolder(ViewHolder holder, int position) {
if (mData == null) {
// we are in precounting stage
holder.mItemView.setVisibility(View.INVISIBLE);
} else {
String item = mData.get(position);
holder.mItemView.setVisibility(View.VISIBLE);
holder.mTextView.setText(item);
}
}
#Override
public int getItemCount() {
return mData == null ? Integer.MAX_VALUE : mData.size();
}
}

Add listview item to the top of a Android Pull to refresh ListView

I am using the RefreshableListView (Pull to refresh) from here .But I am using a Custom ListAdapter and not the ArrayAdapter. It working all fine but the only issue is that it is adding new items to the bottom of the list while I want new items to be displayed on top. .
I have referred to a lot of articles on the net addressing similar issues but have not found a solution for my issue. It will be great if anybody could point out where I am going wrong.
Here are the code snippets:
public void onCreate(Bundle savedInstanceState) {
final CustomAdapter adapter = new CustomAdapter(this, R.layout.mylist, mItems);
mListView = (RefreshableListView) findViewById(R.id.listview);
mListView.setAdapter(adapter);
// Callback to refresh the list
mListView.setOnRefreshListener(new OnRefreshListener() {
#Override
public void onRefresh(RefreshableListView listView) {
ivFooter.setImageResource(R.drawable.back);
new NewDataTask().execute();
}
});
}
and the AsyncTask
private class NewDataTask extends AsyncTask<Void, Void, String> {
#Override
protected String doInBackground(Void... params) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
}
String retVal = myArrayList.get(i);
if (i < myArrayList.size() - 1) {
i++;
} else {
i = 0;
}
return retVal;
}
#Override
protected void onPostExecute(String result) {
mItems.add(0, result);
mListView.completeRefreshing();
super.onPostExecute(result);
}
}
and Custom Adapter class
class CustomAdapter extends BaseAdapter {
public int getCount() {
return mItems.size();
}
public Object getItem(int position) {
return mItems.get(position);
}
public long getItemId(int position) {
return position;
}
public boolean areAllItemsEnabled() {
return false;
}
public boolean isEnabled(int position) {
return false;
}
public View getView(int position, View convertView, ViewGroup parent) {
View v = convertView;
if (v == null) {
LayoutInflater vi = (LayoutInflater) getSystemService(Context.LAYOUT_INFLATER_SERVICE);
v = vi.inflate(R.layout.mylist, null);
}
String quote = myArrayList.get(position);
if (quote != null) {
TextView tt = (TextView) v.findViewById(R.id.tvQuote);
ImageView iv = (ImageView) v.findViewById(R.id.ivPicture);
if (tt != null) {
tt.setText(quote);
}
if (iv != null) {
//Setting image
}
return v;
}
'mItems' is the original list with one item and I am adding 'myArrayList' to it.
Like I said its working fine but adding elements (on refresh) one by one at the bottom ..while I want the new elements to be added on top.
Any inputs would be of great help.
Thanks in advance.
Replace String quote = myArrayList.get(position); with String quote = RefreshableListViewActivity.this.mItems.get(position); and it works as expected.
Thanks to #Dante for helping me out.

Android java.lang.IllegalStateException on ListView

I use ListActivity with SpecialAdapter that show some log information.
I do not use background thread but I get an error when scroll list:
java.lang.IllegalStateException: The content of the adapter has changed but ListView did not receive a notification. Make sure the content of your adapter is not modified from a background thread, but only from the UI thread. [in ListView(16908298, class android.widget.ListView) with Adapter(class ru.FoxGSM.ui.DebugActivity$SpecialAdapter)]
Please, help me.
(FoxLog is static class that get log information, perhaps from another thread. But in list a want show FoxLog snapshot)
public class DebugActivity extends ListActivity {
private class SpecialAdapter extends BaseAdapter {
private LayoutInflater mInflater;
public SpecialAdapter(Context context) {
super();
// Cache the LayoutInflate to avoid asking for a new one each time.
mInflater = LayoutInflater.from(context);
}
public int getCount() {
return FoxLog.count();
}
public long getItemId(int index) {
return index;
}
public Object getItem(int index) {
return FoxLog.get(FoxLog.count()-index-1);
}
// Get the type of View
public View getView(int position, View convertView, ViewGroup parent) {
TextView text;
if (convertView == null) {
convertView = mInflater.inflate(R.layout.debug_list, null);
text = (TextView)convertView.findViewById(R.id.text);
convertView.setTag(text);
} else
text = (TextView) convertView.getTag();
String s = (String) getItem(position);
if (s==null)
return convertView;
text.setText(s);
boolean isError = false;
if (s!=null && s.length()>0) {
String prefix = s.substring(0, 1);
if (prefix.equals("E") || prefix.equals("W"))
isError = true;
}
if (isError)
text.setBackgroundResource(R.color.red);
else
text.setBackgroundDrawable(null);
return convertView;
}
}
private void RefreshData() {
FoxLog.ReInit(this);
SpecialAdapter adapter = (SpecialAdapter)this.getListAdapter();
adapter.notifyDataSetChanged();
}
#Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.debug);
FoxLog.ReInit(this);
SpecialAdapter adapter = new SpecialAdapter(this);
setListAdapter(adapter);
Button mExit = (Button)findViewById(R.id.exit);
mExit.setOnClickListener(new View.OnClickListener() {
public void onClick(View v) {
finish();
}
});
}
}

Categories