Skip to content

Instantly share code, notes, and snippets.

@slidenerd
Last active July 10, 2020 17:49
Show Gist options
  • Star 12 You must be signed in to star a gist
  • Fork 6 You must be signed in to fork a gist
  • Save slidenerd/af52fc80c6ca49aa7b1c to your computer and use it in GitHub Desktop.
Save slidenerd/af52fc80c6ca49aa7b1c to your computer and use it in GitHub Desktop.
A single adapter that supports Cursor + an optional header + optional footer
import android.database.Cursor;
import android.support.annotation.Nullable;
import android.support.v7.widget.RecyclerView;
import android.view.View;
import android.view.ViewGroup;
public abstract class RecyclerCursorAdapter<U, V extends RecyclerView.ViewHolder> extends RecyclerView.Adapter<RecyclerView.ViewHolder> implements OnSwipeListener {
//The number of headers to be displayed by default if child classes want a header
public static final int HEADER_COUNT = 1;
//The number of footers to be displated by default if child classes want a footer
public static final int FOOTER_COUNT = 1;
//A listener that is triggered when items are added or removed to the Recycler View.
protected OnChangedListener<U> onChangedListener;
private View mEmptyView;
//The Cursor object that will contain all the rows that you want to display inside the Recycler View
private Cursor mCursor;
//A variable indicating if the data contained in the Cursor above is valid
private boolean isValid;
//The index of the column containing _id of an SQLite database table from which you want to load data inside the Cursor above
private int indexColumnId;
public Cursor getCursor() {
return mCursor;
}
/**
* @param onChangedListener A class that wishes to get notified when items are added or removed from the Recycler View. When items are added, subclasses must take responsibility of controlling when to fire the 'onAdd' event and when items are swiped to the right in an LTR environment or to the left in an RTL environment, the 'onRemove' event is fired.
*/
public void setModifiedListener(OnChangedListener<U> onChangedListener) {
this.onChangedListener = onChangedListener;
}
/**
* If the data source is set for the first time, create a Cursor object from scratch else swap the existing Cursor object with a new cursor object. Depending on whether the Cursor has any rows at all, update the empty view if set.
*
* @param cursor containing the rows from your SQLite table that you want to display inside your RecyclerView
*/
public void setDataSource(Cursor cursor) {
if (mCursor == null) {
createCursor(cursor);
} else {
swapCursor(cursor);
}
toggleEmptyView();
}
/**
* Check if the data contained by the mCursor is valid and try to extract the value of the column index _id
* To indicate that your RecyclerView needs to refresh what it displays, call notifyDataSetChanged
*
* @param cursor the rows from a database table that you want to display inside your RecyclerView
*/
private void createCursor(Cursor cursor) {
mCursor = cursor;
isValid = (cursor != null);
indexColumnId = isValid ? this.mCursor.getColumnIndex(BaseColumns._ID) : -1;
notifyItemRangeInserted(getPositionForNotifyItemRangeXXX(), getCount());
}
/**
* Change the underlying mCursor to a new mCursor. If there is an existing mCursor it will be
* closed. If the new and old mCursor are same, do nothing, otherwise store the old and new cursors respectively. If the new mCursor is not null, notify that data has changed and mark data as valid. Swap in a new Cursor, returning the old Cursor. Unlike {@link #swapCursor(Cursor)}, the returned old Cursor is <em>not</em> closed.
*/
public void swapCursor(Cursor newCursor) {
if (newCursor == mCursor) {
return;
}
final Cursor oldCursor = mCursor;
mCursor = newCursor;
if (mCursor != null) {
indexColumnId = newCursor.getColumnIndexOrThrow("_id");
isValid = true;
notifyDataSetChanged();
} else {
indexColumnId = -1;
isValid = false;
notifyItemRangeRemoved(getPositionForNotifyItemRangeXXX(), getCount());
//There is no notifyDataSetInvalidated() method in RecyclerView.Adapter
}
if (oldCursor != null) {
oldCursor.close();
}
}
public int getPositionForNotifyItemRangeXXX() {
return hasHeader() ? HEADER_COUNT : 0;
}
public void setEmptyView(View emptyView) {
this.mEmptyView = emptyView;
}
public void toggleEmptyView() {
if (mEmptyView != null)
mEmptyView.setVisibility(getCount() == 0 ? View.VISIBLE : View.GONE);
}
public final int getCount() {
return isValid ? mCursor.getCount() : 0;
}
/**
* @param position of the current item within the RecyclerView whose item id we need to specify.
* @return the index of the column _id from the SQLite database table whose rows you are trying to display inside the RecyclerView, 0 if you dont have valid data in your Cursor
*/
@Override
public long getItemId(int position) {
if (isItem(position) && isValid && mCursor.moveToPosition(position)) {
return mCursor.getLong(indexColumnId);
}
return RecyclerView.NO_ID;
}
@Override
public void setHasStableIds(boolean hasStableIds) {
super.setHasStableIds(true);
}
@Override
public final int getItemCount() {
int itemCount = 0;
if (hasHeader()) {
itemCount += HEADER_COUNT;
}
if (hasFooter()) {
itemCount += FOOTER_COUNT;
}
itemCount += getCount();
return itemCount;
}
@Override
public final int getItemViewType(int position) {
if (isHeader(position)) {
return Type.HEADER.ordinal();
} else if (isFooter(position)) {
return Type.FOOTER.ordinal();
} else {
return Type.ITEM.ordinal();
}
}
public boolean isHeader(int position) {
if (hasHeader()) {
return position == 0 ? true : false;
} else {
return false;
}
}
public boolean isFooter(int position) {
int headerCount = hasHeader() ? HEADER_COUNT : 0;
if (hasFooter()) {
return position > (getCount() + headerCount) ? true : false;
} else {
return false;
}
}
public boolean isItem(int position) {
if (!isHeader(position) && !isFooter(position)) {
return true;
} else {
return false;
}
}
@Override
public final RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
if (viewType == Type.HEADER.ordinal()) {
return onCreate(parent, Type.HEADER.ordinal());
} else if (viewType == Type.FOOTER.ordinal()) {
return onCreate(parent, Type.FOOTER.ordinal());
} else {
return onCreate(parent, Type.ITEM.ordinal());
}
}
@Override
public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {
if (isItem(position)) {
int headerCount = hasHeader() ? HEADER_COUNT : 0;
U object = getObjectAt(position - headerCount);
onBind((V) holder, object, getItemViewType(position));
}
}
@Nullable
public U getObjectAt(int position) {
U object = null;
if (isValid && mCursor.moveToPosition(position)) {
object = extractFromCursor(mCursor);
}
return object;
}
@Override
public void onSwipe(int position) {
int headerCount = hasHeader() ? HEADER_COUNT : 0;
U object = getObjectAt(position - headerCount);
if (onChangedListener != null) {
onChangedListener.onRemove(object);
}
}
public abstract boolean hasHeader();
public abstract boolean hasFooter();
public abstract U extractFromCursor(Cursor cursor);
public abstract V onCreate(ViewGroup parent, int viewType);
public abstract void onBind(V holder, U item, int type);
public enum Type {
HEADER, ITEM, FOOTER;
}
public interface OnChangedListener<U> {
public void onAdd(U item);
public void onRemove(U item);
}
}
@mohanmanu484
Copy link

