0

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 empty
  • Changing 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.

JoeBruzek
  • 509
  • 4
  • 16

1 Answers1

2

Your problem is simply in understanding how RecyclerView.Adapter works. Please have a look at this for clarification pertaining to your problem.

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

Your are right about that, and it's happening because you did not override getItemViewType (int position), and the RecyclerView has no way to know that there are actually two different ViewHolder types or states.

I suggest following the above linked answer, refactor your code to provide two different ViewHolders, and then implement getItemViewType (int position). Something along these lines:

 @Override
    public int getItemViewType(int position) {

        if(empty)
            return EMPTY_VIEW_TYPE_CODE;

        return REGULAR_VIEW_TYPE_CODE;
    }
Community
  • 1
  • 1
Ari
  • 3,086
  • 1
  • 19
  • 27