Do you guys have any best practices regarding using realm with a recyclerview ? I know it's generic question but I found nothing on it on the internet. For example I run into a lot of troubles trying to implement a simple color change on a row . For example consider this typical usage:
public class User extends RealmObject {
@PrimaryKey
String name;
boolean isSelected;
...
constructor, getter and setters
}
public class UserAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> {
private RealmResults<User> users;
public UserAdapter(RealmResults<User> users) {
this.users = users;
}
...
public void markAsSelected(int position){
// get the old selected user and deselect it
notifyItemChanged(? how do i get the position given my User has no index ?);
// mark as selected the new user at position
}
I ran into a lot of issues since I couldn't find anything on the internet. I know this is because I don't know how to properly use realm. But finding the right way is a struggle in itself . I read all their documentation but to no avail.
EDIT : Since I was asked to --> Instead of saying "I have a bunch of issues with [that]", describe your issue(s) and we'll try to provide insights and answers to your incomprehensions.
So my problem is simple :
I have a RealmUser :
public class RealmUser extends RealmObject {
@PrimaryKey
private String key;
private String name;
private boolean isSelected;
private boolean editMode;
private RealmList<RealmItemList> lists;
public RealmUser() {}
public RealmUser(String name, RealmList<RealmItemList> lists, boolean isSelected , boolean editMode) {
this.key = UUID.randomUUID().toString();
this.name = name;
this.isSelected = isSelected;
this.editMode = editMode;
if (lists ==null){
this.lists = new RealmList<RealmItemList>();
}else{
this.lists = lists;
}
}
public String getKey() {
return key;
}
public void setKey(String key) {
this.key = key;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public boolean isSelected() {
return isSelected;
}
public void setSelected(boolean isSelected) {
this.isSelected = isSelected;
}
public boolean isEditMode() {
return editMode;
}
public void setEditMode(boolean editMode) {
this.editMode = editMode;
}
public RealmList<RealmItemList> getLists() {
return lists;
}
public void setLists(RealmList<RealmItemList> lists) {
this.lists = lists;
}
}
Which I put in a RealmResults array using :
RealmResults users = realm.where(RealmUser.class).findAll();
I pass my user array to my custom user adapter :
public class UserAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> {
private RealmResults<RealmUser> users;
public UserAdapter(RealmResults<RealmUser> users) {
this.users = users;
}
@Override
public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
LayoutInflater inflater = LayoutInflater.from(parent.getContext());
if(viewType == 1){
View v = inflater.inflate(R.layout.detail_user, parent, false);
return new UserHolder(v);
}else if(viewType == 2){
View v = inflater.inflate(R.layout.edit_user, parent, false);
return new editUserHolder(v);
}else {
return null;
}
}
@Override
public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {
RealmUser user = users.get(position);
String userName = user.getName();
boolean isSelected = user.isSelected();
if (holder instanceof UserHolder ){
UserHolder uHolder = (UserHolder) holder;
uHolder.userText.setText(userName);
if (isSelected){
uHolder.userContainer.setBackgroundColor(Color.parseColor("#607D8B"));
}
}else if(holder instanceof editUserHolder){
editUserHolder eUserHolder = (editUserHolder) holder;
eUserHolder.userEditContainer.setBackgroundColor(Color.parseColor("#eeeeee"));
}
}
@Override
public int getItemViewType(int position) {
RealmUser user = users.get(position);
if (user.isEditMode()){
return 2;
}else {
return 1;
}
}
@Override
public int getItemCount() {
return users.size();
}
public void markAsSelected(int position, DrawerLayout mDrawerLayout , Toolbar toolbar, Realm realm){
// Here is my problem : How do I get the already selected user asuming there is one in my db and notify the UI that I changed that item.
}
That has a custom click Listener : that gets recyclerview item that was clicked using :
public class UserClickListener implements RecyclerView.OnItemTouchListener{
public static interface OnItemClickListener{
public void onItemClick(View v, int position);
}
private OnItemClickListener mListener;
private GestureDetector mGestureDetector;
public UserClickListener(Context context, final RecyclerView recyclerView, OnItemClickListener listener)
{
mListener = listener;
mGestureDetector = new GestureDetector(context, new GestureDetector.SimpleOnGestureListener() {
@Override
public boolean onSingleTapConfirmed(MotionEvent e) {
View childView = recyclerView.findChildViewUnder(e.getX(), e.getY());
if(childView != null && mListener != null)
{
mListener.onItemClick(childView, recyclerView.getChildPosition(childView));
return true;
}
return false;
}
});
}
@Override
public boolean onInterceptTouchEvent(RecyclerView view, MotionEvent e) {
View childView = view.findChildViewUnder(e.getX(), e.getY());
if(childView != null && mListener != null && mGestureDetector.onTouchEvent(e))
{
mListener.onItemClick(childView, view.getChildPosition(childView));
}
return false;
}
@Override
public void onTouchEvent(RecyclerView rv, MotionEvent e) {
}
}
Which I add to my recyclerView with addOnItemTouchListener :
mListRecycler.addOnItemTouchListener(new UserClickListener(getActivity(), mListRecycler, new UserClickListener.OnItemClickListener(){
@Override
public void onItemClick(View view, int position)
{
UserAdapter myadapter = (UserAdapter) mListRecycler.getAdapter();
myadapter.markAsSelected(position, mDrawerLayout , mToolbar, realm);
}
}));
The RecyclerView is a ViewGroup that renders any adapter-based view in a similar way. It is supposed to be the successor of ListView and GridView. One of the reasons is that RecyclerView has a more extensible framework, especially since it provides the ability to implement both horizontal and vertical layouts.
RecyclerView is a ViewGroup added to the android studio as a successor of the GridView and ListView.
ANSWER FOR 0.89.0 AND ABOVE
For the latest versions, you should use RealmRecyclerViewAdapter in the realm-android-adapters repository.
Versions:
Use 1.5.0 up to 2.X
Use 2.1.1 up to 4.X
Use 3.0.0 above 5.X
OLD ANSWER FOR OLD VERSIONS:
I made this RealmRecyclerViewAdapter
based on the implementation of RealmBaseAdapter
.
This is for v0.89.0 AND ABOVE
public abstract class RealmRecyclerViewAdapter<T extends RealmObject, VH extends RecyclerView.ViewHolder> extends RecyclerView.Adapter<VH> { //put this in `io.realm` protected LayoutInflater inflater; protected OrderedRealmCollection<T> adapterData; protected Context context; private final RealmChangeListener listener; public RealmRecyclerViewAdapter(Context context, OrderedRealmCollection<T> data) { if (context == null) { throw new IllegalArgumentException("Context cannot be null"); } this.context = context; this.adapterData = data; this.inflater = LayoutInflater.from(context); this.listener = new RealmChangeListener<RealmResults<T>>() { @Override public void onChange(RealmResults<T> results) { notifyDataSetChanged(); } }; if (data != null) { addListener(data); } } private void addListener(OrderedRealmCollection<T> data) { if (data instanceof RealmResults) { RealmResults realmResults = (RealmResults) data; realmResults.addChangeListener(listener); } else if (data instanceof RealmList) { RealmList realmList = (RealmList) data; realmList.realm.handlerController.addChangeListenerAsWeakReference(listener); } else { throw new IllegalArgumentException("RealmCollection not supported: " + data.getClass()); } } private void removeListener(OrderedRealmCollection<T> data) { if (data instanceof RealmResults) { RealmResults realmResults = (RealmResults) data; realmResults.removeChangeListener(listener); } else if (data instanceof RealmList) { RealmList realmList = (RealmList) data; realmList.realm.handlerController.removeWeakChangeListener(listener); } else { throw new IllegalArgumentException("RealmCollection not supported: " + data.getClass()); } } /** * Returns how many items are in the data set. * * @return the number of items. */ @Override public int getItemCount() { if (adapterData == null) { return 0; } return adapterData.size(); } /** * Get the data item associated with the specified position in the data set. * * @param position Position of the item whose data we want within the adapter's * data set. * @return The data at the specified position. */ public T getItem(int position) { if (adapterData == null) { return null; } return adapterData.get(position); } /** * Get the row id associated with the specified position in the list. Note that item IDs are not stable so you * cannot rely on the item ID being the same after {@link #notifyDataSetChanged()} or * {@link #updateData(OrderedRealmCollection)} has been called. * * @param position The position of the item within the adapter's data set whose row id we want. * @return The id of the item at the specified position. */ @Override public long getItemId(int position) { // TODO: find better solution once we have unique IDs return position; } /** * Updates the data associated with the Adapter. * * Note that RealmResults and RealmLists are "live" views, so they will automatically be updated to reflect the * latest changes. This will also trigger {@code notifyDataSetChanged()} to be called on the adapter. * * This method is therefore only useful if you want to display data based on a new query without replacing the * adapter. * * @param data the new {@link OrderedRealmCollection} to display. */ public void updateData(OrderedRealmCollection<T> data) { if (listener != null) { if (adapterData != null) { removeListener(adapterData); } if (data != null) { addListener(data); } } this.adapterData = data; notifyDataSetChanged(); } }
This is for v0.84.0 AND ABOVE, BUT OLDER THAN v0.89.0 (updated for v0.87.5):
public abstract class RealmRecyclerViewAdapter<T extends RealmObject, VH extends RecyclerView.ViewHolder> extends RecyclerView.Adapter<VH> { //put this in `io.realm` protected LayoutInflater inflater; protected RealmResults<T> realmResults; protected Context context; private final RealmChangeListener listener; public RealmRecyclerViewAdapter(Context context, RealmResults<T> realmResults, boolean automaticUpdate) { if (context == null) { throw new IllegalArgumentException("Context cannot be null"); } this.context = context; this.realmResults = realmResults; this.inflater = LayoutInflater.from(context); this.listener = (!automaticUpdate) ? null : new RealmChangeListener() { @Override public void onChange() { notifyDataSetChanged(); } }; if (listener != null && realmResults != null) { realmResults.realm.handlerController.addChangeListenerAsWeakReference(listener); } } /** * Returns how many items are in the data set. * * @return count of items. */ @Override public int getItemCount() { if (realmResults == null) { return 0; } return realmResults.size(); } /** * Returns the item associated with the specified position. * * @param i index of item whose data we want. * @return the item at the specified position. */ public T getItem(int i) { if (realmResults == null) { return null; } return realmResults.get(i); } /** * Returns the current ID for an item. Note that item IDs are not stable so you cannot rely on the item ID being the * same after {@link #notifyDataSetChanged()} or {@link #updateRealmResults(RealmResults)} has been called. * * @param i index of item in the adapter. * @return current item ID. */ @Override public long getItemId(int i) { // TODO: find better solution once we have unique IDs return i; } /** * Updates the RealmResults associated to the Adapter. Useful when the query has been changed. * If the query does not change you might consider using the automaticUpdate feature. * * @param queryResults the new RealmResults coming from the new query. */ public void updateRealmResults(RealmResults<T> queryResults) { if (listener != null) { // Making sure that Adapter is refreshed correctly if new RealmResults come from another Realm if (this.realmResults != null) { this.realmResults.realm.removeChangeListener(listener); } if (queryResults != null) { queryResults.realm.addChangeListener(listener); } } this.realmResults = queryResults; notifyDataSetChanged(); } public void addChangeListenerAsWeakReference(RealmChangeListener realmChangeListener) { if(realmResults != null) { realmResults.realm.handlerController.addChangeListenerAsWeakReference(realmChangeListener); } } }
This is for OLDER THAN 0.84.0:
public abstract class RealmRecyclerViewAdapter<T extends RealmObject, VH extends RecyclerView.ViewHolder> extends RecyclerView.Adapter<VH> { //put this in `io.realm` protected LayoutInflater inflater; protected RealmResults<T> realmResults; protected Context context; private final RealmChangeListener listener; public RealmRecyclerViewAdapter(Context context, RealmResults<T> realmResults, boolean automaticUpdate) { if(context == null) { throw new IllegalArgumentException("Context cannot be null"); } this.context = context; this.realmResults = realmResults; this.inflater = LayoutInflater.from(context); this.listener = (!automaticUpdate) ? null : new RealmChangeListener() { @Override public void onChange() { notifyDataSetChanged(); } }; if(listener != null && realmResults != null) { realmResults.getRealm() .addChangeListener(listener); } } @Override public long getItemId(int i) { // TODO: find better solution once we have unique IDs return i; } public T getItem(int i) { if(realmResults == null) { return null; } return realmResults.get(i); } public void updateRealmResults(RealmResults<T> queryResults) { if(listener != null) { // Making sure that Adapter is refreshed correctly if new RealmResults come from another Realm if(this.realmResults != null) { realmResults.getRealm().removeChangeListener(listener); } if(queryResults != null) { queryResults.getRealm().addChangeListener(listener); } } this.realmResults = queryResults; notifyDataSetChanged(); } @Override public int getItemCount() { if(realmResults == null) { return 0; } return realmResults.size(); } }
Some of the answers above include reflection, not to mention that a sectioned RecyclerView would cause complications. They also do not support adding and removing items. Here is my version of the RecyclerView Adapter that works with Realm, supports a sectioned RecyclerView, also adds and removes items at arbitrary positions if need be
Here is our AbstractRealmAdapter that takes care of all the low level stuff, displaying headers, footers, items, loading data inside RealmResults, managing item types
import io.realm.Realm;
import io.realm.RealmObject;
import io.realm.RealmResults;
public abstract class AbstractRealmAdapter<T extends RealmObject, VH extends RecyclerView.ViewHolder>
extends RecyclerView.Adapter<VH> {
public static final int HEADER_COUNT = 1;
public static final int FOOTER_COUNT = 1;
//Our data source
protected RealmResults<T> mResults;
public AbstractRealmAdapter(Realm realm) {
//load data from subclasses
mResults = loadData(realm);
notifyDataSetChanged();
}
public int getHeaderCount() {
return hasHeader() ? HEADER_COUNT : 0;
}
public int getFooterCount() {
return hasFooter() ? FOOTER_COUNT : 0;
}
public boolean isHeader(int position) {
if (hasHeader()) {
return position < HEADER_COUNT;
} else {
return false;
}
}
public boolean isFooter(int position) {
if (hasFooter()) {
return position >= getCount() + getHeaderCount();
} else {
return false;
}
}
@Override
public long getItemId(int i) {
return i;
}
@Override
public final int getItemViewType(int position) {
if (isHeader(position)) {
return ItemType.HEADER.ordinal();
} else if (isFooter(position)) {
return ItemType.FOOTER.ordinal();
} else {
return ItemType.ITEM.ordinal();
}
}
/**
* @param position the position within our adapter inclusive of headers,items and footers
* @return an item only if it is not a header or a footer, otherwise returns null
*/
public T getItem(int position) {
if (!isHeader(position) && !isFooter(position) && !mResults.isEmpty()) {
return mResults.get(position - getHeaderCount());
}
return null;
}
@Override
public final int getItemCount() {
return getHeaderCount() + getCount() + getFooterCount();
}
public final int getCount() {
return mResults.size();
}
public abstract boolean hasHeader();
public abstract boolean hasFooter();
public void setData(RealmResults<T> results) {
mResults = results;
notifyDataSetChanged();
}
protected abstract RealmResults<T> loadData(Realm realm);
public enum ItemType {
HEADER, ITEM, FOOTER;
}
}
To add items by some method or remove items by swipe to delete, we have an extension in the form of AbstractMutableRealmAdapter that looks as shown below
import android.support.v7.widget.RecyclerView;
import io.realm.Realm;
import io.realm.RealmObject;
public abstract class AbstractMutableRealmAdapter<T extends RealmObject, VH extends RecyclerView.ViewHolder>
extends AbstractRealmAdapter<T, VH> implements OnSwipeListener {
private Realm realm;
public AbstractMutableRealmAdapter(Realm realm) {
//call the superclass constructor to load data from subclasses into realmresults
super(realm);
this.realm = realm;
}
public void add(T item, boolean update) {
realm.beginTransaction();
T phraseToWrite = (update == true) ? realm.copyToRealmOrUpdate(item) : realm.copyToRealm(item);
realm.commitTransaction();
notifyItemRangeChanged(0, mResults.size());
}
@Override
public final void onSwipe(int position) {
if (!isHeader(position) && !isFooter(position) && !mResults.isEmpty()) {
int itemPosition = position - getHeaderCount();
realm.beginTransaction();
T item = mResults.get(itemPosition);
item.removeFromRealm();
realm.commitTransaction();
notifyItemRemoved(position);
}
}
}
Notice the use of the interface OnSwipeListener which looks like this
public interface OnSwipeListener {
/**
* @param position the position of the item that was swiped within the RecyclerView
*/
void onSwipe(int position);
}
This SwipeListener is used to perform a Swipe to delete inside our TouchHelperCallback which in turn is used to delete the objects from Realm directly and looks as follows
import android.support.v7.widget.RecyclerView;
import android.support.v7.widget.helper.ItemTouchHelper;
public class TouchHelperCallback extends ItemTouchHelper.Callback {
private final OnSwipeListener mSwipeListener;
public TouchHelperCallback(OnSwipeListener adapter) {
mSwipeListener = adapter;
}
/**
* @return false if you dont want to enable drag else return true
*/
@Override
public boolean isLongPressDragEnabled() {
return false;
}
/**
* @return true of you want to enable swipe in your RecyclerView else return false
*/
@Override
public boolean isItemViewSwipeEnabled() {
return true;
}
@Override
public int getMovementFlags(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder) {
//We want to let the person swipe to the right on devices that run LTR and let the person swipe from right to left on devices that run RTL
int swipeFlags = ItemTouchHelper.END;
return makeMovementFlags(0, swipeFlags);
}
@Override
public boolean onMove(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder,
RecyclerView.ViewHolder target) {
return false;
}
@Override
public void onSwiped(RecyclerView.ViewHolder viewHolder, int direction) {
mSwipeListener.onSwipe(viewHolder.getAdapterPosition());
}
}
The full implementation demo is available here for review https://github.com/slidenerd/SpamWordList/tree/spamphraser_with_realmresults_base Feel free to suggest any improvements
I replaced the notifyXXX methods with notifyDataSetChanged, RealmResults objects are live objects which means they automatically change when the data is updated, I tried calling notifyXXX methods and they caused an RecyclerView inconsistency exception, I am well aware of the fact that notifyDataSetChanged() would mess with animations, will keep you guys updated on a solution that overcomes the inconsistency error and at the same time provides a good adapter experience
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