ashwin mahajan
ashwin mahajan

Reputation: 1782

Loading data in a LiveData observing Recycler View : Inconsistency detected. Invalid view holder adapter positionViewHolder

I am working on a sample social media app with photos and albums. Using Android Architecture components Room, LiveData and ViewModel with Retrofit. There is a repository layer around Room which handles data operations.

RecyclerView -> ViewModel -> Repository -> Room <- Retrofit

@Entity(tableName = "photos",
        foreignKeys = {@ForeignKey(entity = AlbumModel.class, parentColumns = "id", childColumns = "album_id")},
        indices = {@Index(name = "index_album", value = "album_id")})
public class PhotoModel {

    @PrimaryKey
    private int id;

    @ColumnInfo(name = "album_id")
    private int albumId;

    private String title;
    private String url;
    private String thumbnail_url;
}

ViewModel
    public class PhotoViewModel extends AndroidViewModel {
        private LiveData<List<PhotoModel>> photos;
        private PhotoRepository photoRepository;

        public LiveData<List<PhotoModel>> getAlbumPhotos(int album_id) {
            if(photos == null) {
                photos = photoRepository.getAlbumPhotos(album_id);
            }
            return photos;
        }

    }

PhotoRepository
public class PhotoRepository implements PhotosInterface {
    private LiveData<List<PhotoModel>> photos;
    private PhotoDao photoDao;
    private AlbumRepository albumRepository;

    public LiveData<List<PhotoModel>> getAlbumPhotos(int album_id) {
        PhotoController.getAlbumPhotos(this, album_id);
        if (photos == null) {
            photos = photoDao.getAlbumPhotos(album_id);
        }
        return photos;
    }
}

@Dao
public abstract class PhotoDao {

    @Query("SELECT * FROM photos WHERE album_id=:album_id ORDER BY id DESC")
    public abstract LiveData<List<PhotoModel>> getAlbumPhotos(int album_id);
}

This is where the problem is: When Photos are fetched from the server, it checks for the album since album_id is the foreign key in the Photo Model. This happens in an AsycTask. So, the photos are not inserted in order which leads the listadapter to update the list and the UI which causes the error mentioned below. This is the same problem with other Recycler Views in the application based on the same architecture.

//Inserting photos through Photos Dao in Retro callback
    private  void InsertPhotoWithAlbum(PhotoDao photoDao, AlbumRepository albumRepository, PhotoModel photo) {
            if(photoDao.getPhotoObjectById(photo.getId()) == null ) {
                if(albumRepository.getAlbumObjectById(photo.getAlbumId()) != null) {
                    photoDao.insertPhoto(photo);
                } else {
                    albumRepository.fetchAlbum(photo.getAlbumId());
                    photoDao.insertPhoto(photo);
                }
            }
        }

Now an Album contains photos and listed in a RecyclerView using StaggeredGridLayoutManager. Below is the ListAdapter for the RecyclerView.

public class CustomListAdapter extends ListAdapter<PhotoModel, CustomListAdapter.PhotoViewHolder> implements AlbumDetailActivity.RecyclerViewOnClickListener{
        private List<PhotoModel> photos;
        private Context mContext;

        public CustomListAdapter(@NonNull DiffUtil.ItemCallback<PhotoModel> diffCallback, Context context) {
            super(diffCallback);
            mContext = context;
            PhotoViewModel photoViewModel = ViewModelProviders.of(AlbumDetailActivity.this).get(PhotoViewModel.class);
            photoViewModel.getAlbumPhotos(album_id).observe(AlbumDetailActivity.this, new Observer<List<PhotoModel>>() {
                @Override
                public void onChanged(@Nullable List<PhotoModel> photoModels) {
                    submitList(photoModels);
                    photos = photoModels;
                }
            });
//            photos = photoViewModel.getAlbumPhotosObjects(album_id);
        }

        @NonNull
        @Override
        public PhotoViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
            View itemView = LayoutInflater.from(mContext).inflate(R.layout.listitem_photos_list, null);
            PhotoViewHolder viewHolder = new PhotoViewHolder(itemView);
            itemView.setOnClickListener(new View.OnClickListener() {
                @Override
                public void onClick(View v) {

                }
            });
            return viewHolder;
        }

        @Override
        public void onBindViewHolder(@NonNull PhotoViewHolder holder, int position) {
            PhotoModel photo = photos.get(position);
            holder.tv_title.setText(photo.getTitle());
            Picasso.get().load(photo.getThumbnail_url()).into(holder.iv_photo_thumbnail);
        }

        public class PhotoViewHolder extends RecyclerView.ViewHolder {
            TextView tv_title;
            ImageView iv_photo_thumbnail;

            public PhotoViewHolder(View itemView) {
                super(itemView);
                tv_title = itemView.findViewById(R.id.tv_photo_title);
                iv_photo_thumbnail = itemView.findViewById(R.id.iv_photo_thumbnail);
            }

        }

        @Override
        public int getItemCount() {
            return photos == null ? 0 : photos.size();
        }

        @Override
        public void onItemClick(PhotoModel photo) {
            Toast.makeText(mContext, "Clicked on : " + photo.getId() + ":" + photo.getTitle() , Toast.LENGTH_SHORT).show();
        }
    }

While loading photos , the app crashes with the following error.

