0

I have started using Realm for one of my projects a while ago and when trying to upgrade realm to version 0.89.0 or higher, my RecyclerView behavior completely falls apart. I tried to isolate the issue and came down to this sample : https://bitbucket.org/pr-shadoko/realmrecyclerviewtest Here are the main classes: The activity:

public class MainActivity extends AppCompatActivity {
    ItemTouchHelper itemTouchHelper;
    RecyclerView recyclerView;
    TagsAdapter adapter;
    Realm realm;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        RealmConfiguration config = new RealmConfiguration.Builder(this).build();
        Realm.setDefaultConfiguration(config);
        realm = Realm.getDefaultInstance();

        adapter = new TagsAdapter(realm, new OnDragStartListener() {
            @Override
            public void onDragStart(RecyclerView.ViewHolder viewHolder) {
                itemTouchHelper.startDrag(viewHolder);
            }
        });
        itemTouchHelper = new ItemTouchHelper(new TagTouchHelperCallback(adapter));
        recyclerView = (RecyclerView) findViewById(R.id.rv);
        recyclerView.setAdapter(adapter);
        itemTouchHelper.attachToRecyclerView(recyclerView);
    }

    @Override
    public boolean onCreateOptionsMenu(Menu menu) {
        super.onCreateOptionsMenu(menu);
        getMenuInflater().inflate(R.menu.main, menu);
        return true;
    }

    @Override
    public boolean onOptionsItemSelected(MenuItem item) {
        switch(item.getItemId()) {
            case R.id.action_add:
                Tag tag = new Tag();
                tag.setTag("#tag" + tag.getTagId());
                realm.beginTransaction();
                realm.copyToRealmOrUpdate(tag);
                realm.commitTransaction();
                adapter.notifyItemInserted(1);
            default:
                return super.onOptionsItemSelected(item);
        }
    }
}

The RecyclerView Adapter:

public class TagsAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> implements ItemTouchHelperAdapter {
    private static final int TYPE_TAG = 0;
    private static final int TYPE_HEADER = 1;

    private Realm realm;
    private RealmResults<Tag> tags;
    private OnDragStartListener dragStartListener;

    public TagsAdapter(Realm realm, OnDragStartListener dragStartListener) {
        this.realm = realm;
        this.tags = realm.where(Tag.class).findAllSorted("order", Sort.DESCENDING);
        this.dragStartListener = dragStartListener;

        setHasStableIds(true);
    }

    @Override
    public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
        switch(viewType) {
            case TYPE_TAG:
                View taskView = LayoutInflater.from(parent.getContext()).inflate(R.layout.item, parent, false);
                return new TagViewHolder(taskView);
            case TYPE_HEADER:
                View headerView = LayoutInflater.from(parent.getContext()).inflate(R.layout.item, parent, false);
                return new HeaderViewHolder(headerView);
            default:
                return null;
        }
    }

    @Override
    public void onBindViewHolder(final RecyclerView.ViewHolder holder, int adapterPosition) {
        switch(holder.getItemViewType()) {
            case TYPE_TAG:
                onBindViewHolderTag((TagViewHolder) holder, adapterPosition);
                break;
            case TYPE_HEADER:
                onBindViewHolderHeader((HeaderViewHolder) holder, adapterPosition);
                break;
            default:
        }
    }

    private void onBindViewHolderTag(final TagViewHolder holder, final int adapterPosition) {
        final Tag tag = tags.get(getDataSetPosition(adapterPosition));
        holder.title.setText(tag.getTag());
        holder.itemView.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                Log.d("TAGS", "item clicked: id=" + tag.getTagId() + " ; adapterPosition=" + adapterPosition + " ; datasetPosition=" + getDataSetPosition(adapterPosition));
            }
        });
        holder.itemView.setOnTouchListener(new View.OnTouchListener() {
            @Override
            public boolean onTouch(View v, MotionEvent event) {
                if(MotionEventCompat.getActionMasked(event) == MotionEvent.ACTION_DOWN) {
                    dragStartListener.onDragStart(holder);
                }
                return false;
            }
        });
    }

    private void onBindViewHolderHeader(HeaderViewHolder holder, int adapterPosition) {
        holder.header.setText("TITLE");
    }

    private int getDataSetPosition(int adapterPosition) {
        switch(getItemViewType(adapterPosition)) {
            case TYPE_TAG:
                return adapterPosition - 1;
            case TYPE_HEADER:
            default:
                return RecyclerView.NO_POSITION;
        }
    }

    @Override
    public int getItemCount() {
        return tags.size() + 1;
    }

    @Override
    public long getItemId(int adapterPosition) {
        switch(getItemViewType(adapterPosition)) {
            case TYPE_TAG:
                return tags.get(getDataSetPosition(adapterPosition)).getTagId();
            case TYPE_HEADER:
            default:
                return RecyclerView.NO_ID;
        }
    }

    @Override
    public int getItemViewType(int adapterPosition) {
        if(adapterPosition == 0) {
            return TYPE_HEADER;
        }
        return TYPE_TAG;
    }

    @Override
    public boolean onItemMoved(final int fromAdapterPosition, final int toAdapterPosition) {
        Log.i("TAGS", "move from " + fromAdapterPosition + " to " + toAdapterPosition);
        if(getItemViewType(toAdapterPosition) != TYPE_TAG) {
            return false;
        }

        Tag task1 = tags.get(getDataSetPosition(fromAdapterPosition));
        Tag task2 = tags.get(getDataSetPosition(toAdapterPosition));
        Log.i("TAGS", "realm pos from=" + getDataSetPosition(fromAdapterPosition) + " ; to=" + getDataSetPosition(toAdapterPosition));
        int order1 = task1.getOrder();
        realm.beginTransaction();
        task1.setOrder(task2.getOrder());
        task2.setOrder(order1);
        realm.copyToRealmOrUpdate(task1);
        realm.copyToRealmOrUpdate(task2);
        realm.commitTransaction();
        notifyItemMoved(fromAdapterPosition, toAdapterPosition);
        return true;
    }

    @Override
    public void onItemSwiped(RecyclerView.ViewHolder holder, int direction) {}

    public class TagViewHolder extends RecyclerView.ViewHolder {
        public final TextView title;

        public TagViewHolder(View itemView) {
            super(itemView);
            this.title = (TextView) itemView.findViewById(R.id.label);
        }
    }

    public class HeaderViewHolder extends RecyclerView.ViewHolder {
        public final TextView header;

        public HeaderViewHolder(View itemView) {
            super(itemView);
            this.header = (TextView) itemView.findViewById(R.id.label);
        }
    }
}

