Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Best practices to use realm with a recycler view?

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);
            }
    }));
like image 645
Anghelut Florin Avatar asked Mar 11 '15 19:03

Anghelut Florin


People also ask

What advantage do you get in your app when you use recycler view and adapters?

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.

Is recycler view a ViewGroup?

RecyclerView is a ViewGroup added to the android studio as a successor of the GridView and ListView.


2 Answers

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();     } } 
like image 139
EpicPandaForce Avatar answered Sep 22 '22 04:09

EpicPandaForce


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

like image 27
PirateApp Avatar answered Sep 22 '22 04:09

PirateApp