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

背景:

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

为了简化事情,我们假设用户界面上有一个删除button。 当用户点击这个时,我从数据库中删除一行,并在我的装载器上调用onContentChanged() 。 另外,在onLoadFinished()callbacknotifyDatasetChanged()上,我调用适配器上的notifyDatasetChanged()以刷新UI。

问题:

当删除命令快速连续发生时,意味着onContentChanged()被快速的连续调用, bindView()最终将处理陈旧的数据 。 这意味着一行已被删除,但ListView仍在尝试显示该行。 这导致游标例外。

我究竟做错了什么?

码:

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

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

显示LoaderManagercallback的ListFragment类的代码片段; 以及每当用户添加/删除logging时调用的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<Cursor> onCreateLoader(int id, Bundle args) { return new VideoSqliteCursorLoader(getActivity()); } @Override public void onLoadFinished(Loader<Cursor> loader, Cursor data) { mAdapter.swapCursor(data); mAdapter.notifyDataSetChanged(); } @Override public void onLoaderReset(Loader<Cursor> loader) { mAdapter.swapCursor(null); } public void refresh() { mLoader.onContentChanged(); } 

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


编辑1

深入挖掘这一点后,我认为这里的基本问题是CursorAdapter处理底层Cursor 。 我试图了解如何工作。

为了更好地理解,请采取以下scheme。

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

题:

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

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


编辑2

根据各种答案的build议,看起来我的理解Loader的工作有一个根本的错误。 事实certificate:

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

有了这个理解,我尝试了下面的改变。 – 在Loader上没有任何操作。 刷新方法现在什么都不做。

另外,为了debuggingLoaderContentObserver里面发生了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<Cursor> onCreateLoader(int id, Bundle args) { return new VideoSqliteCursorLoader(getActivity()); } @Override public void onLoadFinished(Loader<Cursor> loader, Cursor data) { Utils.logDebug(LOG_TAG, "onLoadFinished()"); mAdapter.swapCursor(data); } @Override public void onLoaderReset(Loader<Cursor> loader) { mAdapter.swapCursor(null); } public void refresh() { Utils.logDebug(LOG_TAG, "CamerasListFragment.refresh() called"); //mLoader.onContentChanged(); } 

现在,无论何时DB(行添加/删除)发生变化, ContentObserveronChange()方法ContentObserver应该被调用 – 正确吗? 我没有看到这种情况发生。 我的ListView从不显示任何更改。 唯一一次我看到任何改变,如果我明确地调用Loader onContentChanged()

这里怎么了?


编辑3

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

只是为了澄清一些事情:

  1. 我使用了CursorLoader的代码,并修改了一行返回Cursor 。 在这里,我用我的DbManager代码(它反过来使用DatabaseHelper来执行查询并返回Cursor )replace了对ContentProvider的调用。

    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发生变化时, CursorCursor上。 但是,然后AFAIK,“游标”不是一个“实时”对象,它更新指向的数据,以及在数据库中修改数据的时间。

这里是我的Loader的最新版本:

 import android.content.Context; import android.database.ContentObserver; import android.database.Cursor; import android.support.v4.content.AsyncTaskLoader; public class VideoSqliteCursorLoader extends AsyncTaskLoader<Cursor> { 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 "支持ListView的自定义CursorLoader和CursorAdapter之间的数据不同步"

我不是100%确定根据您提供的代码,但有几件事情伸出:

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

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

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

  2. 你也不应该在onLoadFinished调用mAdapter.notifyDataSetChanged()swapCursor将为你做这个。

大多数情况下, Loader框架应该做所有涉及加载数据和pipe理Cursor的复杂事情。 你的ListFragment代码应该比较简单。


编辑#1:

从我所知道的, CursorLoader依赖于ForceLoadContentObserverLoader<D>实现中提供的一个嵌套的内部类)…所以看起来这里的问题是您正在实现您的自定义ContentObserver ,但没有任何设立认识它。 Loader<D>AsyncTaskLoader<D>实现中完成了很多“自我通知”的事情,因此隐藏在实际工作的具体Loader (例如CursorLoader )之外(即Loader<D>不知道CustomForceLoadContentObserver ,为什么它会收到任何通知?)。

您在更新后的post中提到,您无法访问final ForceLoadContentObserver mObserver; 直接,因为它是一个隐藏的领域。 您的修补程序是实现您自己的自定义ContentObserver并在您的重写loadInBackground方法(这将导致registerContentObserver被调用您的Cursor )中调用registerObserver() )。 这就是为什么你没有收到通知…因为你已经使用了一个永远不会被Loader框架识别的定制ContentObserver

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

编辑#2:

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

我不怀疑有其他解决scheme(也许通过build立一个全球性的ContentObserver …或者甚至可以通过使用没有 ContentProviderContentResolver#notifyChange方法),但是最ContentResolver#notifyChange最简单的解决scheme似乎只是实现一个私人ContentProvider

(ps确保你在清单中的provider标记中设置了android:export="false" ,这样你的ContentProvider就不能被其他应用程序看到!):p)

这不是真的解决您的问题,但它可能仍然有一些用处:

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

替代scheme:

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


你应该在ListFragment对象上做一些同步。

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

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

我读了你的整个线程,因为我有同样的问题,下面的语句是什么解决这个问题对我来说:

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