我有一个自定义CursorLoader
,它直接与SQLite数据库一起使用,而不是使用ContentProvider
。此加载程序与ListFragment
支持的CursorAdapter
一起使用。到目前为止一切都很好。
为简化起见,我们假设UI上有一个删除按钮。当用户单击此按钮时,我会从数据库中删除一行,并在我的加载程序上调用onContentChanged()
。另外,在onLoadFinished()
回调中,我在适配器上调用notifyDatasetChanged()
以刷新用户界面。
当删除命令快速连续发生时,意味着快速连续调用onContentChanged()
, bindView()
最终将使用陈旧数据。这意味着行已被删除,但ListView仍在尝试显示该行。这导致了Cursor异常。
我做错了什么?
这是一个自定义的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
课程中显示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()
被覆盖,以使用bindView()
将Cursor
列绑定到{View
来覆盖新增的行布局XML和CursorAdapter
1}}在行布局中。
在深入研究之后,我认为这里的根本问题是Cursor
处理基础CursorLoader
的方式。我试图理解它是如何工作的。
采取以下方案以便更好地理解。
Cursor
已完成加载,并返回Adapter
,现在有5行。Cursor
开始显示这些行。它会将getView()
移至下一个位置并调用CursorAdapter
Cursor
已将bindView()
移至与已删除行对应的位置。 Cursor
方法仍然尝试使用此Cursor
来访问此行的列,这是无效的,我们会获得例外。CursorAdapter
不会刷新,除非我要求它。ListView
放弃/中止Cursor
的呈现,即使它正在进行中并要求它使用新鲜Loader#onContentChanged()
(通过Adapter#notifyDatasetChanged()
和Loader
返回)代替?P.S。主持人的问题:此编辑应该转移到单独的问题吗?
根据各种答案的建议,我理解Fragment
的工作方式似乎存在根本性的错误。事实证明:
Adapter
或Loader
不应直接在Loader
上运作。Adapter
应监控数据中的所有更改,并且只要数据发生更改,就应在Cursor
中为onLoadFinished()
提供新的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();
}
onChange()
现在,只要DB中有更改(添加/删除行),就应该调用ContentObserver
的{{1}}方法 - 正确吗?我没有看到这种情况发生。我的ListView
从未显示任何变化。我发现任何更改的唯一情况是,如果我在onContentChanged()
明确调用Loader
。
这里出了什么问题?
好的,所以我重写了Loader
以直接从AsyncTaskLoader
扩展。我仍然没有看到我的数据库更改被刷新,当我在数据库中插入/删除行时,我的onContentChanged()
的{{1}}方法也没有被调用: - (
只是为了澄清一些事情:
我使用Loader
的代码,只修改了一行返回CursorLoader
的代码。在此,我使用Cursor
代码替换了ContentProvider
的调用(后者又使用DbManager
执行查询并返回DatabaseHelper
)。
Cursor
我在数据库上的插入/更新/删除是从其他地方发生的,而不是通过Cursor cursor = AppGlobals.INSTANCE.getDbManager().getAllCameras();
发生的。在大多数情况下,数据库操作发生在后台Loader
中,在某些情况下,来自Service
。我直接使用我的Activity
类来执行这些操作。
我仍然没有得到的是 - 谁告诉我的DbManager
已添加/删除/修改了一行?换句话说,调用Loader
的位置?在我的Loader中,我在ForceLoadContentObserver#onChange()
:
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<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();
}
}
答案 0 :(得分:13)
根据您提供的代码,我不是100%确定,但有几件事情突然出现:
首先要说明的是,您已将此方法纳入ListFragment
:
public void refresh() {
mLoader.onContentChanged();
}
使用LoaderManager
时,很少需要(通常很危险)直接操纵Loader
。第一次调用initLoader
后,LoaderManager
可以完全控制Loader
,并通过在后台调用其方法来“管理”它。在这种情况下,在直接调用Loader
方法时必须非常小心,因为它可能会干扰Loader
的基础管理。我无法肯定地说您对onContentChanged()
的来电不正确,因为您没有在帖子中提及,但在您的情况下它不应该是必要的(也不应该提及{{1} }})。您的mLoader
并不关心如何检测到更改...也不关心数据的加载方式。它只知道在ListFragment
可用时会神奇地提供新数据。
您也不应在onLoadFinished
中致电mAdapter.notifyDataSetChanged()
。 swapCursor
会为您执行此操作。
在大多数情况下,onLoadFinished
框架应该执行所有涉及加载数据和管理Loader
的复杂事情。您的Cursor
代码应该比较简单。
据我所知, ListFragment
依赖于CursorLoader
(ForceLoadContentObserver
实现中提供的嵌套内部类)...所以看起来似乎是这里的问题是您正在实现自定义Loader<D>
,但没有设置任何内容来识别它。许多“自我通知”内容都是在ContentObserver
和Loader<D>
实现中完成的,因此隐藏在具体的AsyncTaskLoader<D>
(例如Loader
)之外做实际的工作(即CursorLoader
不知道Loader<D>
,为什么它会收到任何通知?)。
您在更新的帖子中提到,您无法直接访问CustomForceLoadContentObserver
,因为它是一个隐藏字段。我们的解决方法是实施您自己的自定义final ForceLoadContentObserver mObserver;
并在您的覆盖ContentObserver
方法中调用registerObserver()
(这会导致loadInBackground
上的registerContentObserver
被调用。这就是您没有收到通知的原因...因为您使用了Cursor
框架永远无法识别的自定义ContentObserver
。
要解决此问题,您应该直接让您的班级 Loader
代替extend AsyncTaskLoader<Cursor>
(即只复制并粘贴您从{{1}继承的部分进入你的班级)。这样,您就不会遇到隐藏CursorLoader
字段的任何问题。
According to Commonsware,没有一种简单的方法来设置来自CursorLoader
的全局通知,这就是为什么ForceLoadContentObserver
库中的SQLiteDatabase
依赖于每次进行交易时SQLiteCursorLoader
都会自行调用Loaderex
。直接从数据源广播通知的最简单方法是实现Loader
并使用onContentChanged()
。这样,您就可以相信每次ContentProvider
更新基础数据源时,都会向CursorLoader
广播通知。
我不怀疑还有其他解决方案(即可能通过设置全局CursorLoader
...或甚至可能使用Service
方法而不是 a ContentObserver
),但最简洁和最简单的解决方案似乎是实现私有ContentResolver#notifyChange
。
(请确保您在清单中的提供者代码中设置ContentProvider
,以便其他应用无法看到您的ContentProvider
!:p)
答案 1 :(得分:3)
对于您的问题,这不是真正的解决方案,但它可能对您有用:
有一个方法CursorLoader.setUpdateThrottle(long delayMS)
,它强制执行loadInBackground完成之间的最短时间,以及正在安排的下一个加载。
答案 2 :(得分:2)
我觉得使用CursoLoader对于这个任务太重了。需要同步的是数据库添加/删除,它可以在同步方法中完成。正如我在之前的评论中所说,当mDNS服务停止时,从中删除db(以同步方式),在接收方中发送删除广播:从数据持有者列表中删除并通知。这应该足够了。为了避免使用额外的arraylist(用于支持适配器),使用CursorLoader是额外的工作。
您应该在ListFragment
对象上进行一些同步。
应同步对notifyDatasetChanged()
的调用。
synchronized(this) { // this is ListFragment or ListView.
notifyDatasetChanged();
}
答案 3 :(得分:1)
我阅读了整个帖子,因为我遇到了同样的问题,以下陈述是为我解决了这个问题:
getLoaderManager().restartLoader(0, null, this);
答案 4 :(得分:0)
A有同样的问题。我解决了它:
@Override
public void onResume() {
super.onResume(); // Always call the superclass method first
if (some_condition) {
getSupportLoaderManager().getLoader(LOADER_ID).onContentChanged();
}
}