web-dev-qa-db-ja.com

「LoaderManager」の「initLoader」と「restartLoader」の違い

initLoaderrestartLoaderLoaderManager関数の違いに関して、私は完全に迷っています。

  • 両方とも同じ署名を持っています。
  • restartLoaderは、存在しない場合もローダーを作成します(「このマネージャーで新しいローダーを起動するか、既存のローダーを再起動します」)。

2つの方法の間に何らかの関係がありますか? restartLoaderを呼び出すと、常にinitLoaderを呼び出しますか? restartLoaderを呼び出さずにinitLoaderを呼び出すことはできますか?データを更新するためにinitLoaderを2回呼び出すのは保存ですか?いつ2つのうちの1つを使用する必要があり、(重要!)なぜですか?

127
theomega

この質問に答えるには、LoaderManagerコードを掘り下げる必要があります。 LoaderManager自体のドキュメントは十分に明確ではありませんが(または、この質問はありません)、抽象的なLoaderManagerのサブクラスであるLoaderManagerImplのドキュメントは、はるかに啓発的です。

initLoader

ローダーで特定のIDを初期化するために呼び出します。このIDに既にローダーが関連付けられている場合、それは変更されずに残り、以前のコールバックは新しく提供されたものに置き換えられます。現在IDのローダーがない場合は、新しいローダーが作成されて開始されます。

通常、この関数は、コンポーネントが初期化するときに使用し、コンポーネントが依存するローダーが作成されるようにします。これにより、既存のローダーのデータが既にある場合はそれを再利用できるため、たとえば、構成の変更後にアクティビティを再作成する場合、ローダーを再作成する必要はありません。

restartLoader

特定のIDに関連付けられたローダーを再作成するための呼び出し。現在、このIDに関連付けられているローダーがある場合、必要に応じてキャンセル/停止/破棄されます。指定された引数を持つ新しいローダーが作成され、そのデータが利用可能になると配信されます。

[...]この関数を呼び出した後、このIDに関連付けられた以前のローダーは無効とみなされ、それらからのさらなるデータ更新は受信しません。

基本的に2つのケースがあります。

  1. IDのローダーは存在しません:両方のメソッドが新しいローダーを作成するので、そこに違いはありません
  2. Idのローダーは既に存在します:initLoaderはパラメーターとして渡されたコールバックのみを置き換えますが、ローダーをキャンセルまたは停止しません。 CursorLoaderの場合、カーソルが開いたままアクティブであることを意味します(initLoader呼び出し前の場合)。一方、restartLoaderは、ローダーをキャンセル、停止、および破棄し(そして、カーソルのような基礎となるデータソースを閉じます)、新しいローダーを作成します(ローダーがCursorLoaderの場合、新しいカーソルを作成し、クエリを再実行します) 。

両方のメソッドの簡略化されたコードは次のとおりです。

initLoader

LoaderInfo info = mLoaders.get(id);
if (info == null) {
    // Loader doesn't already exist -> create new one
    info = createAndInstallLoader(id, args, LoaderManager.LoaderCallbacks<Object>)callback);
} else {
   // Loader exists -> only replace callbacks   
   info.mCallbacks = (LoaderManager.LoaderCallbacks<Object>)callback;
}

restartLoader

LoaderInfo info = mLoaders.get(id);
if (info != null) {
    LoaderInfo inactive = mInactiveLoaders.get(id);
    if (inactive != null) {
        // does a lot of stuff to deal with already inactive loaders
    } else {
        // Keep track of the previous instance of this loader so we can destroy
        // it when the new one completes.
        info.mLoader.abandon();
        mInactiveLoaders.put(id, info);
    }
}
info = createAndInstallLoader(id, args,  (LoaderManager.LoaderCallbacks<Object>)callback);

ローダーが存在しない場合(info == null)にわかるように、両方のメソッドは新しいローダーを作成します(info = createAndInstallLoader(...))。ローダーが既に存在する場合、initLoaderはコールバック(info.mCallbacks = ...)のみを置き換え、restartLoaderは古いローダーを非アクティブにし(新しいローダーが作業を完了すると破棄されます)、新しいものを作成します。

したがって、initLoaderを使用するタイミングとrestartLoaderを使用するタイミング、および2つのメソッドを使用することが理にかなっている理由が明らかになったと述べました。 initLoaderは、初期化されたローダーがあることを確認するために使用されます。存在しない場合は新しいものが作成され、既に存在する場合は再利用されます。実行するクエリ(基本データではなく、CursorLoaderのSQLステートメントのような実際のクエリ)が変更されたため、新しいローダーが必要でない限り、常にこのメソッドを使用します。この場合、restartLoaderを呼び出します。

