Create a gist now

Instantly share code, notes, and snippets.

What would you like to do?
CursorRecyclerViewAdapter
/*
* Copyright (C) 2014 skyfish.jy@gmail.com
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
import android.content.Context;
import android.database.Cursor;
import android.database.DataSetObserver;
import android.support.v7.widget.RecyclerView;
/**
* Created by skyfishjy on 10/31/14.
*/
public abstract class CursorRecyclerViewAdapter<VH extends RecyclerView.ViewHolder> extends RecyclerView.Adapter<VH> {
private Context mContext;
private Cursor mCursor;
private boolean mDataValid;
private int mRowIdColumn;
private DataSetObserver mDataSetObserver;
public CursorRecyclerViewAdapter(Context context, Cursor cursor) {
mContext = context;
mCursor = cursor;
mDataValid = cursor != null;
mRowIdColumn = mDataValid ? mCursor.getColumnIndex("_id") : -1;
mDataSetObserver = new NotifyingDataSetObserver();
if (mCursor != null) {
mCursor.registerDataSetObserver(mDataSetObserver);
}
}
public Cursor getCursor() {
return mCursor;
}
@Override
public int getItemCount() {
if (mDataValid && mCursor != null) {
return mCursor.getCount();
}
return 0;
}
@Override
public long getItemId(int position) {
if (mDataValid && mCursor != null && mCursor.moveToPosition(position)) {
return mCursor.getLong(mRowIdColumn);
}
return 0;
}
@Override
public void setHasStableIds(boolean hasStableIds) {
super.setHasStableIds(true);
}
public abstract void onBindViewHolder(VH viewHolder, Cursor cursor);
@Override
public void onBindViewHolder(VH viewHolder, int position) {
if (!mDataValid) {
throw new IllegalStateException("this should only be called when the cursor is valid");
}
if (!mCursor.moveToPosition(position)) {
throw new IllegalStateException("couldn't move cursor to position " + position);
}
onBindViewHolder(viewHolder, mCursor);
}
/**
* Change the underlying cursor to a new cursor. If there is an existing cursor it will be
* closed.
*/
public void changeCursor(Cursor cursor) {
Cursor old = swapCursor(cursor);
if (old != null) {
old.close();
}
}
/**
* Swap in a new Cursor, returning the old Cursor. Unlike
* {@link #changeCursor(Cursor)}, the returned old Cursor is <em>not</em>
* closed.
*/
public Cursor swapCursor(Cursor newCursor) {
if (newCursor == mCursor) {
return null;
}
final Cursor oldCursor = mCursor;
if (oldCursor != null && mDataSetObserver != null) {
oldCursor.unregisterDataSetObserver(mDataSetObserver);
}
mCursor = newCursor;
if (mCursor != null) {
if (mDataSetObserver != null) {
mCursor.registerDataSetObserver(mDataSetObserver);
}
mRowIdColumn = newCursor.getColumnIndexOrThrow("_id");
mDataValid = true;
notifyDataSetChanged();
} else {
mRowIdColumn = -1;
mDataValid = false;
notifyDataSetChanged();
//There is no notifyDataSetInvalidated() method in RecyclerView.Adapter
}
return oldCursor;
}
private class NotifyingDataSetObserver extends DataSetObserver {
@Override
public void onChanged() {
super.onChanged();
mDataValid = true;
notifyDataSetChanged();
}
@Override
public void onInvalidated() {
super.onInvalidated();
mDataValid = false;
notifyDataSetChanged();
//There is no notifyDataSetInvalidated() method in RecyclerView.Adapter
}
}
}
import android.content.Context;
import android.database.Cursor;
import android.support.v7.widget.RecyclerView;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageView;
import android.widget.TextView;
import java.util.List;
/**
* Created by skyfishjy on 10/31/14.
*/
public class MyListCursorAdapter extends CursorRecyclerViewAdapter<MyListCursorAdapter.ViewHolder>{
public MyListCursorAdapter(Context context,Cursor cursor){
super(context,cursor);
}
public static class ViewHolder extends RecyclerView.ViewHolder {
public TextView mTextView;
public ViewHolder(View view) {
super(view);
mTextView = view.findViewById(R.id.text);
}
}
@Override
public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
View itemView = LayoutInflater.from(parent.getContext())
.inflate(R.layout.my_view, parent, false);
ViewHolder vh = new ViewHolder(itemView);
return vh;
}
@Override
public void onBindViewHolder(ViewHolder viewHolder, Cursor cursor) {
MyListItem myListItem = MyListItem.fromCursor(cursor);
viewHolder.mTextView.setText(myListItem.getName());
}
}
public class MyListItem{
private String name;
public void setName(String name){
this.name=name;
}
public String getName(){
return name;
}
public static MyListItem fromCursor(Cursor cursor) {
//TODO return your MyListItem from cursor.
}
}

