Skip to content

Instantly share code, notes, and snippets.

Embed
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.
}
}
@acappelli

This comment has been minimized.

Copy link

acappelli commented Nov 4, 2014

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

@blabadi

This comment has been minimized.

Copy link

blabadi commented Nov 6, 2014

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

@lampione

This comment has been minimized.

Copy link

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,

@IgorGanapolsky

This comment has been minimized.

Copy link

IgorGanapolsky commented Dec 11, 2014

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?

@IgorGanapolsky

This comment has been minimized.

Copy link

IgorGanapolsky commented Dec 12, 2014

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

@PPartisan

This comment has been minimized.

Copy link

PPartisan commented Mar 5, 2015

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

@RadicalMonkey

This comment has been minimized.

Copy link

RadicalMonkey commented Apr 13, 2015

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?

@SteveWaring

This comment has been minimized.

Copy link

SteveWaring commented May 5, 2015

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.

@romulus85

This comment has been minimized.

Copy link

romulus85 commented Jun 5, 2015

I have the same problem as RadicalMonkey.

@JV-TM

This comment has been minimized.

Copy link

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;
}
@Knight704

This comment has been minimized.

Copy link

Knight704 commented Jun 24, 2015

@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);
    }
@slidenerd

This comment has been minimized.

Copy link

slidenerd commented Jul 18, 2015

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?

@yaseen2591

This comment has been minimized.

Copy link

yaseen2591 commented Sep 18, 2015

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

@PRosenb

This comment has been minimized.

Copy link

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.

@alonely1990

This comment has been minimized.

Copy link

alonely1990 commented Oct 15, 2015

@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;
    }

}

@tuanchauict

This comment has been minimized.

Copy link

tuanchauict commented Nov 4, 2015

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

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

@tonyshkurenko

This comment has been minimized.

Copy link

tonyshkurenko commented Nov 4, 2015

How do you close cursor? After setting an adapter?

@jordan1997

This comment has been minimized.

Copy link

jordan1997 commented Jan 15, 2016

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?

@luna-vulpo

This comment has been minimized.

Copy link

luna-vulpo commented Feb 11, 2016

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.

@prateeksamaiya

This comment has been minimized.

Copy link

prateeksamaiya commented Jun 3, 2016

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

@omaakaar

This comment has been minimized.

Copy link

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.

@conacry

This comment has been minimized.

Copy link

conacry commented Jul 12, 2016

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

@pawarlalit29

This comment has been minimized.

Copy link

pawarlalit29 commented Nov 7, 2016

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

@mhkubaid

This comment has been minimized.

Copy link

mhkubaid commented Nov 12, 2016

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

@zishanj

This comment has been minimized.

Copy link

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.

@Emeritus-DarranKelinske

This comment has been minimized.

Copy link

Emeritus-DarranKelinske commented Apr 22, 2017

thank you @lonely1990

@AHMAD8088

This comment has been minimized.

Copy link

AHMAD8088 commented Apr 28, 2017

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

@samuel2629

This comment has been minimized.

Copy link

samuel2629 commented May 1, 2017

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!

@vishusrvstv

This comment has been minimized.

Copy link

vishusrvstv commented Jun 22, 2017

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

This comment has been minimized.

Copy link

anky21 commented Jun 22, 2017

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

@omarhammad

This comment has been minimized.

Copy link

omarhammad commented Jul 31, 2017

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

@PritishSawant

This comment has been minimized.

Copy link

PritishSawant commented Aug 4, 2017

How to sort items in ascending and descending order?

@jackz314

This comment has been minimized.

Copy link

jackz314 commented Sep 14, 2017

I still don't get the last part, where you used the holder.mTextView.setText part to set text, MyListItem.fromCursor(cursor); to get the data, but how did you get the each data from the cursor, can you explain better? thanks

@teapack

This comment has been minimized.

Copy link

teapack commented Oct 29, 2017

I'm confused with this. I added my layout file for list item to the onCreateViewHolder(ViewGroup parent, int viewType) method.
Then I instantiated the adapter by calling adapter = new MyListCursorAdapter(getContext(), cursor);,
instantiated my RecyclerView: recyclerView = v.findViewById(R.id.my_recycler_view);
and then I set the adapter to the recycler view: recyclerView.setAdapter(adapter);.
What am I missing? The RecyclerView with items still doesn't show up. I added a log to the fromCursor(Cursor cursor) method in MyListItem class to see if the method is getting triggered, and no logs appear - so the fromCursor method is never called. Should I somewhere explicitly call e.g. the onBindViewHolder(ViewHolder viewHolder, Cursor cursor) method? What would be the ViewHolder - my list item?
Actually, a simple working example would be extremely helpful. I just need a guide how to "connect" the solution to my app. For comparison - when I tried using ListView and SimpleCursorAdapter to display data from Cursor, I only had to instantiate the SimpleCursorAdapter, the ListView and set the adapter to the ListView - and that was it, it was working. This solution requires me to do something more it seems, and I just can't wrap my head around it.

Edit: Nevermind, I forgot to set layout manager to the RecyclerView. I got it working now, thanks.

@mzdhr

This comment has been minimized.

Copy link

mzdhr commented Dec 11, 2017

work flawlessly

@8enet

This comment has been minimized.

Copy link

8enet commented Dec 15, 2017

android.support.v7.util.AsyncListUtil can also be simple to implement.

@AntonioVitiello

This comment has been minimized.

Copy link

AntonioVitiello commented Jul 7, 2018

Hello, first of all many thanks for sharing this code! Can you help me with a problem please? I have an Activity with a RecyclerView that uses an AsyncTaskLoader to load data from the database and update MyListCursorAdapter with cursor. All works fine until I delete a row from the database using ContentProvider, the adapter don't refresh and the RecyclerView that continue to show the item I deleted with a ContextMenu.
Any idea?

@AliexieievAndrew

This comment has been minimized.

Copy link

AliexieievAndrew commented Sep 8, 2018

@skyfishjy
thanks for the example
@PRosenb
Thanks for the help. Now everything works fine ))

@catiejo

This comment has been minimized.

Copy link

catiejo commented Nov 12, 2018

Thank you so much for this code! The example usage was also incredibly helpful.

@NickJian

This comment has been minimized.

Copy link

NickJian commented Apr 10, 2019

I wonder if we can notifyChanged just for certain range, maybe we can have a lastBindIdx and compare idx within 10 item. It will make the animation work properly.

@ideep

This comment has been minimized.

Copy link

ideep commented Apr 16, 2019

Thank you so much for this code!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
You can’t perform that action at this time.