Dropbox/Storeとは
Store
はデータロードのためのライブラリです.
データの問合せに対して, ネットワーク越しにデータを取得するフェッチャーを定義し, 取得したデータをどのようにキャッシュするのかを決め, 指定したキャッシュポリシーにしたがって, その後のデータ取得を効率的に行うことができます.
Store
にアクセスするクラスは, データの所在(ネットワーク or ディスク or メモリ)を気にすることなくデータ取得することができるようになります.
Store
が主に提供する機能は次の3つです.
- ネットワークを経由してデータをフェッチする方法の宣言(required)
- 取得したデータをメモリまたはディスクにキャッシュする方法の宣言(optional)
- キャッシュのEvictionPolity(optional)
Store
はデータをFlow
で返すためマルチスレッド処理することが容易になるよう設計されています. Flow/Coroutines
による構造化された同時実行性の性質によって, スコープが明確に定義され, メモリリークの減少, パフォーマンスの向上, クラッシュリスクの軽減が期待されます.
Store
によってデータのフェッチ/共有/キャッシュに関するロジックがカプセル化され, ビューで最新のデータを効率的に購読することができ, データをオフラインで使用することもできるようになります.
Store簡単まとめ
- フェッチャーには単一レスポンスと複数レスポンス(
Flow
)のバリエーションがある - ディスクキャッシュする/しないを選べる. メモリキャッシュする/しないを選べる
- メモリキャッシュのEviction Polityには最も過去に生成/更新されたものを破棄, 最終アクセスからn時間経過で破棄, キャッシュの上限個数を指定できる
Store
へのデータ問合せ時にはデータを一意に識別できる汎用キーが必要. これは同一リクエストかの判定やキャッシュヒットの判定に使われ, 汎用キーはKotlin Data Classが推奨される.Store
へのデータ問合せによってLoading
,Data
,Error
のレスポンスがエミットされるStore
へのデータ問合せ中に発生したエラーはError
としてエミットされる- フェッチャーが使う
Flow
のスコープはGlobalScope
- In-flight debouncerが実装されており, 初回の複数同時リクエスト時にもうまくキャッシュが効く
ビルダーによるStoreの構築
Store
はStoreBuilder
によって構築されます.
StoreBuilder
.from(
fetcher = nonFlowValueFetcher { api.fetchSubreddit(it, "10").data.children.map(::toPosts) },
sourceOfTruth = SourceOfTrue.from(
reader = db.postDao()::loadPosts,
writer = db.postDao()::insertPosts,
delete = db.postDao()::clearFeed,
deleteAll = db.postDao()::clearAllFeeds
)
).cachePolicy(
MemoryPolicy.builder()
.setMemorySize(10)
.setExpireAfterAccess(10.minutes) // or setExpireAfterWrite(10.minutes)
.build()
).build()
これは次のことを宣言しています
- 複数回呼び出された場合に備えるフェッチしたデータのメモリキャッシュ
- ネットワークがオフラインの場合に備えたディスクキャッシュ
Store
はネットワークへの過剰な呼び出しを防ぎ, ディスクキャッシュをSource of Truthとして使用することができます. Source of Truthの実装には Room
, SQLDelight
などの監視可能なソースを提供できるデータベースが利用できます.
StoreBuilder.fromNonFlow
fun <Key : Any, Output : Any> fromNonFlow(
fetcher: suspend (key: Key) -> Output
): StoreBuilder<Key, Output>
Flow
を返さないフェッチャーを持つStoreBuilder
を生成します.
リクエストに対してHTTPのように単一の応答を返すフェッチャーを持つStore
を生成します.
StoreBuilder.from
fun <Key : Any, Output : Any> from(
fetcher: (key: Key) -> Flow<Output>
): StoreBuilder<Key, Output> = BuilderImpl(fetcher)
Flow
を返すフェッチャーを持つStoreBuilder
を生成します.
リクエストに対してWebsocketのように複数の応答を返すフェッチャーを持つStore
を生成します.
StoreBuilder.persister
fun <NewOutput : Any> persister(
reader: (Key) -> Flow<NewOutput?>,
writer: suspend (Key, Output) -> Unit,
delete: (suspend (Key) -> Unit)? = null,
deleteAll: (suspend () -> Unit)? = null
): StoreBuilder<Key, NewOutput>
Flow
なディスクキャッシュへアクセスするための reader
, writer
, deleter
を定義します.
柔軟性を確保するため, writer
のレコードタイプ(Output
)とreader
のレコードタイプ(NewOutput
)は異なる型にすることができます. これによって, ネットワークから取得される型とローカルストレージのレコードタイプを分けることができます.
StoreBuilder.nonFlowingPersister
fun <NewOutput : Any> nonFlowingPersister(
reader: suspend (Key) -> NewOutput?,
writer: suspend (Key, Output) -> Unit,
delete: (suspend (Key) -> Unit)? = null,
deleteAll: (suspend () -> Unit)? = null
): StoreBuilder<Key, NewOutput>
Flow
ではないディスクキャッシュへアクセスするための reader
, writer
, deleter
を定義します.
StoreBuilder.cachePolicy
fun cachePolicy(memoryPolicy: MemoryPolicy?): StoreBuilder<Key, Output>
Store
のメモリキャッシュにおけるEvictionポリシーを指定できます.
MemoryPolicy.MemoryPolicyBuilder
で TTLまたは容量ベースのEvictionを設定できます.
ビルダーで特にポリシーの指定がない場合, 次のキャッシュポリシーが適用されます.
- キャッシュできるエントリーの上限個数 = 100
- Evictionポリシー = キャッシュしたエントリーの作成/更新から24時間以上経過したものを破棄
StoreBuilder.disableCache
fun disableCache(): StoreBuilder<Key, Output>
キャッシュ機構を持たないStore
になります.
StoreBuilder.scope
fun scope(scope: CoroutineScope): StoreBuilder<Key, Output>
Store
が汎用キーに対応するデータをフェッチ(あるいはキャッシュヒット)して, 複数の購読者に結果をマルチキャストする際, そのスコープはGlobalScope
となるのがデフォルトの挙動です.
このマルチキャストのスコープを独自にハンドリングしたい場合はこの関数でCoroutineScope
を指定します.
MemoryPolicyBuilder
Store
のメモリキャッシュポリシー(MemoryPolicy
)を定義するためのビルダークラスです.
いわゆるEvictionポリシーはここで指定することになります.
このポリシーは最終的にCache
クラスを生成するパラメータとして利用されます.
MemoryPolicyBuilder.setExpireAfterWrite
fun setExpireAfterWrite(expireAfterWrite: Duration): MemoryPolicyBuilder
キャッシュエントリーが作成 or 置換/更新されてから一定時間後に自動削除するポリシーです.
Duration
が0
で指定されるとキャッシュされなくなります.
MemoryPolicyBuilder.setExpireAfterAccess
fun setExpireAfterAccess(expireAfterAccess: Duration): MemoryPolicyBuilder
キャッシュエントリーが作成 or 置換/更新 or 最後にアクセスされてから一定時間後に自動削除するポリシーです.
Duration
が0
で指定されるとキャッシュされなくなります.
MemoryPolicyBuilder.setMemorySize
fun setMemorySize(maxSize: Long): MemoryPolicyBuilder
キャッシュされるエントリー個数の上限を指定します. エントリー個数が上限を超えた場合, アクセスされた時間の最も古いエントリーが削除対象となります(LRU)
0
が指定されるとすぐにキャッシュを破棄するため, キャッシュされなくなります.
特にビルダーで指定しなかった場合は個数の上限を設けません.
Storeの実装制約
Store
の唯一の実装制約はflow
を返す関数, または特定の型を返すフェッチ用関数(フェッチャー)を実装する必要があることです.
val store = StoreBuilder.from {
articleId -> api.getArticle(articleId) //Flow<Article>
}
.build()
データの識別子
Store
はデータの識別子として汎用キーを使用します.
この汎用キーはtoString()
, equals()
, hashCode()
を適切に実装した値オブジェクトにする必要があります.
汎用キーにはKotlinのdata class
を使うことが強く推奨されます.
この汎用キーはフェッチ関数の引数として渡されます. また, キャッシュのプライマリ識別子としても利用されます.
UIはこの汎用キーさえ知っていれば, いつでもStore
からデータをネットワーク/キャッシュを気にせず再取得できるようになっています.
Stream API
Store
が提供する主要なAPIとしてstream function
があります.
fun stream(request: StoreRequest<Key>): Flow<StoreResponse>Output>>
stream
の呼び出しに渡される StoreRequest
には次の情報が格納されています.
- データを識別するための汎用キー
- キャッシュの利用方針(ディスク/メモリキャッシュの利用有無)
stream
の戻り値はStoreResponse
がFlow
で返されます.
StoreResponse
StoreResponse
はseald class
でサブクラスにはLoading
, Data
, Error
が定義されています.
- それぞれのクラスには
ResponseOrigin
フィールドがあり, データの取得元がキャッシュ or ディスク or フェッチなのかを判別できるようになっています. Loading
はResponseOrigin
のみを持ちます. このクラスはデータのロードをUIに反映するきっかけとして使うことができます.Data
はStore
から返される値を持ったクラスですError
はResponseOrigin
によって投げられた例外をフィールドに持ちます
エラーが発生した場合でもStore
は例外をスローしません. その代わり, StoreResponse.Error
タイプによってこれが表現されます.
これによってFlow
が壊れることがなく, データへの問い合わせやデータの更新が継続して行われることになります.
これによって, UIはflow
の再起動/再接続を意識する必要がなくなります.
lifecycleScope.launchWhenStarted {
store.stream(StoreRequest.cached(key = key, refresh=true)).collect { response ->
when(response) {
is StoreResponse.Loading -> showLoadingSpinner()
is StoreResponse.Data -> {
if (response.origin == ResponseOrigin.Fetcher) hideLoadingSpinner()
updateUI(response.value)
}
is StoreResponse.Error -> {
if (response.origin == ResponseOrigin.Fetcher) hideLoadingSpinner()
showError(response.error)
}
}
}
}
extention function
suspend fun Store.get(key: Key): Value
渡された値に対応するデータを単発取得します.
メモリ/ディスクキャッシュにヒットするデータがあればそこから取得されます.
エラー(StoreResponse.Error
)が発生した場合は例外がスローされ, データがない場合はNullPointerException
がスローされます.
キャッシュされたデータもない初めての Store.get
の呼び出しでは, ネットワークからデータを取得して, ディスク/メモリキャッシュにこれを格納します.
再度, おなじ汎用キーでStore.get
を呼び出したなら, キャッシュからデータを取得して, ネットワーク通信を最低限に抑えようとします.
suspend fun Store.fresh(key: Key): Value
フェッチャーによる問合せによって, ネットワークからデータを単発取得します.
ディスク/メモリキャッシュをスキップしてデータを取得するため, 定期ジョブによるデータ(キャッシュ)更新や, Pull to Refreshによるデータの強制更新時などに利用されます.
suspend fun Store.stream(key: Key): Flow
データを監視してリアルタイムにUI更新をしたい場合などでは Store.stream
が利用できます.
ディスクキャッシュの更新や, ネットワークからのロード/エラーイベントを監視するストリームを作成する方法と考えることができます.
In-flight debouncer
Store
は同じデータに対するリクエストの重複を避けるためにIn-flight debouncerの機能が組み込まれています.
リクエストに対するデータがまだキャッシュされていない場合, 複数個の同じデータに対するリクエストが同時にあると, それぞれが並列に処理されてキャッシュヒットせず, 両方のリクエストがネットワーク通信に至ってしまう可能性があります.
In-flight debouncerはこの不要なネットワーク通信を回避するために, 最初のリクエストはデータ取得のためにブロックされ, 他方の呼び出しはデータの到着を待たせます.