Note that there are some explanatory texts on larger screens.

plurals
  1. POData out of sync between a custom CursorLoader and a CursorAdapter backing a ListView
    primarykey
    data
    text
    <h2>Background:</h2> <p>I have a custom <code>CursorLoader</code> that works directly with SQLite Database instead of using a <code>ContentProvider</code>. This loader works with a <code>ListFragment</code> backed by a <code>CursorAdapter</code>. So far so good.</p> <p>To simplify things, lets assume there is a Delete button on the UI. When user clicks this, I delete a row from the DB, and also call <code>onContentChanged()</code> on my loader. Also, on <code>onLoadFinished()</code> callback, I call <code>notifyDatasetChanged()</code> on my adapter so as to refresh the UI.</p> <h2>Problem:</h2> <p>When the delete commands happen in rapid succession, meaning the <code>onContentChanged()</code> is called in rapid succession, <strong><code>bindView()</code> ends up to be working with stale data</strong>. What this means is a row has been deleted, but the ListView is still attempting to display that row. This leads to Cursor exceptions.</p> <p>What am I doing wrong?</p> <h2>Code:</h2> <p>This is a custom CursorLoader (based on <a href="https://groups.google.com/d/msg/android-developers/J-Uql3Mn73Y/3haYPQ-pR7sJ" rel="noreferrer">this advice</a> by Ms. Diane Hackborn)</p> <pre><code>/** * An implementation of CursorLoader that works directly with SQLite database * cursors, and does not require a ContentProvider. * */ public class VideoSqliteCursorLoader extends CursorLoader { /* * This field is private in the parent class. Hence, redefining it here. */ ForceLoadContentObserver mObserver; public VideoSqliteCursorLoader(Context context) { super(context); mObserver = new ForceLoadContentObserver(); } public VideoSqliteCursorLoader(Context context, Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) { super(context, uri, projection, selection, selectionArgs, sortOrder); mObserver = new ForceLoadContentObserver(); } /* * Main logic to load data in the background. Parent class uses a * ContentProvider to do this. We use DbManager instead. * * (non-Javadoc) * * @see android.support.v4.content.CursorLoader#loadInBackground() */ @Override public Cursor loadInBackground() { Cursor cursor = AppGlobals.INSTANCE.getDbManager().getAllCameras(); if (cursor != null) { // Ensure the cursor window is filled int count = cursor.getCount(); registerObserver(cursor, mObserver); } return cursor; } /* * This mirrors the registerContentObserver method from the parent class. We * cannot use that method directly since it is not visible here. * * Hence we just copy over the implementation from the parent class and * rename the method. */ void registerObserver(Cursor cursor, ContentObserver observer) { cursor.registerContentObserver(mObserver); } } </code></pre> <p>A snippet from my <code>ListFragment</code> class that shows the <code>LoaderManager</code> callbacks; as well as a <code>refresh()</code> method that I call whenever user adds/deletes a record.</p> <pre><code>@Override public void onActivityCreated(Bundle savedInstanceState) { super.onActivityCreated(savedInstanceState); mListView = getListView(); /* * Initialize the Loader */ mLoader = getLoaderManager().initLoader(LOADER_ID, null, this); } @Override public Loader&lt;Cursor&gt; onCreateLoader(int id, Bundle args) { return new VideoSqliteCursorLoader(getActivity()); } @Override public void onLoadFinished(Loader&lt;Cursor&gt; loader, Cursor data) { mAdapter.swapCursor(data); mAdapter.notifyDataSetChanged(); } @Override public void onLoaderReset(Loader&lt;Cursor&gt; loader) { mAdapter.swapCursor(null); } public void refresh() { mLoader.onContentChanged(); } </code></pre> <p>My <code>CursorAdapter</code> is just a regular one with <code>newView()</code> being over-ridden to return newly inflated row layout XML and <code>bindView()</code> using the <code>Cursor</code> to bind columns to <code>View</code>s in the row layout.</p> <hr> <h2>EDIT 1</h2> <p>After digging into this a bit, I think the fundamental issue here is the way the <code>CursorAdapter</code> handles the underlying <code>Cursor</code>. I'm trying to understand how that works.</p> <p>Take the following scenario for better understanding.</p> <ol> <li>Suppose the <code>CursorLoader</code> has finished loading and it returns a <code>Cursor</code> that now has 5 rows.</li> <li>The <code>Adapter</code> starts displaying these rows. It moves the <code>Cursor</code> to the next position and calls <code>getView()</code></li> <li>At this point, even as the list view is in the process of being rendered, a row (say, with _id = 2) is deleted from the database.</li> <li><strong>This is where the issue is</strong> - The <code>CursorAdapter</code> has moved the <code>Cursor</code> to a position which corresponds to a deleted row. The <code>bindView()</code> method still tries to access the columns for this row using this <code>Cursor</code>, which is invalid and we get exceptions.</li> </ol> <h3>Question:</h3> <ul> <li>Is this understanding correct? I am particularly interested in point 4 above where I am making the assumption that when a row gets deleted, the <code>Cursor</code> doesn't get refreshed unless I ask for it to be.</li> <li>Assuming this is right, how do I ask my <code>CursorAdapter</code> to discard/abort its rendering of the <code>ListView</code> <strong>even as it is in progress</strong> and ask it to use the fresh <code>Cursor</code> (returned through <code>Loader#onContentChanged()</code> and <code>Adapter#notifyDatasetChanged()</code>) instead?</li> </ul> <p><strong>P.S.</strong> Question to moderators: Should this edit be moved to a separate question?</p> <hr> <h2>EDIT 2</h2> <p>Based on suggestion from various answers, it looks like there was a fundamental mistake in my understanding of how <code>Loader</code>s work. It turns out that:</p> <ol> <li>The <code>Fragment</code> or <code>Adapter</code> should not be directly operating on the <code>Loader</code> at all.</li> <li>The <code>Loader</code> should monitor for all changes in data and should just give the <code>Adapter</code> the new <code>Cursor</code> in <code>onLoadFinished()</code> whenever data changes.</li> </ol> <p>Armed with this understanding, I attempted the following changes. - No operation on the <code>Loader</code> whatsoever. The refresh method does nothing now.</p> <p>Also, to debug what's going on inside the <code>Loader</code> and the <code>ContentObserver</code>, I came up with this:</p> <pre><code>public class VideoSqliteCursorLoader extends CursorLoader { private static final String LOG_TAG = "CursorLoader"; //protected Cursor mCursor; public final class CustomForceLoadContentObserver extends ContentObserver { private final String LOG_TAG = "ContentObserver"; public CustomForceLoadContentObserver() { super(new Handler()); } @Override public boolean deliverSelfNotifications() { return true; } @Override public void onChange(boolean selfChange) { Utils.logDebug(LOG_TAG, "onChange called; selfChange = "+selfChange); onContentChanged(); } } /* * This field is private in the parent class. Hence, redefining it here. */ CustomForceLoadContentObserver mObserver; public VideoSqliteCursorLoader(Context context) { super(context); mObserver = new CustomForceLoadContentObserver(); } /* * Main logic to load data in the background. Parent class uses a * ContentProvider to do this. We use DbManager instead. * * (non-Javadoc) * * @see android.support.v4.content.CursorLoader#loadInBackground() */ @Override public Cursor loadInBackground() { Utils.logDebug(LOG_TAG, "loadInBackground called"); Cursor cursor = AppGlobals.INSTANCE.getDbManager().getAllCameras(); //mCursor = AppGlobals.INSTANCE.getDbManager().getAllCameras(); if (cursor != null) { // Ensure the cursor window is filled int count = cursor.getCount(); Utils.logDebug(LOG_TAG, "Count = " + count); registerObserver(cursor, mObserver); } return cursor; } /* * This mirrors the registerContentObserver method from the parent class. We * cannot use that method directly since it is not visible here. * * Hence we just copy over the implementation from the parent class and * rename the method. */ void registerObserver(Cursor cursor, ContentObserver observer) { cursor.registerContentObserver(mObserver); } /* * A bunch of methods being overridden just for debugging purpose. * We simply include a logging statement and call through to super implementation * */ @Override public void forceLoad() { Utils.logDebug(LOG_TAG, "forceLoad called"); super.forceLoad(); } @Override protected void onForceLoad() { Utils.logDebug(LOG_TAG, "onForceLoad called"); super.onForceLoad(); } @Override public void onContentChanged() { Utils.logDebug(LOG_TAG, "onContentChanged called"); super.onContentChanged(); } } </code></pre> <p>And here are snippets of my <code>Fragment</code> and <code>LoaderCallback</code></p> <pre><code>@Override public void onActivityCreated(Bundle savedInstanceState) { super.onActivityCreated(savedInstanceState); mListView = getListView(); /* * Initialize the Loader */ getLoaderManager().initLoader(LOADER_ID, null, this); } @Override public Loader&lt;Cursor&gt; onCreateLoader(int id, Bundle args) { return new VideoSqliteCursorLoader(getActivity()); } @Override public void onLoadFinished(Loader&lt;Cursor&gt; loader, Cursor data) { Utils.logDebug(LOG_TAG, "onLoadFinished()"); mAdapter.swapCursor(data); } @Override public void onLoaderReset(Loader&lt;Cursor&gt; loader) { mAdapter.swapCursor(null); } public void refresh() { Utils.logDebug(LOG_TAG, "CamerasListFragment.refresh() called"); //mLoader.onContentChanged(); } </code></pre> <p>Now, whenever there is a change in the DB (row added/deleted), the <code>onChange()</code> method of the <code>ContentObserver</code> should be called - correct? I don't see this happening. My <code>ListView</code> never shows any change. The only time I see any change is if I explicitly call <code>onContentChanged()</code> on the <code>Loader</code>.</p> <p>What's going wrong here?</p> <hr> <h2>EDIT 3</h2> <p>Ok, so I re-wrote my <code>Loader</code> to extend directly from <code>AsyncTaskLoader</code>. I still don't see my DB changes being refreshed, nor the <code>onContentChanged()</code> method of my <code>Loader</code> being called when I insert/delete a row in the DB :-(</p> <p>Just to clarify a few things:</p> <ol> <li><p>I used the code for <code>CursorLoader</code> and just modified one single line that returns the <code>Cursor</code>. Here, I replaced the call to <code>ContentProvider</code> with my <code>DbManager</code> code (which in turn uses <code>DatabaseHelper</code> to perform a query and return the <code>Cursor</code>).</p> <p><code>Cursor cursor = AppGlobals.INSTANCE.getDbManager().getAllCameras();</code></p></li> <li><p>My inserts/updates/deletes on the database happen from elsewhere and not through the <code>Loader</code>. In most cases the DB operations are happening in a background <code>Service</code>, and in a couple of cases, from an <code>Activity</code>. I directly use my <code>DbManager</code> class to perform these operations.</p></li> </ol> <p>What I still don't get is - <strong>who tells my <code>Loader</code> that a row has been added/deleted/modified?</strong> In other words, where is <code>ForceLoadContentObserver#onChange()</code> called? In my Loader, I register my observer on the <code>Cursor</code>:</p> <pre><code>void registerContentObserver(Cursor cursor, ContentObserver observer) { cursor.registerContentObserver(mObserver); } </code></pre> <p>This would imply that the onus is on the <code>Cursor</code> to notify <code>mObserver</code> when it has changed. But, then AFAIK, a 'Cursor' is not a "live" object that updates the data it is pointing to as and when data is modified in the DB.</p> <p>Here's the latest iteration of my Loader:</p> <pre><code>import android.content.Context; import android.database.ContentObserver; import android.database.Cursor; import android.support.v4.content.AsyncTaskLoader; public class VideoSqliteCursorLoader extends AsyncTaskLoader&lt;Cursor&gt; { private static final String LOG_TAG = "CursorLoader"; final ForceLoadContentObserver mObserver; Cursor mCursor; /* Runs on a worker thread */ @Override public Cursor loadInBackground() { Utils.logDebug(LOG_TAG , "loadInBackground()"); Cursor cursor = AppGlobals.INSTANCE.getDbManager().getAllCameras(); if (cursor != null) { // Ensure the cursor window is filled int count = cursor.getCount(); Utils.logDebug(LOG_TAG , "Cursor count = "+count); registerContentObserver(cursor, mObserver); } return cursor; } void registerContentObserver(Cursor cursor, ContentObserver observer) { cursor.registerContentObserver(mObserver); } /* Runs on the UI thread */ @Override public void deliverResult(Cursor cursor) { Utils.logDebug(LOG_TAG, "deliverResult()"); if (isReset()) { // An async query came in while the loader is stopped if (cursor != null) { cursor.close(); } return; } Cursor oldCursor = mCursor; mCursor = cursor; if (isStarted()) { super.deliverResult(cursor); } if (oldCursor != null &amp;&amp; oldCursor != cursor &amp;&amp; !oldCursor.isClosed()) { oldCursor.close(); } } /** * Creates an empty CursorLoader. */ public VideoSqliteCursorLoader(Context context) { super(context); mObserver = new ForceLoadContentObserver(); } @Override protected void onStartLoading() { Utils.logDebug(LOG_TAG, "onStartLoading()"); if (mCursor != null) { deliverResult(mCursor); } if (takeContentChanged() || mCursor == null) { forceLoad(); } } /** * Must be called from the UI thread */ @Override protected void onStopLoading() { Utils.logDebug(LOG_TAG, "onStopLoading()"); // Attempt to cancel the current load task if possible. cancelLoad(); } @Override public void onCanceled(Cursor cursor) { Utils.logDebug(LOG_TAG, "onCanceled()"); if (cursor != null &amp;&amp; !cursor.isClosed()) { cursor.close(); } } @Override protected void onReset() { Utils.logDebug(LOG_TAG, "onReset()"); super.onReset(); // Ensure the loader is stopped onStopLoading(); if (mCursor != null &amp;&amp; !mCursor.isClosed()) { mCursor.close(); } mCursor = null; } @Override public void onContentChanged() { Utils.logDebug(LOG_TAG, "onContentChanged()"); super.onContentChanged(); } } </code></pre>
    singulars
    1. This table or related slice is empty.
    plurals
    1. This table or related slice is empty.
    1. This table or related slice is empty.
 

Querying!

 
Guidance

SQuiL has stopped working due to an internal error.

If you are curious you may find further information in the browser console, which is accessible through the devtools (F12).

Reload