29

バックグラウンド:

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。私はそれがどのように機能するかを理解しようとしています。

理解を深めるために、次のシナリオを取り上げます。

  1. がロードCursorLoaderを終了しCursor、現在 5 行の を返しているとします。
  2. Adapterは、これらの行の表示を開始します。を次の位置に移動しCursorて呼び出すgetView()
  3. この時点で、リスト ビューがレンダリング中であっても、行 (_id = 2 など) がデータベースから削除されます。
  4. これが問題です-が削除された行に対応する位置にCursorAdapter移動しました。CursorメソッドはbindView()引き続き this を使用してこの行の列にアクセスしようとしますがCursor、これは無効であり、例外が発生します。

質問:

  • この理解は正しいでしょうか?上記のポイント 4 に特に興味があります。ここでは、行が削除されたときに、Cursor要求しない限り更新されないという仮定を立てています。
  • これが正しいと仮定すると、進行中CursorAdapterのイベントのレンダリングを破棄/中止し、代わりに新鮮な( andを介して返される) を使用するように依頼するにはどうすればよいですか?ListView CursorLoader#onContentChanged()Adapter#notifyDatasetChanged()

PSモデレーターへの質問: この編集は別の質問に移動する必要がありますか?


編集2

さまざまな回答からの提案に基づいて、 s がどのように機能するかについての私の理解に根本的な誤りがあったようLoaderです。次のことがわかります。

  1. FragmentまたはAdapterを直接操作するべきではありませんLoader
  2. はデータのすべての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

いくつかのことを明確にするために:

  1. のコードを使用しCursorLoaderCursor. ここで、 への呼び出しをContentProvider自分のDbManagerコードに置き換えました (次に、 を使用DatabaseHelperしてクエリを実行し、 を返しますCursor)。

    Cursor cursor = AppGlobals.INSTANCE.getDbManager().getAllCameras();

  2. データベースでの挿入/更新/削除は、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();
    }

}
4

5 に答える 5

13

あなたが提供したコードに基づいて100%確信があるわけではありませんが、いくつかの点が突き出ています:

  1. 最初に目立つのは、このメソッドを に含めたことですListFragment

    public void refresh() {     
        mLoader.onContentChanged();
    }
    

    を使用する場合、 を直接LoaderManager操作する必要はほとんどありません (多くの場合危険です) 。Loaderへの最初の呼び出しの後、initLoaderLoaderManagerを完全に制御しLoader、バックグラウンドでメソッドを呼び出してそれを「管理」します。Loaderこの場合、 s メソッドを直接呼び出すときは十分に注意する必要がありますLoader。. 投稿で言及していないため、への呼び出しonContentChanged()が間違っているとは断言できませんが、状況では必要ないはずです (また、への参照を保持する必要もありませんmLoader)。ListFragment変更がどのように検出されるかは気にしません...また、データがどのようにロードされるかも気にしません。それが知っているのは、新しいデータが魔法のように提供されるということだけですonLoadFinished利用可能な場合。

  2. また、電話mAdapter.notifyDataSetChanged()をかけないでくださいonLoadFinishedswapCursorあなたのためにこれを行います。

ほとんどの場合、Loaderフレームワークは、データのロードと の管理を含むすべての複雑なことを行う必要がありますCursor。あなたのListFragmentコードは比較すると単純でなければなりません。


編集#1:

私が知る限り、は(実装で提供されるネストされた内部クラス) にCursorLoader依存しています... したがって、ここでの問題は、 on custom を実装していることですが、それを認識するように何も設定されていないようです。多くの「自己通知」の処理はandの実装で行われるため、実際の作業を行う具体的なs ( など)から隠されています (つまり、について何も知らないのに、なぜ通知を受け取る必要があるのでしょうか? )。ForceLoadContentObserverLoader<D>ContentObserverLoader<D>AsyncTaskLoader<D>LoaderCursorLoaderLoader<D>CustomForceLoadContentObserver

final ForceLoadContentObserver mObserver;更新された投稿で、隠しフィールドであるため直接アクセスできないと述べました。あなたの修正は、独自のカスタムを実装し、オーバーライドされたメソッドContentObserverを呼び出すことでした(これにより、 が呼び出されます)。これが、通知を受け取らない理由です...フレームワークによって認識されないカスタムを使用したためです。registerObserver()loadInBackgroundregisterContentObserverCursorContentObserverLoader

extend AsyncTaskLoader<Cursor>この問題を解決するには、代わりにクラスを直接作成する必要がありますCursorLoader(つまり、継承元の部分をコピーしCursorLoaderてクラスに貼り付けるだけです)。このようにして、隠しForceLoadContentObserverフィールドで問題が発生することはありません。

編集#2:

Commonsware によると、 からのグローバル通知を設定する簡単な方法はありません。SQLiteDatabaseためSQLiteCursorLoader、彼の、トランザクションが行われるたびに自身呼び出しLoaderex依存していデータ ソースから直接通知をブロードキャストする最も簡単な方法は、 を実装してを使用することです。このようにして、基になるデータ ソースを更新するたびに通知がブロードキャストされることを信頼できますLoaderonContentChanged()ContentProviderCursorLoaderCursorLoaderService

他の解決策があることは間違いありません(つまり、おそらくグローバルを設定することによってContentObserver...または を使用せずContentResolver#notifyChangeメソッドを使用することさえあります)が、最もクリーンで最も簡単な解決策は、プライベートを実装することです。ContentProviderContentProvider

(psandroid:export="false"マニフェストのプロバイダータグに設定して、ContentProvider他のアプリから見られないようにしてください!:p)

于 2012-07-23T16:23:48.050 に答える
3

これは実際にはあなたの問題の解決策ではありませんが、それでも役に立つかもしれません:

CursorLoader.setUpdateThrottle(long delayMS)loadInBackground が完了してから次のロードがスケジュールされるまでの最小時間を強制するメソッドがあります。

于 2012-07-20T20:08:34.853 に答える
2

私は同じ問題を抱えていたので、スレッド全体を読みました。次のステートメントがこの問題を解決したものです。

getLoaderManager().restartLoader(0, null, this);

于 2013-06-06T04:28:10.087 に答える
0

Aさんも同じ問題を抱えていました。私はそれを解決しました:

@Override
public void onResume() {
    super.onResume();  // Always call the superclass method first
    if (some_condition) {
        getSupportLoaderManager().getLoader(LOADER_ID).onContentChanged();
    }
}
于 2013-06-13T13:09:36.937 に答える