I'm working on a project where I have a RecyclerView displaying items from a SQLiteDatabase. So I have build a custom adapter that uses a cursor instead of a list of data so I can update it straight from database queries. Additionally, I have a "placeholder" view that gets put into the recyclerView if the cursor is empty (no rows returned) that basically says "nothing here, add something by clicking +".
My issue is that when I add an item in that state (when the RecyclerView is initially empty) and call notifyDataSetChanged, the RecyclerView attempts to recycle the placeholder view instead of inflating the correct view item and I get a NullPointerException because I am attempting to update UI elements that were never initialized.
Here's what my adapter looks like now:
public class TextItemListAdapter extends RecyclerView.Adapter<TextItemListAdapter.ViewHolder> {
private List<TextItem> dataList;
private Cursor cursor;
private Context context;
private boolean empty = false;
private TextItemDataLayer dataLayer;
/**
* Constructor
* @param c Context
* @param curs The cursor over the data (Must be a cursor over the TextItems table)
*/
public TextItemListAdapter(Context c, Cursor curs) {
context = c;
dataLayer = new TextItemDataLayer(c);
dataLayer.openDB();
if (curs != null) {
switchCursor(curs);
}
empty = isEmpty(cursor);
}
/**
* ViewHolders populate the RecyclerView. Each item in the list is contained in a VewHolder.
*/
public class ViewHolder extends RecyclerView.ViewHolder implements View.OnClickListener {
View parent;
TextItem textItem;
TextView location;
TextView text;
TextView time;
/**
* Create the viewholder for the item
* @param itemView the view contained in the viewHolder
*/
public ViewHolder(View itemView) {
super(itemView);
this.parent = itemView;
if (!empty) {
itemView.setClickable(true);
itemView.setOnClickListener(this);
}
location = (TextView) itemView.findViewById(R.id.textItem_item_title);
text = (TextView) itemView.findViewById(R.id.textItem_item_text);
time = (TextView) itemView.findViewById(R.id.textItem_item_time);
}
/**
* Handle a click event on an TextItem
* @param v
*/
@Override
public void onClick(View v) {
Intent i = new Intent(context, EditTextItemActivity.class);
i.putExtra("textItem", textItem);
context.startActivity(i);
}
}
/**
* Creation of the viewholder
* @param parent
* @param i
* @return
*/
@Override
public ViewHolder onCreateViewHolder(ViewGroup parent, int i) {
View view;
if (empty) {
view = LayoutInflater.from(parent.getContext()).inflate(R.layout.text_item_list_item_empty, parent, false);
} else {
view = LayoutInflater.from(parent.getContext()).inflate(R.layout.text_item_list_item, parent, false);
}
ViewHolder vh = new ViewHolder(view);
return vh;
}
/**
* When the viewholder is bound to the recyclerview, initialize things here
* @param holder
* @param position
*/
@Override
public void onBindViewHolder(ViewHolder holder, int position) {
if (!empty) {
if (!cursor.moveToPosition(position)) {
throw new IllegalStateException("couldn't move cursor to position " + position);
}
holder.textItem = dataLayer.buildTextItem(cursor);
holder.location.setText(holder.textItem.getLocation().getName());
holder.text.setText(holder.textItem.getMessage());
holder.time.setText(holder.textItem.getTime());
}
}
/**
* Get the number of items in the dataset
*
* If the dataset is empty, return 1 so we can imflate the "empty list" viewHolder
* @return
*/
@Override
public int getItemCount() {
if (empty) {
return 1;
}
return cursor.getCount();
}
/**
* switch the existing cursor with a new one
* @param c
*/
public void switchCursor(Cursor c) {
// helper method in TextItemDataLayer that compares two cursors' contents
if (TextItemDataLayer.cursorsEqual(cursor, c)) {
c.close();
return;
}
if (cursor != null) {
cursor.close();
}
cursor = c;
cursor.moveToFirst();
empty = isEmpty(cursor);
notifyDataSetChanged();
}
/**
* Check to see if the cursor is empty
*
* @param c the cursor to check
* @return
*/
private boolean isEmpty(Cursor c) {
return (c == null || c.getCount() == 0);
}
}
Some things I've tried that I haven't gotten to work:
notifyItemRemoved(0);
if the list changes from empty to not emptyChanging switchCursor to compare the two cursors and call either notifyDataSetChanged() or notifyItemRemoved(0) like this
// helper method in TextItemDataLayer that compares two cursors' contents if (OterDataLayer.cursorsEqual(cursor, c)) { c.close(); return; } if (!isEmpty(c) && isEmpty(cursor)) { empty = false; notifyItemRemoved(0); // or notifyDataSetChanged(); } if (cursor != null) { cursor.close(); } cursor = c; cursor.moveToFirst(); empty = isEmpty(cursor); notifyDataSetChanged();
I've also considered having the empty placeholder view not be in the RecyclerView but instead be in the parent view to avoid the issue altogether, although I think it's better in terms of the overall structure of the app for it to be handled in the RecyclerView.