コンテンツにスキップ

キャッシュのしくみ

2 つの画面は同じリクエストを共有します。古いデータは自動で再取得されます。使われないデータは、デフォルトでは 5 分後にメモリから削除されます。この動作は、次の要素が連携して生み出しています。

用語 説明 API の型
クエリ サーバーデータの定義です。キー、クエリ関数、オプションを持ちます。データは持ちません。 Query、InfiniteQuery
ミューテーション 変更の定義です。ミューテーション関数とオプションを持ちます。状態は持ちません。 Mutation、NoVariablesMutation
クライアント クエリキャッシュとミューテーションキャッシュの所有者です。 QueryClient
キャッシュエントリ 1 つのクライアント内の、1 つのキーのデータと状態です。 CachedQuery
オブザーバー 定義と 1 つのクライアントをつなぐものです。クエリのオブザーバーは 1 つのキャッシュエントリを監視します。ミューテーションのオブザーバーは実行を開始し、最新の実行を報告します。 QueryObserver、InfiniteQueryObserver、MutationObserver
結果 オブザーバーが報告するものです。オブザーバーから見える状態と、refetch や mutate などのアクションを含みます。 QueryResult、InfiniteQueryResult、MutationResult
実行 1 回の mutate 呼び出しです。変数と状態を持ちます。 CachedMutation

定義は、何を取得または変更するかだけを表します。Query はキー、クエリ関数、オプションを持ちます。Mutation はミューテーション関数とオプションを持ちます。どちらもデータやクライアントは持ちません。定義を作っても、何も始まりません。

そのため、定義はトップレベルの値にも、関数で作る値にも、build の中で作る値にもできます。

final todosQuery = Query(
queryKey: ['todos'],
queryFn: (_) => api.getTodos(),
);
Query<Todo> todoQuery(int id) => Query(
queryKey: ['todos', id],
queryFn: (_) => api.getTodo(id),
);

todoQuery(1) を 2 回呼び出すと、キーが ['todos', 1] のオブジェクトが 2 つできます。Fuery はキーを値で比較するので、どちらのオブジェクトも同じキャッシュエントリを使います。キーに入れられるものは、クエリキーにまとめています。

QueryClient は、キャッシュされるすべてのものを 2 つのキャッシュに保持します。

  • QueryCache は、キーごとに 1 つのキャッシュエントリを保持します。
  • MutationCache は、mutate 呼び出しごとに 1 つの実行を保持します。

クライアントは、キャッシュが使うデフォルト設定とストレージも保持します。

Fuery.client はデフォルトのクライアントで、最初に使うときに Fuery が作成します。FueryProvider はサブツリーに専用のクライアントを渡し、その下のウィジェットはそのクライアントを使います。クライアントはそれぞれ独自のキャッシュを持ちます。そのため、2 つのクライアントにある同じキーは、別々のデータを持つ 2 つのキャッシュエントリです。ウィジェット、observe()、クエリ関数がどのクライアントを使うかのルールは、クエリが使うクライアントにまとめています。

オブザーバーは、定義を 1 つのクライアントのキャッシュエントリにつなぎます。オブザーバーを作るものは 2 つあります。

  • 定義を渡されたウィジェットやフックは、マウントされている間、1 つのオブザーバーを保持します。
  • observe() は、Cubit やサービスなど、ウィジェットの外のコードでオブザーバーを作ります。build の中ではなく、1 回だけ呼び出してください。呼び出すたびに新しいオブザーバーができます。

オブザーバーの役割は次のとおりです。

  • 購読:オブザーバーは、最初のリスナーを得たときにキャッシュエントリを購読します。最初のリスナーとは、マウントされたウィジェットやフック、または observe() で作ったオブザーバーへの stream.listen 呼び出しです。オブザーバーは、キャッシュエントリにデータがないとき、またはデータが古いときに取得します(後者は refetchOnMount で制御します)。
  • 取得の共有:キャッシュエントリの取得中に購読したオブザーバーは、その取得に合流します。同時に開いた 2 つの画面が送るリクエストは 1 つです。
  • 購読の解除:最後のリスナーがいなくなると、オブザーバーは購読を解除します。たとえば、ウィジェットがアンマウントされたときや、ストリームの購読がキャンセルされたときです。
  • 独自のオプションの適用:各オブザーバーは、自身の staleTime と refetchOnMount を適用します。2 つの画面が同じキャッシュエントリを監視していても、データが古いかどうかはそれぞれが判断します。
  • キャッシュエントリが残る期間の設定:各オブザーバーは、ガベージコレクション時間の gcTime を指定します。gcTime は、どのオブザーバーも監視しなくなった後に、キャッシュエントリがメモリに残る時間です。キャッシュエントリは、オブザーバーや取得が指定したうち最も長い時間を使います。
  • キーへの追従:ウィジェットが別のキーの定義でリビルドされると、ウィジェットのオブザーバーはそのキーのキャッシュエントリに移ります。
  • 結果の報告:オブザーバーは、変更があるたびに結果を報告します。結果は、キャッシュエントリの状態をオブザーバーのオプションに沿って示します。たとえば isStale は、そのオブザーバー自身の staleTime で判断します。フィールドの一覧は QueryResult のフィールドにあります。

クエリを監視する 2 つの方法は、クエリを使うで紹介しています。オブザーバーを保持するウィジェットの一覧は、ウィジェットにあります。

キャッシュエントリは、次の段階を進みます。各オブザーバーは自身の staleTime で鮮度を判断するので、段階 3 と 4 はオブザーバーごとに異なります。

