【发布时间】:2012-07-17 11:01:36
【问题描述】:
背景:
我有一个自定义的CursorLoader,它直接与 SQLite 数据库一起使用,而不是使用 ContentProvider。此加载程序与由CursorAdapter 支持的ListFragment 一起使用。到目前为止一切顺利。
为了简单起见,我们假设 UI 上有一个删除按钮。当用户单击它时,我会从数据库中删除一行,并在我的加载器上调用onContentChanged()。另外,在onLoadFinished() 回调中,我在我的适配器上调用notifyDatasetChanged() 以刷新UI。
问题:
当删除命令快速连续发生时,意味着onContentChanged() 被快速连续调用,bindView() 最终会处理陈旧数据。这意味着一行已被删除,但 ListView 仍在尝试显示该行。这会导致光标异常。
我做错了什么?
代码:
这是一个自定义 CursorLoader(基于 Diane Hackborn 女士的 this advice)
/**
* 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 类的一个sn-p,它显示了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<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 将列绑定到行布局中的Views。
编辑 1
在深入研究之后,我认为这里的根本问题是CursorAdapter 处理底层Cursor 的方式。我正在尝试了解它是如何工作的。
请看下面的场景以便更好地理解。
- 假设
CursorLoader已完成加载,它返回一个Cursor现在有 5 行。 -
Adapter开始显示这些行。它将Cursor移动到下一个位置并调用getView() - 此时,即使正在呈现列表视图,也会从数据库中删除一行(例如,_id = 2)。
-
这就是问题所在 -
CursorAdapter已将Cursor移动到对应于已删除行的位置。bindView()方法仍然尝试使用此Cursor访问此行的列,这是无效的,我们会遇到异常。
问题:
- 这种理解正确吗?我对上面的第 4 点特别感兴趣,我假设当一行被删除时,
Cursor不会被刷新,除非我要求它。 - 假设这是正确的,我如何让我的
CursorAdapter丢弃/中止其对ListView的渲染即使它正在进行中,并要求它使用新的Cursor(通过Loader#onContentChanged()和Adapter#notifyDatasetChanged()返回)而不是?
P.S.向版主提出的问题:是否应该将此编辑移至单独的问题?
编辑 2
根据各种答案的建议,我对Loaders 工作原理的理解似乎存在根本性错误。原来:
-
Fragment或Adapter根本不应该直接在Loader上运行。 -
Loader应该监控数据的所有变化,并且只要数据发生变化,就应该在onLoadFinished()中为Adapter提供新的Cursor。
有了这种理解,我尝试了以下更改。
- 在Loader 上没有任何操作。 refresh 方法现在什么都不做。
另外,为了调试 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的sn-ps
@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 发生变化(添加/删除行),就应该调用 ContentObserver 的 onChange() 方法 - 对吗?我没有看到这种情况发生。我的ListView 从未显示任何变化。我看到任何变化的唯一一次是我在Loader 上明确调用onContentChanged()。
这里出了什么问题?
编辑 3
好的,所以我重写了我的Loader 以直接从AsyncTaskLoader 扩展。我仍然没有看到我的数据库更改被刷新,当我在数据库中插入/删除一行时,我的Loader 的onContentChanged() 方法也没有被调用:-(
澄清几点:
-
我使用了
CursorLoader的代码,只修改了一行返回Cursor。在这里,我用我的DbManager代码替换了对ContentProvider的调用(它又使用DatabaseHelper执行查询并返回Cursor)。Cursor cursor = AppGlobals.INSTANCE.getDbManager().getAllCameras(); 我对数据库的插入/更新/删除是从其他地方发生的,而不是通过
Loader。在大多数情况下,数据库操作发生在后台Service,在少数情况下,来自Activity。我直接使用我的DbManager类来执行这些操作。
我仍然没有得到的是 - 谁告诉我的 Loader 一行已被添加/删除/修改? 换句话说,ForceLoadContentObserver#onChange() 是在哪里调用的?在我的加载器中,我在Cursor 上注册了我的观察者:
void registerContentObserver(Cursor cursor, ContentObserver observer) {
cursor.registerContentObserver(mObserver);
}
这意味着Cursor 有责任在更改时通知mObserver。但是,然后 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();
}
}
【问题讨论】:
-
你为什么不保持简单:手动删除 UI 元素和 db 行,而不是在每次删除时加载光标..
-
实际上我在这里展示的场景是我的应用程序实际执行的非常简化的形式。话虽如此,如何在不通过适配器的情况下选择性地删除
ListView中的单行? -
我认为同步是更好的解决方案。试试我的答案..
-
要明确一点...您在使用
LoaderManager的支持/非支持版本时保持一致,对吗?看起来您正在使用support.v4.content.CursorLoader和android.content.LoaderManager... 您应该改为调用getSupportLoaderManager()吗?不是说这就是问题……但仍然如此。 -
另外,您将
0作为int flag参数传递给您的CursorAdapter,对吗? (并且您添加到问题中的编辑完全没问题,IMO)。
标签: android android-loadermanager android-cursorloader asynctaskloader android-loader