cannot resole onswipe listener??
where you are using that onswipe interface?

@mohanmanu484
Copy link

can you please show me an example how to use this adapter please??

@ErstwhileIII
Copy link

Would be good to replace
indexColumnId = isValid ? this.mCursor.getColumnIndex("_id") : -1;
with
indexColumnId = isValid ? this.mCursor.getColumnIndex(BaseColumns._ID) : -1;

@ErstwhileIII
Copy link

Also in public void swapCursor(Cursor cursor) you need to obtain the count of elements from the old cursor to properly handle the call to notifyItemRangeRemoved() (since getCount will refer to the new null mCursor

Perhaps something like:
if (mCursor != null) {
mIdColumn = mCursor.getColumnIndex(BaseColumns._ID);
isDataValid = true;
notifyDataSetChanged();
} else {
mIdColumn = -1;
isDataValid = false;
if (oldCursor != null) {
//TODO Ensure that getCount is right on for old range!!
notifyItemRangeRemoved(0, oldCursor.getCount());
}
}

@Ashok-Varma
Copy link

@slidenerd

The logic in isHeader() and isFooter() is wrong

  1. You only considering only one header but there can be many headers (so replace 0 with HEADER_COUNT)
public boolean isHeader(int position) {
        if (hasHeader()) {
            return position < HEADER_COUNT ? true : false;
        } else {
            return false;
        }
    }
  1. position index starts with 0 and getCount index starts with 1 (so replace > with >=)
public boolean isFooter(int position) {
        int headerCount = hasHeader() ? HEADER_COUNT : 0;
        if (hasFooter()) {
            return position >= (getCount() + headerCount) ? true : false;
        } else {
            return false;
        }
    }
  1. Also in lines 62 and 83 getCount() should be replaced with getCount()-1
notifyItemRangeInserted(getPositionForNotifyItemRangeXXX(), getCount()-1);
notifyItemRangeRemoved(getPositionForNotifyItemRangeXXX(), getCount()-1);

👍 👍 👍

@Ashok-Varma
Copy link

as @ErstwhileIII said old cursor need to be considered instead of doing those, copy newCursor to mCursor after those calls. Modified swapCursor

    public void swapCursor(Cursor newCursor) {
        if (newCursor == mCursor) {
            return;
        }
        final Cursor oldCursor = mCursor;
        if (newCursor != null) {
            indexColumnId = newCursor.getColumnIndexOrThrow("_id");
            isValid = true;
           mCursor = newCursor;
            notifyDataSetChanged();
        } else {
            indexColumnId = -1;
            isValid = false;
            notifyItemRangeRemoved(getPositionForNotifyItemRangeXXX(), getCount()-1);
            mCursor = newCursor;
            //There is no notifyDataSetInvalidated() method in RecyclerView.Adapter
        }
        if (oldCursor != null && !oldCursor.isClosed()) {
            oldCursor.close();
        }
    }

@slidenerd
Copy link
Author

Just came back and saw this :) thanks i made the changes to my code back then on my local machine however forgot to update this GIST, thanks for the suggestions guys, if you can think of better alternatives, feel free to suggest, always a learner 🎯

@VaibhavProbity
Copy link

How to use This class ?can u show me any example using this.

@VaibhavProbity
Copy link

Hi Vivz, Its taking too long time to load contact list ?How to improve performace?Please help me.

@vineet-devin
Copy link

Hey Vivz, is there a tutorial in which you have used this adapter so that I can refer to it while using it? thanks a lot. I have learnt a lot from your videos! :)

@loeschg
Copy link

loeschg commented Mar 18, 2016

For getItemId(int position), shouldn't cursor.moveToPosition(position) take the header into account (adjust position if it exists)?

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