Hi! Can you help me to update my adapter with yours?

blabadi commented Nov 6, 2014

Thanks alot for this class, I have used it and works correctly :)

lampione commented Dec 6, 2014

Hi! What should I return from MyListItem? Cause I can't get my cursor because it's in a Database class and I can't get reference to it,

What do you mean by "return your MyListItem from cursor"? Do I have to load the data using a Loader, and then replicating the data into a ListView?

Why are you extending only DataSetObserver, and not ContentObserver? A lot of people use ContentProviders with LoaderManagers in their code...

I'm confused by the purpose of your MyListItem class. Would you be able to explain that please?

Thanks for this code, it came in really helpful! I do have a query though. The default animations for adding and removing items don't seem to play.
When I delete an item from my database using a content provider the RecyclerView gets notified of the change and removes the view but there is no animation.

Do you know what the issue could be?

I would be very interested if you provided a version of this to populate a RecyclerView from a ContentProvider. I would like to replace a ListView populated by a LoaderManager with a RecyclerView.

I have the same problem as RadicalMonkey.

JV-TM commented Jun 10, 2015

Hello, I need multiple viewTypes, unfortunatelly, current implementation needs to traverse cursor and parse data every time a getViewType() is called ... Any idea how to solve this? Anybody faced this situation before? How did you solved it? Thanks.

@Override
public int getItemViewType(int position) {
    int result = ID_VIEW_HOLDER;
    if (null != getCursor() && getCursor().moveToPosition(position)) {
        if (-1 != getCursor().getColumnIndex(Order.COLUMN_STATUS)) {
            String statusString = getCursor().getString(getCursor().getColumnIndex(Order.COLUMN_STATUS));
            try {
                OrderStateEnum orderState = OrderStateEnum.valueOf(statusString);
                if (OrderStateEnum.ON_WAY == orderState) {
                    result = ID_VIEW_HOLDER_ACTUAL;
                }
            } catch (Exception e) {
                Log.w(TAG, "Fail to convert OrderStateEnum by name: " + statusString, e);
            }
        }
    }

    return result;
}

@RadicalMonkey, @romulus85
This happens due to notifyDataSetChanged() usage in swapCursor method instead of specific notifications like notifyItemInserted, notifyItemDeleted, etc.

You could try to use this snippets for swapCursor method, but it works only for add animation, one should implement logic to find difference between two cursors and detect the deletion:

/**
     * Swap cursor overridden method to add animations when data changes
     * This method was copied from parent. Replaced notifyDataSetChanged in case of new data
     *
     * @param newCursor
     * @return
     */
    @Override
    public Cursor swapCursor(Cursor newCursor) {
        if (newCursor == mCursor) {
            return null;
        }
        Cursor oldCursor = mCursor;
        if (oldCursor != null) {
            if (mChangeObserver != null) {
                oldCursor.unregisterContentObserver(mChangeObserver);
            }
            if (mDataSetObserver != null) {
                oldCursor.unregisterDataSetObserver(mDataSetObserver);
            }
        }
        mCursor = newCursor;
        if (newCursor != null) {
            if (mChangeObserver != null) {
                newCursor.registerContentObserver(mChangeObserver);
            }
            if (mDataSetObserver != null) {
                newCursor.registerDataSetObserver(mDataSetObserver);
            }
            mRowIDColumn = newCursor.getColumnIndexOrThrow("_id");
            mDataValid = true;

            int newSize = newCursor.getCount();
            int oldSize = (oldCursor != null) ? oldCursor.getCount() : 0;
            notifyItemRangeInserted(oldSize, newSize - oldSize);
        } else {
            mRowIDColumn = -1;
            mDataValid = false;
            notifyDataSetChanged();
        }
        return oldCursor;
    }

    /**
     * This is helper method, which choose between two types of cursor swapping. The difference is about notify methods.
     * Parent implementation use {@link #notifyDataSetChanged()} to inform adapter about changes. This method do not animate any data changes.
     * This class implementation use {@link #notifyItemRangeInserted(int, int)} to indicate where
     * items were inserted to allow proper addition animation.
     *
     * @param newCursor The cursor to swap.
     * @param animate   Flag, that tells the adapter should this items be notified properly to make animation or not.
     * @return oldCursor.
     */
    public Cursor swapCursor(Cursor newCursor, boolean animate) {
        if (animate) {
            return swapCursor(newCursor);
        }
        return super.swapCursor(newCursor);
    }

