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??
- Clicking on the 'add item' button:
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.