自定义CursorLoader和支持ListView的CursorAdapter之间的数据不同步

背景:

我有一个自定义CursorLoader ,它直接与SQLite数据库一起使用,而不是使用ContentProvider 。 此加载器使用由CursorAdapter支持的ListFragment 。 到现在为止还挺好。

为了简化操作,我们假设UI上有一个Delete按钮。 当用户单击此onContentChanged() ,我从DB中删除一行,并在我的加载器上调用onContentChanged() 。 另外,在onLoadFinished()回调中,我在我的适配器上调用notifyDatasetChanged()以刷新UI。

问题:

当删除命令快速连续发生时,意味着快速连续调用bindView()bindView()最终将使用陈旧数据 。 这意味着行已被删除,但ListView仍在尝试显示该行。 这导致了Cursorexception。

我究竟做错了什么?

码:

这是一个自定义的CursorLoader(基于Diane Hackborn女士的建议 )

 /** * 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); } } 

我的ListFragment类中的一个片段,显示LoaderManager回调; 以及每当用户添加/删除记录时我调用的refresh()方法。

 @Override public void onActivityCreated(Bundle savedInstanceState) { super.onActivityCreated(savedInstanceState); mListView = getListView(); /* * Initialize the Loader */ mLoader = getLoaderManager().initLoader(LOADER_ID, null, this); } @Override public Loader onCreateLoader(int id, Bundle args) { return new VideoSqliteCursorLoader(getActivity()); } @Override public void onLoadFinished(Loader loader, Cursor data) { mAdapter.swapCursor(data); mAdapter.notifyDataSetChanged(); } @Override public void onLoaderReset(Loader loader) { mAdapter.swapCursor(null); } public void refresh() { mLoader.onContentChanged(); } 

我的CursorAdapter只是一个常规的, newView()被覆盖以返回新膨胀的行布局XML和bindView()使用Cursor将列绑定到行布局中的View


编辑1

在深入研究之后,我认为这里的基本问题是CursorAdapter处理底层Cursor 。 我试图了解它是如何工作的。

采用以下方案以便更好地理解。

  1. 假设CursorLoader已完成加载并返回一个现在有5行的Cursor
  2. Adapter开始显示这些行。 它将Cursor移动到下一个位置并调用getView()
  3. 此时,即使列表视图处于呈现过程中,也会从数据库中删除一行(例如,带_id = 2)。
  4. 这就是问题所在CursorAdapter已将Cursor移动到与已删除行对应的位置。 bindView()方法仍然尝试使用此Cursor访问此行的列,这是无效的,我们得到exception。

题:

  • 这种理解是否正确? 我对上面的第4点特别感兴趣,我假设当一行被删除时, Cursor不会刷新,除非我要求它。
  • 假设这是正确的,我怎么要求我的CursorAdapter丢弃/中止它的ListView呈现, 即使它正在进行中并要求它使用新的Cursor (通过Loader#onContentChanged()Adapter#notifyDatasetChanged() ) ?

PS问题主持人:这个编辑应该转移到一个单独的问题吗?


编辑2

基于各种答案的建议,我对Loader的工作原理的理解似乎存在根本性的错误。 事实certificate:

  1. FragmentAdapter不应该直接在Loader上运行。
  2. Loader应该监视数据的所有变化,并且只要数据发生变化,就应该在onLoadFinished()Adapter新的Cursor

有了这种理解,我尝试了以下改变。 – Loader都不对Loader操作。 刷新方法现在什么都不做。

另外,为了调试LoaderContentObserver内部的ContentObserver ,我想出了这个:

 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(); } } 