アクティビティ/フラグメントのライフサイクルは、いずれかの方法を使用する決定とは関係ありません(サイモンが提案したように、ワンショットフラグを使用して呼び出しを追跡する必要はありません)!この決定は、新しいローダーの「必要性」のみに基づいて行われます。同じクエリを実行する場合はinitLoaderを使用し、別のクエリを実行する場合はrestartLoaderを使用します。常にrestartLoaderを使用できますが、それは非効率的です。画面の回転後、またはユーザーがアプリから離れて同じアクティビティに戻った場合、通常は同じクエリ結果を表示したいため、restartLoaderはローダーを不必要に再作成し、基になる(潜在的に高価な)クエリ結果を破棄します。

ロードされるデータと、そのデータをロードするための「クエリ」の違いを理解することは非常に重要です。注文のためにテーブルをクエリするCursorLoaderを使用すると仮定しましょう。そのテーブルに新しい注文が追加されると、CursorLoaderはonContentChanged()を使用して、新しい注文を更新して表示するようにUIに通知します(この場合、restartLoaderを使用する必要はありません)。未処理の注文のみを表示する場合は、新しいクエリが必要です。restartLoaderを使用して、新しいクエリを反映する新しいCursorLoaderを返します。

2つの方法の間に何らかの関係がありますか?

彼らはコードを共有して新しいローダーを作成しますが、ローダーが既に存在する場合は異なることをします。

RestartLoaderの呼び出しは常にinitLoaderを呼び出しますか?

いいえ、決してありません。

InitLoaderを呼び出さずにrestartLoaderを呼び出すことはできますか?

はい。

InitLoaderを2回呼び出してデータを更新しても安全ですか?

InitLoaderを2回呼び出すのは安全ですが、データは更新されません。

いつ2つのうちの1つを使用する必要があり、(重要!)なぜですか?

上記の説明の後、それが(できれば)明確になるはずです。

構成の変更

LoaderManagerは、構成の変更(向きの変更を含む)にわたってその状態を保持するため、私たちにできることは何もないと思われます。もう一度考えて...

まず第一に、LoaderManagerはコールバックを保持しないため、何もしなければonLoadFinished()などのコールバックメソッドの呼び出しを受け取らず、アプリを中断する可能性が非常に高くなります。したがって、少なくともinitLoaderを呼び出してコールバックメソッドを復元する必要があります(もちろん、restartLoaderも可能です)。 ドキュメント 状態:

呼び出し時点で呼び出し元が開始状態にあり、要求されたローダーが既に存在し、そのデータを生成している場合、コールバックonLoadFinished(Loader、D)がすぐに呼び出されます(この関数内)[...]。

つまり、向きの変更後にinitLoaderを呼び出すと、データが既に読み込まれているため、すぐにonLoadFinished呼び出しが取得されます(変更前の場合を想定)。簡単に聞こえますが、注意が必要です(Androidが大好きではないでしょうか...).

2つのケースを区別する必要があります。

  1. 構成の変更自体を処理します。これは、setRetainInstance(true)を使用するフラグメントの場合、またはマニフェスト内のAndroid:configChangesタグに対応するアクティビティの場合です。これらのコンポーネントは、例えば画面の回転。したがって、別のコールバックメソッド(たとえばonActivityCreated(Bundle))でinitLoader/restartLoaderを呼び出すことに注意してください。ローダーを初期化できるようにするには、ローダーIDを保存する必要があります(リストなど)。コンポーネントは構成が変更されても保持されるため、既存のローダーIDをループして、initLoader(loaderid、...)を呼び出すだけです。
  2. 構成の変更自体は処理しません。この場合、ローダーはonCreateで初期化できますが、ローダーIDを手動で保持する必要があります。そうしないと、必要なinitLoader/restartLoader呼び出しを行うことができません。 IDがArrayListに保存されている場合、
    outState.putIntegerArrayList(loaderIdsKey、loaderIdsArray)onSaveInstanceStateで、onCreateでidを復元します:loaderIdsArray = savedInstanceState.getIntegerArrayList(loaderIdsKey)initLoader呼び出しを行います。
200

ローダーが既に作成されているときにinitLoaderを呼び出す(これは通常、構成の変更後などに発生します)LoaderManagerに、ローダーの最新データをonLoadFinishedにすぐに配信するように指示します。ローダーがまだ作成されていない場合(アクティビティ/フラグメントが最初に起動するときなど)、initLoaderへの呼び出しはLoaderManagerにonCreateLoaderを呼び出して新しいローダーを作成するように指示します。

restartLoaderを呼び出すと、既存のローダー(およびそれに関連付けられている既存のデータ)が破棄され、LoaderManagerにonCreateLoaderを呼び出して新しいローダーを作成し、新しいロードを開始するように指示します。


