Currently there is no default implementation of RecyclerView.Adapter available.
May be with the official release, Google will add it.
Since there is no support for CursorAdapter
with the RecyclerView
currently, how can we use a RecyclerView
with a database ? Any suggestions ?
xml. Create an another layout file (list_row. xml) in /res/layout folder to show the data in listview, for that right click on layout folder à add new Layout resource file à Give name as list_row. xml and write the code like as shown below.
If you are running a query with a CursorLoader
and you want RecyclerView
instead of ListView
.
You can try my CursorRecyclerViewAdapter
: CursorAdapter in RecyclerView
My solution was to hold a CursorAdapter member in my recyclerView.Adapter implementation. Then passing all the handling of creating the new view & binding it to the cursor adapter, something like this:
public class MyRecyclerAdapter extends Adapter<MyRecyclerAdapter.ViewHolder> {
// Because RecyclerView.Adapter in its current form doesn't natively
// support cursors, we wrap a CursorAdapter that will do all the job
// for us.
CursorAdapter mCursorAdapter;
Context mContext;
public MyRecyclerAdapter(Context context, Cursor c) {
mContext = context;
mCursorAdapter = new CursorAdapter(mContext, c, 0) {
@Override
public View newView(Context context, Cursor cursor, ViewGroup parent) {
// Inflate the view here
}
@Override
public void bindView(View view, Context context, Cursor cursor) {
// Binding operations
}
};
}
public static class ViewHolder extends RecyclerView.ViewHolder {
View v1;
public ViewHolder(View itemView) {
super(itemView);
v1 = itemView.findViewById(R.id.v1);
}
}
@Override
public int getItemCount() {
return mCursorAdapter.getCount();
}
@Override
public void onBindViewHolder(ViewHolder holder, int position) {
// Passing the binding operation to cursor loader
mCursorAdapter.getCursor().moveToPosition(position); //EDITED: added this line as suggested in the comments below, thanks :)
mCursorAdapter.bindView(holder.itemView, mContext, mCursorAdapter.getCursor());
}
@Override
public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
// Passing the inflater job to the cursor-adapter
View v = mCursorAdapter.newView(mContext, mCursorAdapter.getCursor(), parent);
return new ViewHolder(v);
}
}
Since your question says "How to use RecyclerView
with a database" and you are not being specific whether you want SQLite or anything else with the RecyclerView
, I'll give you a solution that is highly optimal. I'll be using Realm as database and let you display all the data inside your RecyclerView
. It has asynchronous query support as well without using Loaders
or AsyncTask
.
Why realm?
Step 1
Add the gradle dependency for Realm , the dependency for the latest version is found here
Step 2
Create your model class, for example, lets say something simple like Data
which has 2 fields, a string to be displayed inside the RecyclerView
row and a timestamp which will be used as itemId for allowing the RecyclerView
to animate items. Notice that I extend RealmObject
below because of which your Data
class will be stored as a table and all your properties will stored as columns of that table Data
. I have marked the data text as primary key in my case since I don't want a string to be added more than once. But if you prefer having duplicates, then make the timestamp as the @PrimaryKey. You can have a table without a primary key but it will cause problems if you try updating the row after creating it. A composite primary key at the time of writing this answer is not supported by Realm.
import io.realm.RealmObject;
import io.realm.annotations.PrimaryKey;
public class Data extends RealmObject {
@PrimaryKey
private String data;
//The time when this item was added to the database
private long timestamp;
public String getData() {
return data;
}
public void setData(String data) {
this.data = data;
}
public long getTimestamp() {
return timestamp;
}
public void setTimestamp(long timestamp) {
this.timestamp = timestamp;
}
}
Step 3
Create your layout for how a single row should appear inside the RecyclerView
.
The layout for a single row item inside our Adapter
is as follows
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android:id="@+id/area"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:background="@android:color/white"
android:padding="16dp"
android:text="Data"
android:visibility="visible" />
</FrameLayout>
Notice that I have kept a FrameLayout
as root even though I have a TextView
inside. I plan to add more items in this layout and hence made it flexible for now :)
For the curious people out there, this is how a single item looks currently.
Step 4
Create your RecyclerView.Adapter
implementation. In this case, the data source object is a special object called RealmResults
which is basically a LIVE ArrayList
, in other words, as items are added or removed from your table, this RealmResults
object auto updates.
import android.content.Context;
import android.support.v7.widget.RecyclerView;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;
import io.realm.Realm;
import io.realm.RealmResults;
import slidenerd.vivz.realmrecycler.R;
import slidenerd.vivz.realmrecycler.model.Data;
public class DataAdapter extends RecyclerView.Adapter<DataAdapter.DataHolder> {
private LayoutInflater mInflater;
private Realm mRealm;
private RealmResults<Data> mResults;
public DataAdapter(Context context, Realm realm, RealmResults<Data> results) {
mRealm = realm;
mInflater = LayoutInflater.from(context);
setResults(results);
}
public Data getItem(int position) {
return mResults.get(position);
}
@Override
public DataHolder onCreateViewHolder(ViewGroup parent, int viewType) {
View view = mInflater.inflate(R.layout.row_data, parent, false);
DataHolder dataHolder = new DataHolder(view);
return dataHolder;
}
@Override
public void onBindViewHolder(DataHolder holder, int position) {
Data data = mResults.get(position);
holder.setData(data.getData());
}
public void setResults(RealmResults<Data> results) {
mResults = results;
notifyDataSetChanged();
}
@Override
public long getItemId(int position) {
return mResults.get(position).getTimestamp();
}
@Override
public int getItemCount() {
return mResults.size();
}
public void add(String text) {
//Create a new object that contains the data we want to add
Data data = new Data();
data.setData(text);
//Set the timestamp of creation of this object as the current time
data.setTimestamp(System.currentTimeMillis());
//Start a transaction
mRealm.beginTransaction();
//Copy or update the object if it already exists, update is possible only if your table has a primary key
mRealm.copyToRealmOrUpdate(data);
//Commit the transaction
mRealm.commitTransaction();
//Tell the Adapter to update what it shows.
notifyDataSetChanged();
}
public void remove(int position) {
//Start a transaction
mRealm.beginTransaction();
//Remove the item from the desired position
mResults.remove(position);
//Commit the transaction
mRealm.commitTransaction();
//Tell the Adapter to update what it shows
notifyItemRemoved(position);
}
public static class DataHolder extends RecyclerView.ViewHolder {
TextView area;
public DataHolder(View itemView) {
super(itemView);
area = (TextView) itemView.findViewById(R.id.area);
}
public void setData(String text) {
area.setText(text);
}
}
}
Notice that I am calling notifyItemRemoved
with the position at which the removal happened but I DON'T call notifyItemInserted
or notifyItemRangeChanged
because there is no direct way to know which position the item was inserted into the database since Realm entries are not stored in an ordered fashion. The RealmResults
object auto updates whenever a new item is added, modified or removed from the database so we call notifyDataSetChanged
while adding and inserting bulk entries. At this point, you are probably concerned about the animations that won't be triggered because you are calling notifyDataSetChanged
in place of notifyXXX
methods. That is exactly why I have the getItemId
method return the timestamp for each row from the results object. Animation is achieved in 2 steps with notifyDataSetChanged
if you call setHasStableIds(true)
and then override getItemId
to provide something other than just the position.
Step 5
Lets add the RecyclerView
to our Activity
or Fragment
. In my case, I am using an Activity
. The layout file containing the RecyclerView
is pretty trivial and would look something like this.
<android.support.v7.widget.RecyclerView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/recycler"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_margin="@dimen/text_margin"
app:layout_behavior="@string/appbar_scrolling_view_behavior" />
I have added an app:layout_behavior
since my RecyclerView
goes inside a CoordinatorLayout
which I have not posted in this answer for brevity.
Step 6
Construct the RecyclerView
in code and supply the data it needs. Create and initialise a Realm object inside onCreate
and close it inside onDestroy
pretty much like closing an SQLiteOpenHelper
instance. At the simplest your onCreate
inside the Activity
will look like this. The initUi
method is where all the magic happens. I open an instance of Realm inside onCreate
.
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mRealm = Realm.getInstance(this);
initUi();
}
private void initUi() {
//Asynchronous query
RealmResults<Data> mResults = mRealm.where(Data.class).findAllSortedAsync("data");
//Tell me when the results are loaded so that I can tell my Adapter to update what it shows
mResults.addChangeListener(new RealmChangeListener() {
@Override
public void onChange() {
mAdapter.notifyDataSetChanged();
Toast.makeText(ActivityMain.this, "onChange triggered", Toast.LENGTH_SHORT).show();
}
});
mRecycler = (RecyclerView) findViewById(R.id.recycler);
mRecycler.setLayoutManager(new LinearLayoutManager(this));
mAdapter = new DataAdapter(this, mRealm, mResults);
//Set the Adapter to use timestamp as the item id for each row from our database
mAdapter.setHasStableIds(true);
mRecycler.setAdapter(mAdapter);
}
Notice that in the first step, I query Realm to give me all objects from the Data
class sorted by their variable name called data in an asynchronous manner. This gives me a RealmResults
object with 0 items on the main thread which I am setting on the Adapter
. I added a RealmChangeListener
to be notified when the data has finished loading from the background thread where I call notifyDataSetChanged
with my Adapter
. I have also called setHasStableIds
to true to let the RecyclerView.Adapter
implementation keep track of items that are added, removed or modified. The onDestroy
for my Activity
closes the Realm instance
@Override
protected void onDestroy() {
super.onDestroy();
mRealm.close();
}
This method initUi
can be called inside onCreate
of your Activity
or onCreateView
or onViewCreated
of your Fragment
. Notice the following things.
Step 7
BAM! There is data from database inside your RecyclerView
asynchonously loaded without CursorLoader
, CursorAdapter
, SQLiteOpenHelper
with animations. The GIF image shown here is kinda laggy but the animations are happening when you add items or remove them.
You can implement all the required methods yourself. I recently made my own implementation by just copy pasting code from CursorAdapter.
public class MyAdapter extends RecyclerView.Adapter<ViewHolder> {
protected boolean mDataValid;
protected boolean mAutoRequery;
protected Cursor mCursor;
protected Context mContext;
protected int mRowIDColumn;
protected ChangeObserver mChangeObserver;
protected DataSetObserver mDataSetObserver;
protected FilterQueryProvider mFilterQueryProvider;
public static final int FLAG_AUTO_REQUERY = 0x01;
public static final int FLAG_REGISTER_CONTENT_OBSERVER = 0x02;
public Cursor getCursor() {
return mCursor;
}
//Recommended
public MyAdapter(Context context, Cursor c, int flags) {
init(context, c, flags);
}
public MyAdapter(Context context, Cursor c) {
init(context, c, FLAG_AUTO_REQUERY);
}
public MyAdapter(Context context, Cursor c, boolean autoRequery) {
init(context, c, autoRequery ? FLAG_AUTO_REQUERY : FLAG_REGISTER_CONTENT_OBSERVER);
}
void init(Context context, Cursor c, int flags) {
if ((flags & FLAG_AUTO_REQUERY) == FLAG_AUTO_REQUERY) {
flags |= FLAG_REGISTER_CONTENT_OBSERVER;
mAutoRequery = true;
} else {
mAutoRequery = false;
}
boolean cursorPresent = c != null;
mCursor = c;
mDataValid = cursorPresent;
mContext = context;
mRowIDColumn = cursorPresent ? c.getColumnIndexOrThrow("_id") : -1;
if ((flags & FLAG_REGISTER_CONTENT_OBSERVER) == FLAG_REGISTER_CONTENT_OBSERVER) {
mChangeObserver = new ChangeObserver();
mDataSetObserver = new MyDataSetObserver();
} else {
mChangeObserver = null;
mDataSetObserver = null;
}
if (cursorPresent) {
if (mChangeObserver != null) c.registerContentObserver(mChangeObserver);
if (mDataSetObserver != null) c.registerDataSetObserver(mDataSetObserver);
}
}
// Create new views (invoked by the layout manager)
@Override
public ViewHolder onCreateViewHolder(final ViewGroup parent,
int viewType) {
// create a new view
final View view = LayoutInflater.from(parent.getContext())
.inflate(R.layout.list_item, parent, false);
// set the view's size, margins, paddings and layout parameters
ViewHolder vh = new ViewHolder(view, mCursor, new ViewHolder.IMyViewHolderClicks() {
@SuppressLint("NewApi")
@Override
public void onClick(Cursor cursor) {
Log.e("Item :", cursor.getString(cursor.getColumnIndex(MyDatabaseHelper.MW_NAAM)));
Intent intent = new Intent(TasksListFragment.this.getActivity(), DetailActivity.class);
intent.putExtra(DetailActivity.EXTRA_PARAM_ID, cursor.getLong(cursor.getColumnIndex(MyDatabaseHelper.MW_ID)));
ActivityOptions activityOptions = ActivityOptions.makeSceneTransitionAnimation(
TasksListFragment.this.getActivity(),
// Now we provide a list of Pair items which contain the view we can transitioning
// from, and the name of the view it is transitioning to, in the launched activity
new Pair<View, String>(
view.findViewById(R.id.imageview_item),
DetailActivity.VIEW_NAME_HEADER_IMAGE),
new Pair<View, String>(
view.findViewById(R.id.textview_name),
DetailActivity.VIEW_NAME_HEADER_TITLE)
);
// Now we can start the Activity, providing the activity options as a bundle
startActivity(intent, activityOptions.toBundle());
// END_INCLUDE(start_activity)
}
});
return vh;
}
// Replace the contents of a view (invoked by the layout manager)
@SuppressLint("NewApi")
@Override
public void onBindViewHolder(ViewHolder holder, int position) {
// - get element from your dataset at this position
// - replace the contents of the view with that element
final Cursor cursor = getItem(position);
holder.mTextView.setText(cursor.getString(cursor.getColumnIndex(MyDatabaseHelper.MW_NAAM)));
holder.mImageView.setTransitionName("grid:image:" + cursor.getLong(cursor.getColumnIndex(MyDatabaseHelper.MW_ID)));
holder.mTextView.setTransitionName("grid:name:" + cursor.getLong(cursor.getColumnIndex(MyDatabaseHelper.MW_ID)));
}
//@Override
// public View getView(int position, View view, ViewGroup viewGroup) {
// return view;
// }
// Return the size of your dataset (invoked by the layout manager)
@Override
public int getItemCount() {
return getCount();
}
public int getCount() {
if (mDataValid && mCursor != null) {
return mCursor.getCount();
} else {
return 0;
}
}
public Cursor getItem(int position) {
if (mDataValid && mCursor != null) {
mCursor.moveToPosition(position);
return mCursor;
} else {
return null;
}
}
@Override
public long getItemId(int position) {
if (mDataValid && mCursor != null) {
if (mCursor.moveToPosition(position)) {
return mCursor.getLong(mRowIDColumn);
} else {
return 0;
}
} else {
return 0;
}
}
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;
// notify the observers about the new cursor
notifyDataSetChanged();
} else {
mRowIDColumn = -1;
mDataValid = false;
// notify the observers about the lack of a data set
notifyDataSetInvalidated();
}
return oldCursor;
}
public void changeCursor(Cursor cursor) {
Cursor old = swapCursor(cursor);
if (old != null) {
old.close();
}
}
public CharSequence convertToString(Cursor cursor) {
return cursor == null ? "" : cursor.toString();
}
public Cursor runQueryOnBackgroundThread(CharSequence constraint) {
if (mFilterQueryProvider != null) {
return mFilterQueryProvider.runQuery(constraint);
}
return mCursor;
}
public FilterQueryProvider getFilterQueryProvider() {
return mFilterQueryProvider;
}
public void setFilterQueryProvider(FilterQueryProvider filterQueryProvider) {
mFilterQueryProvider = filterQueryProvider;
}
protected void onContentChanged() {
if (mAutoRequery && mCursor != null && !mCursor.isClosed()) {
if (false) Log.v("Cursor", "Auto requerying " + mCursor + " due to update");
mDataValid = mCursor.requery();
}
}
private class ChangeObserver extends ContentObserver {
public ChangeObserver() {
super(new Handler());
}
@Override
public boolean deliverSelfNotifications() {
return true;
}
@Override
public void onChange(boolean selfChange) {
onContentChanged();
}
}
private class MyDataSetObserver extends DataSetObserver {
@Override
public void onChanged() {
mDataValid = true;
notifyDataSetChanged();
}
@Override
public void onInvalidated() {
mDataValid = false;
notifyDataSetInvalidated();
}
}
private final DataSetObservable mDataSetObservable = new DataSetObservable();
public void registerDataSetObserver(DataSetObserver observer) {
mDataSetObservable.registerObserver(observer);
}
public void unregisterDataSetObserver(DataSetObserver observer) {
mDataSetObservable.unregisterObserver(observer);
}
public void notifyDataSetInvalidated() {
mDataSetObservable.notifyInvalidated();
}
}
I made a RecyclerViewCursorAdapter using a SortedList as backend, extending RecyclerView.Adapter
Can be used with SQLiteCursor and Loaders
Just another answer, since I did not like the accepted one (which imo has not an intuitive usage).
The following is my own implementation, which is very similar to (and partially inspired by) SimpleCursorAdapter
:
public class RecyclerViewSimpleCursorAdapter extends RecyclerView.Adapter {
private int mLayout;
private Cursor mCursor;
private String[] mFrom;
private int[] mTo;
private boolean mAutoRequery;
private ContentObserver mContentObserver;
/**
* Standard constructor.
*
* @param layout resource identifier of a layout file that defines the views for this list item. The layout file should include at least those named views defined in "to"
* @param c The database cursor. Can be null if the cursor is not available yet.
* @param from A list of column names representing the data to bind to the UI. Can be null if the cursor is not available yet.
* @param to The views that should display column in the "from" parameter. These should all be TextViews and ImageViews. The first N views in this list are given the values of the first N columns in the from parameter. Can be null if the cursor is not available yet.
*/
public RecyclerViewSimpleCursorAdapter(int layout, Cursor c, String[] from, int[] to, boolean autoRequery) {
mLayout = layout;
mCursor = c;
mFrom = from;
mTo = to;
mAutoRequery = autoRequery;
if (mAutoRequery) {
initializeContentObserver();
}
}
@Override
public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
return new RecyclerView.ViewHolder(
LayoutInflater.from(parent.getContext())
.inflate(mLayout, parent, false)
) {
};
}
@Override
public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {
mCursor.moveToPosition(position);
if (mFrom == null || mTo == null)
return;
for (int i = 0; i < mFrom.length && i < mTo.length; i++) {
String from = mFrom[i];
int columnIndex = mCursor.getColumnIndex(from);
String value = mCursor.getString(columnIndex);
View view = holder.itemView.findViewById(mTo[i]);
if (view instanceof TextView) {
((TextView) view).setText(value);
} else if (view instanceof ImageView) {
try {
((ImageView) view).setImageResource(Integer.parseInt(value));
} catch (NumberFormatException nfe) {
((ImageView) view).setImageURI(Uri.parse(value));
}
} else {
throw new IllegalStateException(view.getClass().getName() + " is not a view that can be bound by this RecyclerViewSimpleCursorAdapter");
}
}
}
@Override
public int getItemCount() {
return mCursor != null ? mCursor.getCount() : 0;
}
private void initializeContentObserver() {
mContentObserver = new ContentObserver(new Handler()) {
@Override
public boolean deliverSelfNotifications() {
return true;
}
@Override
public void onChange(boolean selfChange) {
notifyDataSetChanged();
}
};
mCursor.registerContentObserver(mContentObserver);
}
/**
* Change the underlying cursor to a new cursor. If there is an existing cursor it will be closed.
*
* @param cursor The new cursor to be used
*/
public void changeCursor(Cursor cursor) {
Cursor oldCursor = mCursor;
if (mAutoRequery) {
if (mCursor != null) {
mCursor.unregisterContentObserver(mContentObserver);
}
mContentObserver = new ContentObserver(new Handler()) {
@Override
public boolean deliverSelfNotifications() {
return true;
}
@Override
public void onChange(boolean selfChange) {
notifyDataSetChanged();
}
};
mCursor = cursor;
if (mCursor != null) {
mCursor.registerContentObserver(mContentObserver);
}
}
notifyDataSetChanged();
if (oldCursor != null && oldCursor != mCursor) {
oldCursor.close();
}
}
/**
* Change the cursor and change the column-to-view mappings at the same time.
*
* @param cursor The database cursor. Can be null if the cursor is not available yet.
* @param from A list of column names representing the data to bind to the UI. Can be null if the cursor is not available yet.
* @param to The views that should display column in the "from" parameter. These should all be TextViews or ImageViews. The first N views in this list are given the values of the first N columns in the from parameter. Can be null if the cursor is not available yet.
*/
public void changeCursorAndColumns(Cursor cursor, String[] from, int[] to) {
mFrom = from;
mTo = to;
changeCursor(cursor);
}
/**
* Returns the cursor.
* @return the cursor
*/
public Cursor getCursor() {
return mCursor;
}
}
You may modify it for other particular usages, but having a cursor it works just as SimpleCursorAdapter
does, only with a RecyclerView
.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With