这里是我的FragmentLoaderCallback Fragment

 @Override public void onActivityCreated(Bundle savedInstanceState) { super.onActivityCreated(savedInstanceState); mListView = getListView(); /* * Initialize the Loader */ getLoaderManager().initLoader(LOADER_ID, null, this); } @Override public Loader onCreateLoader(int id, Bundle args) { return new VideoSqliteCursorLoader(getActivity()); } @Override public void onLoadFinished(Loader loader, Cursor data) { Utils.logDebug(LOG_TAG, "onLoadFinished()"); mAdapter.swapCursor(data); } @Override public void onLoaderReset(Loader loader) { mAdapter.swapCursor(null); } public void refresh() { Utils.logDebug(LOG_TAG, "CamerasListFragment.refresh() called"); //mLoader.onContentChanged(); } 

现在,只要DB中有更改(添加/删除行),就应该调用ContentObserveronChange()方法 – 正确吗? 我没有看到这种情况发生。 我的ListView从不显示任何更改。 我看到任何更改的唯一一次是我在Loader上显式调用onContentChanged()

这里出了什么问题?


编辑3

好的,所以我重写了我的Loader ,直接从AsyncTaskLoader扩展。 我仍然没有看到我的DB更改被刷新,当我在DB中插入/删除行时,我的LoaderonContentChanged()方法也没有被调用:-(

只是为了澄清一些事情:

  1. 我使用CursorLoader的代码,只修改了一行返回Cursor 。 在这里,我用我的DbManager代码替换了对ContentProvider的调用(后者又使用DatabaseHelper执行查询并返回Cursor )。

    Cursor cursor = AppGlobals.INSTANCE.getDbManager().getAllCameras();

  2. 我在数据库上的插入/更新/删除是从其他地方发生的,而不是通过Loader 。 在大多数情况下,数据库操作发生在后台Service ,在某些情况下,来自Activity 。 我直接使用我的DbManager类来执行这些操作。

我还没有得到的是 – 谁告诉我的Loader已经添加/删除/修改了一行? 换句话说, ForceLoadContentObserver#onChange()在哪里调用? 在我的Loader中,我在Cursor上注册我的观察者:

 void registerContentObserver(Cursor cursor, ContentObserver observer) { cursor.registerContentObserver(mObserver); } 

这意味着在Cursor上有责任在更改时通知mObserver 。 但是,然后AFAIK,’Cursor’不是一个“实时”对象,它更新它所指向的数据以及在数据库中修改数据的时间。

这是我的Loader的最新版本:

 import android.content.Context; import android.database.ContentObserver; import android.database.Cursor; import android.support.v4.content.AsyncTaskLoader; public class VideoSqliteCursorLoader extends AsyncTaskLoader { 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 && oldCursor != cursor && !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 && !cursor.isClosed()) { cursor.close(); } } @Override protected void onReset() { Utils.logDebug(LOG_TAG, "onReset()"); super.onReset(); // Ensure the loader is stopped onStopLoading(); if (mCursor != null && !mCursor.isClosed()) { mCursor.close(); } mCursor = null; } @Override public void onContentChanged() { Utils.logDebug(LOG_TAG, "onContentChanged()"); super.onContentChanged(); } } 

Solutions Collecting From Web of "自定义CursorLoader和支持ListView的CursorAdapter之间的数据不同步"

根据您提供的代码,我不是100%肯定,但有几件事情发生了:

  1. 第一件事就是你已经在ListFragment包含了这个方法:

     public void refresh() { mLoader.onContentChanged(); } 

    使用LoaderManager ,很少需要(通常很危险)直接操作Loader 。 在第一次调用initLoaderLoaderManager可以完全控制Loader并通过在后台调用其方法来“管理”它。 在这种情况下,直接调用Loader的方法时必须非常小心,因为它可能会干扰Loader的底层管理。 我不能确定你对onContentChanged()调用是不正确的,因为你没有在你的post中提到它,但在你的情况下它不应该是必要的(并且都不应该持有对mLoader的引用)。 您的ListFragment不关心如何检测更改…也不关心如何加载数据。 它所知道的是,当onLoadFinished可用时,神奇地提供新数据。

  2. 您也不应该在onLoadFinished调用mAdapter.notifyDataSetChanged()swapCursor会为你做这件事。

在大多数情况下, Loader框架应该完成所有涉及加载数据和管理Cursor的复杂事情。 ListFragment代码应该很简单。


编辑#1:

据我所知, CursorLoader依赖于ForceLoadContentObserverLoader实现中提供的嵌套内部类)…所以看起来这里的问题是你正在实现你的自定义ContentObserver ,但没有什么是设置识别它。 很多“自我通知”的东西都是在LoaderAsyncTaskLoader实现中完成的,因此隐藏在执行实际工作的具体Loader (例如CursorLoader )之后(即Loader不知道CustomForceLoadContentObserver ,为什么它应该收到任何通知?)。

您在更新的post中提到您无法访问final ForceLoadContentObserver mObserver; 直接,因为它是一个隐藏的领域。 您的修复是实现您自己的自定义ContentObserver并在您的覆盖loadInBackground方法中调用registerObserver() (这将导致在您的Cursor上调用registerContentObserver )。 这就是您没有收到通知的原因…因为您使用了Loader框架永远不会识别的自定义ContentObserver

要解决这个问题,你应该让你的类直接extend AsyncTaskLoader而不是CursorLoader (即只需将你从CursorLoaderinheritance的部分复制并粘贴到你的类中)。 这样您就不会遇到隐藏的ForceLoadContentObserver字段的任何问题。

编辑#2:

根据Commonsware的说法 ,没有一种简单的方法来设置来自SQLiteDatabase全局通知,这就是为什么SQLiteCursorLoader ,他的Loaderex库中的Loaderex依赖于Loader调用onContentChanged() 。 直接从数据源广播通知的最简单方法是实现ContentProvider并使用CursorLoader 。 通过这种方式,您可以信任每次Service更新基础数据源时,都会将通知广播到CursorLoader

我不怀疑还有其他解决方案(即可能通过设置全局ContentObserver ……或者甚至可以使用没有 ContentProviderContentResolver#notifyChange方法),但最ContentResolver#notifyChange和最简单的解决方案似乎只是实现一个私人ContentProvider

(ps确保你在清单中的provider标签中设置了android:export="false" ,这样其他应用就无法看到你的ContentProvider !:p)

这不是解决您问题的真正方法,但它可能仍然适合您:

有一个方法CursorLoader.setUpdateThrottle(long delayMS) ,它强制执行loadInBackground完成和下一个正在调度的负载之间的最短时间。

替代方案:

我觉得使用CursoLoader对于这个任务太重了。 需要同步的是数据库添加/删除,它可以在同步方法中完成。 正如我在之前的评论中所说,当mDNS服务停止时,从它中删除db(以同步方式),在接收方中发送删除广播:从数据持有者列表中删除并通知。 这应该足够了。 为了避免使用额外的arraylist(用于支持适配器),使用CursorLoader是额外的工作。


您应该在ListFragment对象上执行一些同步。

应该同步对notifyDatasetChanged()的调用。

 synchronized(this) { // this is ListFragment or ListView. notifyDatasetChanged(); } 

我读了你的整个post,因为我遇到了同样的问题,以下陈述是为我解决了这个问题:

getLoaderManager().restartLoader(0, null, this);

A有同样的问题。 我通过以下方式解决了

 @Override public void onResume() { super.onResume(); // Always call the superclass method first if (some_condition) { getSupportLoaderManager().getLoader(LOADER_ID).onContentChanged(); } }