ドキュメントもこれについてかなり明確です:

  • initLoaderは、ローダーが初期化されてアクティブであることを確認します。ローダーがまだ存在しない場合は、ローダーが作成され、(アクティビティ/フラグメントが現在開始されている場合)ローダーが開始されます。そうでない場合、最後に作成されたローダーが再利用されます。

  • restartLoaderは、このマネージャーで新しいローダーを起動するか、既存のローダーを再起動し、コールバックを登録して、(アクティビティ/フラグメントが現在開始されている場合)ロードを開始します。同じIDのローダーが以前に起動されている場合、新しいローダーが作業を完了すると自動的に破棄されます。コールバックは、古いローダーが破棄される前に配信されます。

46
Alex Lockwood

最近、複数のローダーマネージャーと画面の向きの変更で問題が発生しました。多くの試行錯誤の後、アクティビティとフラグメントの両方で次のパターンが機能すると言いたいと思います。

onCreate: call initLoader(s)
          set a one-shot flag
onResume: call restartLoader (or later, as applicable) if the one-shot is not set.
          unset the one-shot in either case.

(つまり、initLoader)がalways一度だけ実行され、restartLoaderが実行されるようにフラグを設定します2番目以降のパススルーonResume

また、アクティビティ内のローダーごとに異なるIDを割り当てることを忘れないでください(番号付けに注意しないと、そのアクティビティ内のフラグメントで少し問題になる可能性があります)


initLoaderのみを使用してみました....効果的に機能しないようでした。

ヌル引数でonCreateinitLoaderを試しました(これは大丈夫と言います) &restartLoader(有効な引数付き)inonResume.... docs間違っている&initLoaderはnullpointer例外をスローします。

試してみましたrestartLoaderのみ...しばらくは機能しますが、5番目または6番目の画面の向きが変更されます。

試してみましたinitLoaderinonResume;再びしばらく動作し、その後吹きます。 (具体的には、「開始されていないときにdoRetainが呼び出されました:」エラー)

以下を試してみました:(コンストラクタに渡されたローダーIDを持つカバークラスからの抜粋)

/**
 * start or restart the loader (why bother with 2 separate functions ?) (now I know why)
 * 
 * @param manager
 * @param args
 * @deprecated use {@link #restart(LoaderManager, Bundle)} in onResume (as appropriate) and {@link #initialise(LoaderManager, Bundle)} in onCreate 
 */
@Deprecated 
public void start(LoaderManager manager, Bundle args) {
    if (manager.getLoader(this.id) == null) {
        manager.initLoader(this.id, args, this);
    } else {
        manager.restartLoader(this.id, args, this);
    }
}

(Stack-Overflowのどこかにありました)

繰り返しますが、これはしばらくは機能しましたが、それでも時折不具合が発生しました。


デバッグ中に理解できることから、initLoader(/ s​​)が必要なインスタンス状態の保存/復元に関係があると思いますサイクルのスピンに耐える場合は、ライフサイクルのonCreate部分で実行します。 (私は間違っているかもしれません。)

結果が別のマネージャーまたはタスクから返されるまで開始できないマネージャーの場合(つまり、onCreateで初期化できない)、 initLoader。 (これは正しくないかもしれませんが、動作しているようです。これらのセカンダリローダーは即時インスタンス状態の一部ではないため、initLoaderを実際に使用できますこの場合は正しい)

lifecycle


ダイアグラムとドキュメントを見ると、initLoaderはアクティビティのonRestartでonCreate&restartLoaderに入るべきだと思っていましたが、フラグメントはいくつかの異なるパターンを使用するため、これが実際に安定しているかどうかを調べる時間はありませんでした。アクティビティのこのパターンで成功した場合、他の誰かがコメントできますか?

16
Simon

ローダーが既に存在する場合、initLoaderは同じパラメーターを再利用します。古いパラメーターが既に読み込まれている場合、新しいパラメーターで呼び出した場合でも、すぐに戻ります。ローダーは、理想的には新しいデータのアクティビティを自動的に通知する必要があります。画面が回転すると、initLoaderが再度呼び出され、古いデータがすぐに表示されます。

restartLoaderは、リロードを強制してパラメーターも変更する場合に使用します。ローダーを使用してログイン画面を作成する場合、ボタンがクリックされるたびにrestartLoaderのみを呼び出します。 (間違った資格情報などにより、ボタンが複数回クリックされる場合があります。)ログインの進行中に画面が回転した場合にアクティビティの保存されたインスタンスの状態を復元するときにのみ、initLoaderを呼び出します。

0
Monstieur