背景:
我有一个自定义的CursorLoader
,它直接与SQLite数据库配合工作,而不是使用ContentProvider
。这个加载器与由CursorAdapter
支持的ListFragment
一起工作。到目前为止都很好。
为了简化事情,假设UI上有一个“删除”按钮。当用户点击它时,我会从DB中删除一行,并在我的加载器上调用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<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
的方式。我正在尝试理解它是如何工作的。
为了更好地理解,请看以下场景:
- 假设
CursorLoader
完成加载并返回具有 5 行的Cursor
。 - 适配器开始显示这些行。它移动
Cursor
到下一个位置并调用getView()
。 - 此时,即使列表视图正在被渲染,也会从数据库中删除一行(例如 _id = 2)。
- 问题就出在这里——
CursorAdapter
已将Cursor
移动到对应已删除行的位置。而bindView()
方法仍然尝试使用该Cursor
访问此行的列,导致无效并抛出异常。
问题:
- 我的理解正确吗?我特别关注上面第四点,即当删除行时,只有在请求刷新后,
Cursor
才会刷新。 - 假设我的理解是正确的,那么我如何要求我的
CursorAdapter
放弃 / 中止其正在进行的ListView
渲染,并要求其使用新的Cursor
(通过Loader#onContentChanged()
和Adapter#notifyDatasetChanged()
返回)代替?
P.S. 提问者:这个编辑是否应该移动到一个单独的问题中?
编辑2
根据各种答案的建议,我的理解中存在基本错误,即:
Fragment
或Adapter
不应直接操作Loader
。Loader
应监视所有数据更改,并在数据更改时仅在onLoadFinished()
中将新的Cursor
提供给Adapter
。
在理解了这一点之后,我尝试了以下更改。- 对 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<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
。我仍然没有看到我的DB更改被刷新,也没有看到在DB中插入/删除一行时调用我的Loader
的onContentChanged()
方法:-(
只是为了澄清一些事情:
我使用了
CursorLoader
的代码,并只修改了一个返回Cursor
的单行代码。在这里,我用我的DbManager
代码(它又使用DatabaseHelper
执行查询并返回Cursor
)替换了对ContentProvider
的调用。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'不是一个“活动”对象,它不会在数据在数据库中被修改时更新指向的数据。
这是我的加载器的最新迭代:
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();
}
}
ListView
中的单个行? - curioustechizenLoaderManager
的支持/非支持版本时保持一致,对吗?看起来你正在使用support.v4.content.CursorLoader
和android.content.LoaderManager
...你应该调用getSupportLoaderManager()
而不是吗?并不是说这就是问题所在...但还是要注意。 - Alex Lockwoodint flag
еҸӮж•°з»ҷдҪ зҡ„CursorAdapter
жҳҜ0
пјҢеҜ№еҗ—пјҹпјҲиҖҢдё”дҪ еңЁй—®йўҳдёӯж·»еҠ зҡ„зј–иҫ‘е®Ңе…ЁжІЎй—®йўҳпјҢжҲ‘и§үеҫ—пјүгҖӮ - Alex LockwoodgetLoaderManager
,但这可能是因为我扩展了SherlockListFragment
,它似乎没有一个getSupportLoaderManager()
。CursorAdapter的整数标志是0。 - curioustechizen