Deleting Crime deletes the Crime from the DB but throws IndexOutOfBoundsException

Hi @cstewart,

I finished the challenge of deleting a Crime however while the crime gets deleted the app also crashes and I see this StackTrace:

StackTrace
05-24 18:50:32.029 16997-16997/com.bignerdranch.android.criminalintent E/AndroidRuntime: FATAL EXCEPTION: main
Process: com.bignerdranch.android.criminalintent, PID: 16997
java.lang.IndexOutOfBoundsException: Inconsistency detected. Invalid view holder adapter positionViewHolder{107c5af6 position=3 id=-1, oldPos=3, pLpos:-1 scrap [attachedScrap] tmpDetached no parent}
	at android.support.v7.widget.RecyclerView$Recycler.validateViewHolderForOffsetPosition(RecyclerView.java:5251)
	at android.support.v7.widget.RecyclerView$Recycler.tryGetViewHolderForPositionByDeadline(RecyclerView.java:5433)
	at android.support.v7.widget.RecyclerView$Recycler.getViewForPosition(RecyclerView.java:5394)
	at android.support.v7.widget.RecyclerView$Recycler.getViewForPosition(RecyclerView.java:5390)
	at android.support.v7.widget.LinearLayoutManager$LayoutState.next(LinearLayoutManager.java:2149)
	at android.support.v7.widget.LinearLayoutManager.layoutChunk(LinearLayoutManager.java:1533)
	at android.support.v7.widget.LinearLayoutManager.fill(LinearLayoutManager.java:1496)
	at android.support.v7.widget.LinearLayoutManager.onLayoutChildren(LinearLayoutManager.java:593)
	at android.support.v7.widget.RecyclerView.dispatchLayoutStep1(RecyclerView.java:3488)
	at android.support.v7.widget.RecyclerView.dispatchLayout(RecyclerView.java:3264)
	at android.support.v7.widget.RecyclerView.onLayout(RecyclerView.java:3798)
	at android.view.View.layout(View.java:15797)
	at android.view.ViewGroup.layout(ViewGroup.java:5198)
	at android.widget.FrameLayout.layoutChildren(FrameLayout.java:579)
	at android.widget.FrameLayout.onLayout(FrameLayout.java:514)
	at android.view.View.layout(View.java:15797)
	at android.view.ViewGroup.layout(ViewGroup.java:5198)
	at android.widget.FrameLayout.layoutChildren(FrameLayout.java:579)
	at android.widget.FrameLayout.onLayout(FrameLayout.java:514)
	at android.view.View.layout(View.java:15797)
	at android.view.ViewGroup.layout(ViewGroup.java:5198)
	at android.widget.FrameLayout.layoutChildren(FrameLayout.java:579)
	at android.widget.FrameLayout.onLayout(FrameLayout.java:514)
	at android.view.View.layout(View.java:15797)
	at android.view.ViewGroup.layout(ViewGroup.java:5198)
	at android.support.v7.widget.ActionBarOverlayLayout.onLayout(ActionBarOverlayLayout.java:437)
	at android.view.View.layout(View.java:15797)
	at android.view.ViewGroup.layout(ViewGroup.java:5198)
	at android.widget.FrameLayout.layoutChildren(FrameLayout.java:579)
	at android.widget.FrameLayout.onLayout(FrameLayout.java:514)
	at android.view.View.layout(View.java:15797)
	at android.view.ViewGroup.layout(ViewGroup.java:5198)
	at android.widget.LinearLayout.setChildFrame(LinearLayout.java:1703)
	at android.widget.LinearLayout.layoutVertical(LinearLayout.java:1557)
	at android.widget.LinearLayout.onLayout(LinearLayout.java:1466)
	at android.view.View.layout(View.java:15797)
	at android.view.ViewGroup.layout(ViewGroup.java:5198)
	at android.widget.FrameLayout.layoutChildren(FrameLayout.java:579)
	at android.widget.FrameLayout.onLayout(FrameLayout.java:514)
	at android.view.View.layout(View.java:15797)
	at android.view.ViewGroup.layout(ViewGroup.java:5198)
	at android.view.ViewRootImpl.performLayout(ViewRootImpl.java:2111)
	at android.view.ViewRootImpl.performTraversals(ViewRootImpl.java:1868)
	at android.view.ViewRootImpl.doTraversal(ViewRootImpl.java:1086)
	at android.view.ViewRootImpl$TraversalRunnable.run(ViewRootImpl.java:6474)
	at android.view.Choreographer$CallbackRecord.run(Choreographer.java:846)
	at android.view.Choreographer.doCallbacks(Choreographer.java:647)
	at android.view.Choreographer.doFrame(Choreographer.java:601)
	at android.view.Choreographer$FrameDisplayEventReceiver.run(Choreographer.java:829)
	at android.os.Handler.handleCallback(Handler.java:739)
	at android.os.Handler.dispatchMessage(Handler.java:95)
	at android.os.Looper.loop(Looper.java:135)
	at android.app.ActivityThread.main(ActivityThread.java:5254)
	at java.lang.reflect.Method.invoke(Native Method)
	at java.lang.reflect.Method.invoke(Method.java:372)
	at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:977)
	at co