You already checked mDataValid = cursor != null; in the constructor, so why's your condition saying this if (mDataValid && mCursor != null) { return mCursor.getCount();}, isnt that saying the same condition twice?

How can I extend this CursorRecyclerViewAdapter.java to RecyclerViews that has multiple Viewtypes??

PRosenb commented Oct 14, 2015

Instead of overwriting setHasStableIds() better call it in the constructor. As it is now, it's only called when the extending class calls setHasStableIds().
When setHasStableIds() is set to true and getItemId() returns unique ids, change animations are shown.

@yaseen2591: multi viewtypes.
In CursorRecyclerViewAdapter, you add:

@override
public int getItemViewType(int position) {
if (mDataValid && mCursor != null && mCursor.moveToPosition(position)) {
return mCursor.getInt(mCursor.getColumnIndex(NotificationTable.COL_TYPE));
}
return 0;
}

and In NotificationCursorAdapter, you modify:
public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
View itemView;

     switch (viewType) {
            case TYPE_1:

            break;
            case TYPE_2:

            break;
    }

}

I create an exteionsion that supports section for this cursor adapter. Tested for ListView only.

https://gist.github.com/tuanchauict/c6c1eda617523de224c5

How do you close cursor? After setting an adapter?

Hi for all.
Your can delete context from your adapter, because you don't use it in adapter, add RecyclerView don't want Context for work. Why you include Context in your code?

What should I do when I delete item form database by getWritableDatabase().delete() ? Now I have strange behavior: the onBindViewHolder is called with strange cursor.

please help me with the implementation part,can you give an example program how to implement this...?

omaakaar commented Jul 9, 2016

https://omaakaar.github.io/Hello-World/
need help to take benefit for and from github.
I am newbie and want to make money online.

Hi! Thank you very much for this class, he helped me a lot!

hey ,
what inside in fromCursor method
public static MyListItem fromCursor(Cursor cursor) {
//TODO return your MyListItem from cursor.
}

Why holding context? We can always get it from parent.getContext()

zishanj commented Nov 19, 2016

Any example on how to use it with SQLite db file in assets folder. I just want to retrieve it and display with RecyclerView using above CursorLoader.

thank you @lonely1990

zishanj do you found your answer abov question. i want in my project display data with RecyclerView using above CursorLoader from .db file asset folder

Hey,
Where do you call on click? Cause the implementation doesn't trigger it, and when I call it in the onBind method with the cursor, the if is wrong.. Any ideas? Thanks!

I am facing some problem with this code. I am populating some items in recyclerview using above cursor adapter. But after scrolling, i am getting wrong cursor position on click event. Sometimes it work fine, sometime mislead the cursor by 1 or 2 positions

anky21 commented Jun 22, 2017

Same problem here. Will try to search a solution tomorrow. Does anyone else know a solution?

omarhammad commented Jul 31, 2017 edited

public static MyListItem fromCursor(Cursor cursor) {
MyListItem listItem = new MyListItem();
listItem.setName(cursor.getString(cursor.getColumnIndex(STUDENT_NAME)));
return listItem;
}

How to sort items in ascending and descending order?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment