バックグラウンド:
CursorLoaderを使用する代わりに、SQLite データベースで直接動作するカスタムがありContentProviderます。このローダーは、 にListFragment裏打ちされた で動作しCursorAdapterます。ここまでは順調ですね。
簡単にするために、UI に [削除] ボタンがあると仮定します。ユーザーがこれをクリックすると、DB から行を削除onContentChanged()し、ローダーも呼び出します。また、onLoadFinished()コールバック時にnotifyDatasetChanged()、UI を更新するためにアダプターを呼び出します。
問題:
削除コマンドが立て続けに実行される場合、つまり が立て続けにonContentChanged()呼び出されると、bindView()古いデータを操作することになります。これは行が削除されたことを意味しますが、ListView はまだその行を表示しようとしています。これにより、カーソル例外が発生します。
私は何を間違っていますか?
コード:
これはカスタムの CursorLoader です ( 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();
}
Myは、オーバーライドされて新しく膨張した行レイアウト XML を返し、を使用して列を行レイアウトの s にバインドするCursorAdapter通常のものです。newView()bindView()CursorView
編集1
これを少し掘り下げた後、ここでの根本的な問題は、CursorAdapterが基になる を処理する方法だと思いますCursor。私はそれがどのように機能するかを理解しようとしています。
理解を深めるために、次のシナリオを取り上げます。
- がロード
CursorLoaderを終了しCursor、現在 5 行の を返しているとします。 Adapterは、これらの行の表示を開始します。を次の位置に移動しCursorて呼び出すgetView()- この時点で、リスト ビューがレンダリング中であっても、行 (_id = 2 など) がデータベースから削除されます。
- これが問題です-が削除された行に対応する位置に
CursorAdapter移動しました。CursorメソッドはbindView()引き続き this を使用してこの行の列にアクセスしようとしますがCursor、これは無効であり、例外が発生します。
質問:
- この理解は正しいでしょうか?上記のポイント 4 に特に興味があります。ここでは、行が削除されたときに、
Cursor要求しない限り更新されないという仮定を立てています。 - これが正しいと仮定すると、進行中
CursorAdapterのイベントのレンダリングを破棄/中止し、代わりに新鮮な( andを介して返される) を使用するように依頼するにはどうすればよいですか?ListViewCursorLoader#onContentChanged()Adapter#notifyDatasetChanged()
PSモデレーターへの質問: この編集は別の質問に移動する必要がありますか?
編集2
さまざまな回答からの提案に基づいて、 s がどのように機能するかについての私の理解に根本的な誤りがあったようLoaderです。次のことがわかります。
FragmentまたはAdapterを直接操作するべきではありませんLoader。- はデータのすべての
Loader変更を監視し、データが変更されるたびAdapterに新しいものを提供する必要があります。CursoronLoadFinished()
この理解を武器に、次の変更を試みました。- 一切の操作はあり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
@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 に変更 (行の追加/削除) があるたびに、のonChange()メソッドをContentObserver呼び出す必要があります - 正しいですか? 私はこれが起こっているのを見ません。私ListViewの変化は決して見られません。変更が見られるのは、 を明示的に呼び出しonContentChanged()た場合だけLoaderです。
ここで何がうまくいかないのですか?
編集3
Loaderわかりましたので、から直接拡張するように書き直しましたAsyncTaskLoader。DB の変更が更新されていることも、DB に行を挿入/削除するときに呼び出されるonContentChanged()メソッドも表示されません:-(Loader
いくつかのことを明確にするために:
のコードを使用し
CursorLoader、Cursor. ここで、 への呼び出しをContentProvider自分のDbManagerコードに置き換えました (次に、 を使用DatabaseHelperしてクエリを実行し、 を返しますCursor)。Cursor cursor = AppGlobals.INSTANCE.getDbManager().getAllCameras();データベースでの挿入/更新/削除は、
Loader. ほとんどの場合、DB 操作はバックグラウンドServiceで行われ、いくつかのケースではActivity.DbManagerクラスを直接使用してこれらの操作を実行します。
私がまだ得ていないのは、行が追加/削除/変更されたことを誰が教えてくれるのですか? Loader言い換えれば、どこでForceLoadContentObserver#onChange()呼ばれますか?ローダーで、オブザーバーを次の場所に登録しますCursor。
void registerContentObserver(Cursor cursor, ContentObserver observer) {
cursor.registerContentObserver(mObserver);
}
Cursorこれは、変更されたときに通知する責任があることを意味しmObserverます。しかし、私の知る限り、「カーソル」は、DBでデータが変更されたときに、それが指しているデータを更新する「ライブ」オブジェクトではありません。
これが私のローダーの最新の反復です。
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();
}
}