数据在自定义CursorLoader和支持ListView的CursorAdapter之间不同步。
数据在自定义CursorLoader和支持ListView的CursorAdapter之间不同步。
背景:
我有一个自定义的CursorLoader
,它直接与SQLite数据库进行交互,而不是使用ContentProvider
。这个Loader与一个由CursorAdapter
支持的ListFragment
一起工作。到目前为止一切都很好。
为了简化事情,让我们假设UI上有一个删除按钮。当用户单击它时,我会从数据库中删除一行,并在我的Loader上调用onContentChanged()
。此外,在onLoadFinished()
回调中,我调用我的适配器上的notifyDatasetChanged()
以刷新UI。
问题:
当删除操作迅速连续发生时,意味着onContentChanged()
被迅速调用,bindView()
会使用过时的数据。这意味着一行已被删除,但ListView仍在尝试显示该行,这会导致游标异常。
我做错了什么?
代码:
这是一个自定义的CursorLoader(基于Ms. 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()
使用游标将列绑定到行布局中的View
。
编辑1
经过一番研究,我认为这里的根本问题在于CursorAdapter
如何处理基础的Cursor
。我正在尝试理解它的工作方式。
为了更好地理解,考虑以下情形。
- 假设
CursorLoader
已经完成加载并返回一个现在有5行的Cursor
。 - 适配器开始显示这些行。它将
Cursor
移到下一个位置并调用getView()
- 此时,即使列表视图正在呈现过程中,一个行(比如_id = 2)已经从数据库中删除了。
- 问题就在这里 -
CursorAdapter
已将Cursor
移动到对应于已删除行的位置。bindView()
方法仍然尝试使用这个Cursor
访问此行的列,但是这个Cursor
是无效的,我们会得到异常。
问题:
- 这种理解是正确的吗?我特别关心上述第4点,在此我假定当一行被删除时,
Cursor
不会刷新,除非我要求刷新它。 - 假设这是正确的,那么我该如何要求我的
CursorAdapter
放弃/中止正在进行的ListView
呈现,并要求它使用新鲜的Cursor
(通过Loader#onContentChanged()
和Adapter#notifyDatasetChanged()
返回)呢?
P.S. 问题提交给管理人员:这个编辑应该移到单独的问题中吗?
编辑2
根据各种答案的建议,我的Loader的工作原理有一个基本错误。以下是结果:
- 片段或适配器根本不应直接操作Loader。
- Loader应该监视所有数据的更改,并且每当数据更改时,均应在onLoadFinished()中为Adapter提供新的Cursor。
凭借这个理解,我尝试了以下更改。
- Loader上没有任何操作。刷新方法现在不执行任何操作。
此外,为了调试Loader和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(); } }
这是我Fragment和LoaderCallback的代码片段:
@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发生更改(添加/删除行)时,ContentObserver的onChange()方法应该被调用-正确吗?我没有看到这种情况发生。我的ListView从未显示任何更改。唯一见到任何更改的时间是我在Loader上显式调用onContentChanged()的时候。
这里出了什么问题?
编辑3
好的,所以我重新编写了我的Loader直接从AsyncTaskLoader扩展。我仍然看不到我的DB更改被刷新,也没有看到我的Loader的onContentChanged()方法在我插入/删除DB中的行时被调用:-(
仅为澄清一些事情:
- 我使用了
CursorLoader
代码,只修改了一个返回Cursor
的代码行。 在这里,我用我的DbManager
代码替换了对ContentProvider
的调用(而我的DbManager
代码则使用DatabaseHelper
来执行查询并返回Cursor
)。Cursor cursor = AppGlobals.INSTANCE.getDbManager().getAllCameras();
- 我的数据库插入/更新/删除发生在其他地方,而不是通过
Loader
。 在大多数情况下,DB操作在后台Service
中进行,在少数情况下,在Activity
中进行。 我直接使用我的DbManager
类执行这些操作。
我仍然不明白的是 - 是谁告诉我的Loader
添加/删除/修改了一行数据呢?换句话说,在哪里调用ForceLoadContentObserver#onChange()
呢? 在我的Loader中,我在Cursor
上注册了观察者:
void registerContentObserver(Cursor cursor, ContentObserver observer) { cursor.registerContentObserver(mObserver); }
这意味着责任在Cursor
上向mObserver
发送通知。 但是,据我所知,\'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(); } }
基于您提供的代码,我不能完全确定,但有几个问题需要注意:
-
首先,引起注意的是您在
ListFragment
中包含了这个方法:public void refresh() { mLoader.onContentChanged(); }
使用
LoaderManager
时,直接操作Loader
是很少必要(通常是危险的)。在第一次调用initLoader
后,LoaderManager
完全控制Loader
,并通过后台调用其方法"管理"它。在这种情况下,直接调用Loader
的方法时必须非常小心,因为它可能会干扰Loader
的基本管理。我不能确定您对onContentChanged()
的调用是否不正确,因为您在帖子中没有提及它,但在您的情况下不需要它(也不需要持有mLoader
的引用)。您的ListFragment
不关心如何检测更改...也不关心数据如何加载。它只知道在onLoadFinished
中自动提供新的数据。 -
在
onLoadFinished
中,您也不应调用mAdapter.notifyDataSetChanged()
。swapCursor
将为您执行此操作。
在大多数情况下,Loader
框架应该处理所有涉及加载数据和管理Cursor
的复杂事情。相对而言,您的ListFragment
代码应该很简单。
##编辑#1:
据我所知,CursorLoader
依赖ForceLoadContentObserver
(在Loader
实现中提供的嵌套的内部类)...所以问题似乎在于您正在实现自定义的ContentObserver
,但是没有设置能够识别它的内容。在Loader
和AsyncTaskLoader
实现中完成了许多“自我通知”的工作,并且因此从具体的Loader
(例如CursorLoader
)中隐藏了这些工作 (即Loader
对于CustomForceLoadContentObserver
一无所知,那么它为什么会接收到任何通知呢?)。
您在更新的帖子中提到,您无法直接访问final ForceLoadContentObserver mObserver;
,因为它是一个隐藏字段。您的解决方法是实现自己的自定义ContentObserver
,并在您重写的loadInBackground
方法中调用registerObserver()
(这将导致在您的Cursor
上调用registerContentObserver
)。这就是为什么您没有收到通知的原因...因为您使用了一个Loader
框架永远不会识别的自定义ContentObserver
。
为了解决此问题,你应该直接将你的类extend AsyncTaskLoader
而不是CursorLoader
(即只需复制并粘贴你从CursorLoader
继承的部分)。这样,你就不会遇到任何与隐藏的ForceLoadContentObserver
字段相关的问题。
编辑#2:
根据Commonsware的说法,设置来自SQLiteDatabase
的全局通知并不容易,这就是为什么他的Loaderex
库中的SQLiteCursorLoader
依赖于Loader
在每次交易时调用onContentChanged()
。从数据源直接广播通知的最简单方法是实现一个ContentProvider
并使用CursorLoader
。这样,每次你的Service
更新底层数据源时,都可以确信通知将被广播到你的CursorLoader
。
我不怀疑还有其他解决方案(例如通过设置全局ContentObserver
……或甚至使用没有ContentProvider
的ContentResolver#notifyChange
方法),但最干净和最简单的解决方案似乎是只实现一个私有ContentProvider
。
(请注意,在提供程序标记中设置android:export="false"
,以便你的ContentProvider
不能被其他应用程序看到!:p)