E/AndroidRuntime: FATAL EXCEPTION: main
                  Process: assess.talview.com.yalview_yasma, PID: 14830
                  java.lang.IndexOutOfBoundsException: Inconsistency detected. Invalid view holder adapter positionViewHolder{b4cfec1 position=42 id=-1, oldPos=39, pLpos:39 scrap [attachedScrap] tmpDetached not recyclable(1) no parent} android.support.v7.widget.RecyclerView{a26494c VFED..... .F....ID 0,0-1080,1731 #7f08007b app:id/recyclerview_photos}, adapter:assess.talview.com.yalview_yasma.album.AlbumDetailActivity$CustomListAdapter@6a61dfa, layout:android.support.v7.widget.StaggeredGridLayoutManager@b391fab, context:assess.talview.com.yalview_yasma.album.AlbumDetailActivity@520aa8f
                      at android.support.v7.widget.RecyclerView$Recycler.validateViewHolderForOffsetPosition(RecyclerView.java:5610)
                      at android.support.v7.widget.RecyclerView$Recycler.tryGetViewHolderForPositionByDeadline(RecyclerView.java:5792)
                      at android.support.v7.widget.RecyclerView$Recycler.getViewForPosition(RecyclerView.java:5752)
                      at android.support.v7.widget.RecyclerView$Recycler.getViewForPosition(RecyclerView.java:5748)
                      at android.support.v7.widget.LayoutState.next(LayoutState.java:100)
                      at android.support.v7.widget.StaggeredGridLayoutManager.fill(StaggeredGridLayoutManager.java:1611)
                      at android.support.v7.widget.StaggeredGridLayoutManager.onLayoutChildren(StaggeredGridLayoutManager.java:685)
                      at android.support.v7.widget.StaggeredGridLayoutManager.onLayoutChildren(StaggeredGridLayoutManager.java:607)
                      at android.support.v7.widget.RecyclerView.dispatchLayoutStep1(RecyclerView.java:3763)
                      at android.support.v7.widget.RecyclerView.dispatchLayout(RecyclerView.java:3527)
                      at android.support.v7.widget.RecyclerView.onLayout(RecyclerView.java:4082)
                      at android.view.View.layout(View.java:19659)
                      at android.view.ViewGroup.layout(ViewGroup.java:6075)
                      at android.support.design.widget.CoordinatorLayout.layoutChild(CoordinatorLayout.java:1191)
                      at android.support.design.widget.CoordinatorLayout.onLayoutChild(CoordinatorLayout.java:876)
                      at android.support.design.widget.CoordinatorLayout.onLayout(CoordinatorLayout.java:895)
                      at android.view.View.layout(View.java:19659)
                      at android.view.ViewGroup.layout(ViewGroup.java:6075)
                      at android.widget.FrameLayout.layoutChildren(FrameLayout.java:323)
                      at android.widget.FrameLayout.onLayout(FrameLayout.java:261)
                      at android.view.View.layout(View.java:19659)
                      at android.view.ViewGroup.layout(ViewGroup.java:6075)
                      at android.widget.LinearLayout.setChildFrame(LinearLayout.java:1791)
                      at android.widget.LinearLayout.layoutVertical(LinearLayout.java:1635)
                      at android.widget.LinearLayout.onLayout(LinearLayout.java:1544)
                      at android.view.View.layout(View.java:19659)
                      at android.view.ViewGroup.layout(ViewGroup.java:6075)
                      at android.widget.FrameLayout.layoutChildren(FrameLayout.java:323)
                      at android.widget.FrameLayout.onLayout(FrameLayout.java:261)
                      at android.view.View.layout(View.java:19659)
                      at android.view.ViewGroup.layout(ViewGroup.java:6075)
                      at android.widget.LinearLayout.setChildFrame(LinearLayout.java:1791)
                      at android.widget.LinearLayout.layoutVertical(LinearLayout.java:1635)
                      at android.widget.LinearLayout.onLayout(LinearLayout.java:1544)
                      at android.view.View.layout(View.java:19659)
                      at android.view.ViewGroup.layout(ViewGroup.java:6075)
                      at android.widget.FrameLayout.layoutChildren(FrameLayout.java:323)
                      at android.widget.FrameLayout.onLayout(FrameLayout.java:261)
                      at com.android.internal.policy.DecorView.onLayout(DecorView.java:761)
                      at android.view.View.layout(View.java:19659)
                      at android.view.ViewGroup.layout(ViewGroup.java:6075)
                      at android.view.ViewRootImpl.performLayout(ViewRootImpl.java:2496)
                      at android.view.ViewRootImpl.performTraversals(ViewRootImpl.java:2212)
                      at android.view.ViewRootImpl.doTraversal(ViewRootImpl.java:1392)
                      at android.view.ViewRootImpl$TraversalRunnable.run(ViewRootImpl.java:6752)
                      at android.view.Choreographer$CallbackRecord.run(Choreographer.java:911)
                      at android.view.Choreographer.doCallbacks(Choreographer.java:723)
E/AndroidRuntime:     at android.view.Choreographer.doFrame(Choreographer.java:658)
                      at android.view.Choreographer$FrameDisplayEventReceiver.run(Choreographer.java:897)
                      at android.os.Handler.handleCallback(Handler.java:790)
                      at android.os.Handler.dispatchMessage(Handler.java:99)
                      at android.os.Looper.loop(Looper.java:164)
                      at android.app.ActivityThread.main(ActivityThread.java:6494)
                      at java.lang.reflect.Method.invoke(Native Method)
                      at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:438)
                      at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:807)

Searching for the error suggested submitting a copy of the new list instead of the existing list which did not make a difference.

How to solve this problem? Is there a workaround or a different approach to handle this, with or without LiveData?

Upvotes: 5

Views: 1107

Answers (1)

ashwin mahajan
ashwin mahajan

Reputation: 1782

For someone who experiences the same issue:

The problem was when the data list got bigger, data adapter position was greater than the data size since getItemCount was overridden and not updated. So it was failing this condition in RecyclerView.Recycle.validateViewHolderForOffsetPosition method.

The solution was to not override the getItemCount method of the list adapter and let the Recycler View handle the data size. And the app worked flawlessly after that.

Upvotes: 4

Related Questions