I have a feeling that the error is happening because of this line of code in the updateUI() method:

       mAdapter.notifyItemChanged(clickedCrimePosition);

Aside from changing this to

       mAdapter.notifyDataSetChanged();

would there be a better way to do this?

1 Like

Any time you see the “Inconsistency detected” error, it means that you’ve changed your backing dataset but you haven’t correctly told the RecyclerView about that change. When you delete something, you can use the notifyItemRemoved method.

That said, most people do not bother with the notifyItemChanged/notifyItemRemoved/etc methods because they require a whole lot more work from the developer for very little gain. Most people go straight to notifyDataSetChanged so that they do not have to keep track of exactly what changed. This method will reload all of the views in the RecyclerView that you can see (so ~10-20 rows). If your app does not have heavy rows (facebook for example does have heavy rows), then I would just use notifyDataSetChanged.

1 Like

Thanks @cstewart,
Out of curiosity how would I go about telling the RecyclerView about the change?

With the various notify... methods.

notifyItemChanged(...)/notifyItemRemoved(...)/notifyItemInserted(...), etc. or the notifyDataSetChanged method (which is what most people use).

In your case, you did tell the adapter that your item changed but you didn’t call the correct change method for what happened.

Understood. Lets say this app has heavy rows. It sounds like I would need to use some sort of conditional check to decide on the appropriate method to use. I was thinking of having a variable in CrimeListFragment to track the last operation type (Inserted, Changed, Removed). The challenge I have is figuring out how to set this variable. Should I create an intent in CrimeFragment and use getActivity.setResult(…) to send the operation type back to CrimeListFragment?
Do let me know if there is a better way to do this.

In the case where an item was inserted I don’t see the mAdapter.notifyItemChanged(clickedCrimePosition) method throw an Exception

e.g.

if (lastOperationType == ITEM_REMOVED) {
    mAdapter.notifyItemRemoved(clickedCrimePosition);
}
if (lastOperationType == ITEM_CHANGED) {
    mAdapter.notifyItemChanged(clickedCrimePosition);
}
1 Like

I haven’t used it myself, but I think the DiffUtil class exists to solve this problem. You give it your old list and your new list and it figures out the right way to update the RecyclerView: https://medium.com/@iammert/using-diffutil-in-android-recyclerview-bdca8e4fbb00

If you do try that out, let me know how it works.

Without that, you just have to do a lot of manual bookkeeping like what you mentioned. Using the startActivityForResult mechanism is probably how I would do it if I wasn’t using DiffUtil.

1 Like

may i have your solution for deleting a crime?

i wrote this method:

public void deleteCrime (Crime crime) {
    String uuidString = crime.getId().toString();

    mDatabase.delete(CrimeTable.NAME,
             "id = ?" ,
            new String[] { uuidString }); }

i’m confuse about how to get id for selected item in my recyclerView to pass in here

i’ll be grateful if u let me know about that part of your code))

Hi @drgnme,

Here is the code for CrimeLab.java for Ch14 Challenge 1. Let me know if you have any further questions.