段階 内容 終了条件
1. 作成 キーに対する最初のオブザーバーまたは client.query 呼び出しが、そのキーのキャッシュエントリを作ります。データはありません。 オブザーバーが購読するか、client.query が取得します。
2. pending クエリ関数が実行されます。status は pending、fetchStatus は fetching です。再試行がアプリのフォアグラウンド復帰を待つ間と、接続状態のソースがある場合にデバイスがオフラインの間は、fetchStatus が paused になります。 データが届きます(success)。または、再試行しても取得が失敗します(error)。
3. 新鮮 データの経過時間が staleTime 未満です(デフォルトはゼロなので、設定しない限りこの段階は飛ばされます)。デフォルトでは、マウント、フォーカス、再接続で再取得しません。 staleTime が経過するか、invalidateQueries がデータを古い状態にします。
4. 古い データは画面に表示されたままです。オブザーバーが購読したとき、アプリがフォアグラウンドに戻ったとき、接続状態のソースがある場合はネットワークに再接続したときに、Fuery がバックグラウンドで再取得します。 再取得で新しいデータが届き、段階 3 に戻ります。
5. 非アクティブ キャッシュエントリを監視するオブザーバーがありません。最後のオブザーバーがいなくなった場合と、client.query や setData だけでデータを入れたキャッシュエントリのように、一度も監視されていない場合です。データはメモリに残るので、戻ってきた画面はすぐにデータを表示します。 オブザーバーが購読します(段階 3 または 4 に戻ります)。または、gcTime が経過します。
6. 削除 オブザーバーがない状態で gcTime(デフォルトは 5 分)が経過すると、Fuery がキャッシュエントリを削除します。永続化されたデータはストレージに残ります。 次にキーが使われると段階 1 から始まり、永続化されたデータがあれば復元します。

ライフサイクルの途中では、次のことも起こります。

  • initialData、setData、永続化されたデータから作られたキャッシュエントリは、データを持った状態で段階 3 または 4 から始まります。
  • オブザーバーが始めた取得は、デフォルトで 3 回再試行します。待ち時間は 1 秒、2 秒、4 秒です。client.query が再試行するのは、クエリまたはクライアントのデフォルト(DefaultOptions、setQueryDefaults)が retry を設定している場合だけです。
  • 最初の取得が失敗すると、キャッシュエントリはデータのない error 状態になります。このキャッシュエントリは、オブザーバーが購読したとき(retryOnMount が false でない限り)と、古いデータと同じくフォーカス時と再接続時に、もう一度取得します。
  • 再取得が失敗しても、データは残ります。status は error になり、データは古いままなので、次のきっかけで再取得します。
  • invalidateQueries は一致するキャッシュエントリを古い状態にし、そのうちオブザーバーが監視しているキャッシュエントリを再取得します。
  • refetchInterval は、このオプションを持つ有効なオブザーバーが購読している間、タイマーで再取得します。refetchIntervalInBackground が true でない限り、アプリがバックグラウンドにある間は一時停止します。
  • staleTime: staticStaleTime のオブザーバーは、invalidateQueries の後も、データをずっと段階 3 に保ちます。
  • removeQueries と clear() はキャッシュエントリをすぐに削除し、永続化されたデータも削除します。
  • 再取得がキャッシュされたデータと等しいデータを返すと、Fuery は以前のオブジェクトを保持します。そのため、データを比較するウィジェットはリビルドされません。リストが変わった場合も、同じインデックスにある以前の項目と等しい項目は、以前のオブジェクトのままです。独自のクラスが等しいとみなされるのは、== を実装している場合だけです。リストへの効果は変わった部分だけをリビルドするで紹介しています。

再取得のきっかけには、それぞれ refetchOnFocus などのオプションがあります。各オプションとそのデフォルトの一覧はクエリのオプションにあります。

ミューテーションは、クエリのようにキャッシュエントリを共有しません。mutate を呼び出すたびに、独自の変数と状態を持つ実行がミューテーションキャッシュに追加されます。実行は、定義やウィジェットではなく、クライアントのキャッシュに属します。

  • 定義からの実行:addTodo.mutate('Buy milk') は、Fuery.client、または渡したクライアントのキャッシュに実行を追加します。この実行を保持するオブザーバーはありません。
  • 最新の実行の表示:MutationResult は、そのオブザーバーが開始した最新の実行を示します。たとえば、1 つの MutationBuilder の実行です。次に MutationResult で mutate を呼び出すとその実行が置き換わり、reset() を呼び出すと結果は idle に戻ります。
  • 実行の分離:同じミューテーションの 2 つの MutationBuilder は、それぞれオブザーバーを保持し、それぞれ自身の実行だけを表示します。複数のウィジェットで 1 つのオブザーバーが必要な場合は、1 つのオブザーバーを共有するで説明しています。
  • すべての実行の検索:MutationStateBuilder、MutationStateListener、MutationStateSelector、useMutationState は、どこで開始された実行でも見つけます。対象は、定義の mutationKey に一致するすべての実行、または MutationFilters に一致するすべての実行です。ミューテーションのすべての実行を表示するを参照してください。
  • 再試行:実行が再試行するのは、ミューテーションまたはクライアントのミューテーションのデフォルト(DefaultOptions、setMutationDefaults)が retry を設定している場合だけです。
  • キャッシュからの削除:実行は、オブザーバーが表示している間は残ります。実行が完了し、表示するオブザーバーがなくなると、Fuery は gcTime(デフォルトは 5 分)の後に実行を削除します。定義から開始した実行にはオブザーバーがないので、Fuery は実行の完了から gcTime 後に削除します。client.clear() はすべての実行をすぐに削除します。

MutationResult と MutationState のフィールドの一覧は、ミューテーションの結果にあります。