The Tag table definition:

public class Tag extends RealmObject {
    @Ignore
    private final static Object nextIdLock = new Object();
    @Ignore
    private static Integer nextId;

    @PrimaryKey
    @Required
    private Integer tagId;
    @Required
    private Integer order;
    @Required
    private String tag;

    public Tag() {
        synchronized(nextIdLock) {
            if(nextId == null) {
                Realm realm = Realm.getDefaultInstance();
                RealmResults<Tag> tags = realm.where(Tag.class).findAll();
                if(tags.size() != 0) {
                    nextId = tags.max("tagId").intValue() + 1;
                } else {
                    nextId = 0;
                }
                realm.close();
            }
            order = tagId = nextId++;
        }
    }

    public Integer getTagId() {
        return tagId;
    }

    public Tag setTagId(Integer tagId) {
        this.tagId = tagId;
        return this;
    }

    public Integer getOrder() {
        return order;
    }

    public Tag setOrder(Integer order) {
        this.order = order;
        return this;
    }

    public String getTag() {
        return tag;
    }

    public Tag setTag(String tag) {
        this.tag = tag;
        return this;
    }
}

Other files (classes, interfaces, layouts, etc.) are pretty straigthforward so I will not paste them here to keep the question readable but they are available in the repository.

Now the symptoms:

  • realm version 0.88.2 (branch master):
    • Clicking on the 'add item' button adds an item on top of the list with a smooth animation
    • Touching an item allows to drag it to reorder the list
    • So it works as expected
  • realm version 0.89.0 (branch realm-0.89.0):
    • Clicking on the 'add item' button:
      • 1st click: adds the item to the realm but does not update the view
      • further clicks: crashes the app ("java.lang.IndexOutOfBoundsException: Inconsistency detected. Invalid view holder adapter positionViewHolder{349439e7 position=2 id=0, oldPos=1, pLpos:1 scrap [attachedScrap] tmpDetached no parent}")
    • Touching an item:
      • Stops the dragging at first swap
      • swaps the items in the realm (can be seen by forcing the activity to re-create, thus building the RecyclerView from fresh data)
      • Reverts view to its original state or swaps others elements??

The only thing that changed between the two branches is the Realm version. I looked around for a similar issue with no luck. At this point, I am not sure if this is the 0.89.0+ that breaks the RecyclerView behavior or if this is the 0.88.2 that was not not supposed to work this way.

Any help would be greatly appreciated.

pr-shadoko
  • 131
  • 7

1 Answers1

1

That's because in 0.89.0, to create proper iteration behavior on RealmResults, local commits update the RealmResults only on the next looper event, and not immediately.

So this code here

    realm.beginTransaction();
    task1.setOrder(task2.getOrder());
    task2.setOrder(order1);
    realm.copyToRealmOrUpdate(task1);
    realm.copyToRealmOrUpdate(task2);
    realm.commitTransaction();
    notifyItemMoved(fromAdapterPosition, toAdapterPosition);

Will fail, because the list hasn't yet updated after commitTransaction().

I've been meaning to figure out a smart way to solve this problem but to no avail, although forcing an immediate refresh via the HandlerController can achieve the same results.

Community
  • 1
  • 1
EpicPandaForce
  • 79,669
  • 27
  • 256
  • 428