CrimeLab.java
package com.bignerdranch.android.criminalintent;

import android.content.ContentValues;
import android.content.Context;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;

import com.bignerdranch.android.criminalintent.database.CrimeBaseHelper;
import com.bignerdranch.android.criminalintent.database.CrimeCursorWrapper;
import com.bignerdranch.android.criminalintent.database.CrimeDbSchema.CrimeTable;

import java.io.File;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;

public class CrimeLab {
    public static CrimeLab sCrimeLab;

    private Context mContext;
    private SQLiteDatabase mDatabase;

    public static CrimeLab get(Context context) {
        if (sCrimeLab == null) {
            sCrimeLab = new CrimeLab(context);
        }
        return sCrimeLab;
    }

    private CrimeLab(Context context) {
        mContext = context.getApplicationContext();
        mDatabase = new CrimeBaseHelper(mContext).getWritableDatabase();
    }

    public void addCrime(Crime c) {
        ContentValues values = getContentValues(c);

        mDatabase.insert(CrimeTable.NAME, null, values);
    }

    public void deleteCrime(Crime c) {
        mDatabase.delete(CrimeTable.NAME,
                CrimeTable.Cols.UUID + " = ?",
                new String[] { c.getID().toString() });
    }

    public List<Crime> getCrimes() {
        List<Crime> crimes = new ArrayList<>();

        CrimeCursorWrapper cursor = queryCrimes(null, null);

        try {
            cursor.moveToFirst();
            while (!cursor.isAfterLast()) {
                crimes.add(cursor.getCrime());
                cursor.moveToNext();
            }
        } finally {
            cursor.close();
        }

        return crimes;
    }

    public Crime getCrime(UUID id) {
        CrimeCursorWrapper cursor = queryCrimes(
                CrimeTable.Cols.UUID + " = ?",
                new String[] { id.toString() }
        );

        try {
            if (cursor.getCount() == 0) {
                return null;
            }

            cursor.moveToFirst();
            return cursor.getCrime();
        } finally {
            cursor.close();
        }
    }

    public File getPhotoFile(Crime crime) {
        File filesDir = mContext.getFilesDir();
        return new File(filesDir, crime.getPhotoFileName());
    }

    public void updateCrime(Crime crime) {
        String uuidString = crime.getID().toString();
        ContentValues values = getContentValues(crime);

        mDatabase.update(CrimeTable.NAME, values,
                CrimeTable.Cols.UUID + " = ?",
                new String[] { uuidString });
    }

    private CrimeCursorWrapper queryCrimes(String whereClause, String[] whereArgs) {
        Cursor cursor = mDatabase.query(
                CrimeTable.NAME,
                null, // columns - null selects all columns
                whereClause,
                whereArgs,
                null, // groupBy
                null, // having
                null  // orderBy
        );

        return new CrimeCursorWrapper(cursor);
    }

    private static ContentValues getContentValues(Crime crime) {
        ContentValues values = new ContentValues();
        values.put(CrimeTable.Cols.UUID, crime.getID().toString());
        values.put(CrimeTable.Cols.TITLE, crime.getTitle());
        values.put(CrimeTable.Cols.DATE, crime.getDate().getTime());
        values.put(CrimeTable.Cols.TIME, crime.getTime().getTime());
        values.put(CrimeTable.Cols.SOLVED, crime.isSolved() ? 1 : 0);
        values.put(CrimeTable.Cols.SUSPECT, crime.getSuspect());
        values.put(CrimeTable.Cols.SUSPECT_CONTACT_ID, crime.getSuspectContactID());

        return values;
    }
}

Thanks! i will…
but it didn’t work for me… my error was for replacing the Constructor

i used this method for deleting a crime and it worked:

public void deleteCrime (Crime crime) {
    mDatabase.delete(CrimeTable.NAME,
            CrimeTable.Cols.UUID + " = ?",
            new String[]{crime.getId().toString()}); }

@drgnme, that looks correct. I think I must have copied the code for CrimeLab.java from the Chapter 13 solution which did not involve using the SQLite DB. I have corrected my previous statement