fix dokka, update comments, integrate persister (#56)
* fix dokka, update comments, integrate persister
This commit is contained in:
parent
0cd21be2ee
commit
267f803db0
11 changed files with 163 additions and 48 deletions
|
@ -9,7 +9,7 @@ buildscript {
|
||||||
}
|
}
|
||||||
|
|
||||||
ext.versions = [
|
ext.versions = [
|
||||||
androidGradlePlugin : '3.6.0-rc01',
|
androidGradlePlugin : '4.0.0-alpha07',
|
||||||
dexcountGradlePlugin: '1.0.2',
|
dexcountGradlePlugin: '1.0.2',
|
||||||
kotlin : '1.3.61',
|
kotlin : '1.3.61',
|
||||||
dokkaGradlePlugin : '0.10.0',
|
dokkaGradlePlugin : '0.10.0',
|
||||||
|
@ -89,3 +89,5 @@ subprojects {
|
||||||
project.plugins.withType(com.android.build.gradle.AppPlugin).whenPluginAdded(preDexClosure)
|
project.plugins.withType(com.android.build.gradle.AppPlugin).whenPluginAdded(preDexClosure)
|
||||||
project.plugins.withType(com.android.build.gradle.LibraryPlugin).whenPluginAdded(preDexClosure)
|
project.plugins.withType(com.android.build.gradle.LibraryPlugin).whenPluginAdded(preDexClosure)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
4
gradle/wrapper/gradle-wrapper.properties
vendored
4
gradle/wrapper/gradle-wrapper.properties
vendored
|
@ -1,6 +1,6 @@
|
||||||
#Sat Sep 14 18:42:21 PDT 2019
|
#Tue Jan 07 09:58:49 EST 2020
|
||||||
distributionBase=GRADLE_USER_HOME
|
distributionBase=GRADLE_USER_HOME
|
||||||
distributionPath=wrapper/dists
|
distributionPath=wrapper/dists
|
||||||
zipStoreBase=GRADLE_USER_HOME
|
zipStoreBase=GRADLE_USER_HOME
|
||||||
zipStorePath=wrapper/dists
|
zipStorePath=wrapper/dists
|
||||||
distributionUrl=https\://services.gradle.org/distributions/gradle-6.0.1-all.zip
|
distributionUrl=https\://services.gradle.org/distributions/gradle-6.1-milestone-2-all.zip
|
||||||
|
|
|
@ -54,3 +54,9 @@ compileTestKotlin {
|
||||||
jvmTarget = "1.8"
|
jvmTarget = "1.8"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
dokka {
|
||||||
|
outputFormat = 'javadoc'
|
||||||
|
outputDirectory = "$buildDir/dokkaHtml"
|
||||||
|
}
|
|
@ -1,5 +1,14 @@
|
||||||
package com.dropbox.android.external.store4
|
package com.dropbox.android.external.store4
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Interface for retrieving [Raw] data from disk/persistent sources based on a [key] identifier
|
||||||
|
* @param Raw - the type of data returned from a persistent source
|
||||||
|
* @param Key - a unique identifier for data
|
||||||
|
*/
|
||||||
interface DiskRead<Raw, Key> {
|
interface DiskRead<Raw, Key> {
|
||||||
|
/**
|
||||||
|
* @param key identifier for data
|
||||||
|
* @return data for a [key]
|
||||||
|
*/
|
||||||
suspend fun read(key: Key): Raw?
|
suspend fun read(key: Key): Raw?
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,10 +1,15 @@
|
||||||
package com.dropbox.android.external.store4
|
package com.dropbox.android.external.store4
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Interface for saving [Raw] data from disk/persistent sources based on a [key] identifier
|
||||||
|
* @param Raw - the type of data to be saved to a persistent source
|
||||||
|
* @param Key - a unique identifier for data
|
||||||
|
*/
|
||||||
interface DiskWrite<Raw, Key> {
|
interface DiskWrite<Raw, Key> {
|
||||||
/**
|
/**
|
||||||
* @param key to use to get data from persister
|
* @param key to use to get data from a persistent source.
|
||||||
* If data is not available implementer needs to
|
* If data is not available implementer needs to throw an exception
|
||||||
* either return Observable.empty or throw an exception
|
* @return true if data was successfully written
|
||||||
*/
|
*/
|
||||||
suspend fun write(key: Key, raw: Raw): Boolean
|
suspend fun write(key: Key, raw: Raw): Boolean
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,18 +3,15 @@ package com.dropbox.android.external.store4
|
||||||
import java.util.concurrent.TimeUnit
|
import java.util.concurrent.TimeUnit
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* MemoryPolicy holds all required info to create MemoryCache and
|
* MemoryPolicy holds all required info to create MemoryCache
|
||||||
* [NoopPersister]
|
|
||||||
*
|
*
|
||||||
*
|
*
|
||||||
* This class is used, in order to define the appropriate parameters for the MemoryCache
|
* This class is used, in order to define the appropriate parameters for the Memory [com.dropbox.android.external.cache3.Cache]
|
||||||
* to be built.
|
* to be built.
|
||||||
*
|
*
|
||||||
*
|
*
|
||||||
* MemoryPolicy is used by a [Store]
|
* MemoryPolicy is used by a [Store]
|
||||||
* and defines the in-memory cache behavior. It is also used by
|
* and defines the in-memory cache behavior.
|
||||||
* [NoopPersister]
|
|
||||||
* to define a basic caching mechanism.
|
|
||||||
*/
|
*/
|
||||||
class MemoryPolicy internal constructor(
|
class MemoryPolicy internal constructor(
|
||||||
val expireAfterWrite: Long,
|
val expireAfterWrite: Long,
|
||||||
|
|
|
@ -2,14 +2,15 @@ package com.dropbox.android.external.store4
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Interface for fetching data from persister
|
* Interface for fetching data from persister
|
||||||
* when implementing also think about implementing PathResolver to ease in creating primary keys
|
|
||||||
*
|
*
|
||||||
* @param <Raw> data type before parsing
|
* @param <Raw> data type
|
||||||
|
* @param Key unique identifier for data
|
||||||
</Raw> */
|
</Raw> */
|
||||||
interface Persister<Raw, Key> : DiskRead<Raw, Key>, DiskWrite<Raw, Key> {
|
interface Persister<Raw, Key> : DiskRead<Raw, Key>, DiskWrite<Raw, Key> {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param key to use to get data from persister
|
* @param key to use to get data from persister
|
||||||
|
*
|
||||||
* If data is not available implementer needs to
|
* If data is not available implementer needs to
|
||||||
* either return null or throw an exception
|
* either return null or throw an exception
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -6,13 +6,40 @@ import kotlinx.coroutines.flow.filterNot
|
||||||
import kotlinx.coroutines.flow.first
|
import kotlinx.coroutines.flow.first
|
||||||
import kotlinx.coroutines.flow.transform
|
import kotlinx.coroutines.flow.transform
|
||||||
|
|
||||||
// possible replacement for [Store] as an internal only representation
|
/**
|
||||||
// if this class becomes public, should probaly be named IntermediateStore to distingush from
|
* A Store is responsible for managing a particular data request.
|
||||||
// Store and also clarify that it still needs to be built/open? (how do we ensure?)
|
*
|
||||||
|
* When you create an implementation of a Store, you provide it with a Fetcher, a function that defines how data will be fetched over network.
|
||||||
|
*
|
||||||
|
* You can also define how your Store will cache data in-memory and on-disk. See [StoreBuilder] for full configuration
|
||||||
|
*
|
||||||
|
* Example usage:
|
||||||
|
*
|
||||||
|
* val store = StoreBuilder
|
||||||
|
* .fromNonFlow<Pair<String, RedditConfig>, List<Post>> { (query, config) ->
|
||||||
|
* provideRetrofit().fetchData(query, config.limit).data.children.map(::toPosts)
|
||||||
|
* }
|
||||||
|
* .persister(reader = { (query, _) -> db.postDao().loadData(query) },
|
||||||
|
* writer = { (query, _), posts -> db.dataDAO().insertData(query, posts) },
|
||||||
|
* delete = { (query, _) -> db.dataDAO().clearData(query) })
|
||||||
|
* .build()
|
||||||
|
* //single shot response
|
||||||
|
* viewModelScope.launch {
|
||||||
|
* val data = store.fresh(key)
|
||||||
|
* }
|
||||||
|
*
|
||||||
|
* //get cached data and collect future emissions as well
|
||||||
|
* viewModelScope.launch {
|
||||||
|
* val data = store.cached(key, refresh=true)
|
||||||
|
* .collect{data.value=it }
|
||||||
|
* }
|
||||||
|
*
|
||||||
|
*/
|
||||||
interface Store<Key, Output> {
|
interface Store<Key, Output> {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Return a flow for the given key
|
* Return a flow for the given key
|
||||||
|
* @param request - see [StoreRequest] for configurations
|
||||||
*/
|
*/
|
||||||
fun stream(request: StoreRequest<Key>): Flow<StoreResponse<Output>>
|
fun stream(request: StoreRequest<Key>): Flow<StoreResponse<Output>>
|
||||||
|
|
||||||
|
@ -38,12 +65,18 @@ fun <Key, Output> Store<Key, Output>.stream(key: Key) = stream(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper factory that will return data for [key] if it is cached otherwise will return fresh/network data (updating your caches)
|
||||||
|
*/
|
||||||
suspend fun <Key, Output> Store<Key, Output>.get(key: Key) = stream(
|
suspend fun <Key, Output> Store<Key, Output>.get(key: Key) = stream(
|
||||||
StoreRequest.cached(key, refresh = false)
|
StoreRequest.cached(key, refresh = false)
|
||||||
).filterNot {
|
).filterNot {
|
||||||
it is StoreResponse.Loading
|
it is StoreResponse.Loading
|
||||||
}.first().requireData()
|
}.first().requireData()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper factory that will return fresh data for [key] while updating your caches
|
||||||
|
*/
|
||||||
suspend fun <Key, Output> Store<Key, Output>.fresh(key: Key) = stream(
|
suspend fun <Key, Output> Store<Key, Output>.fresh(key: Key) = stream(
|
||||||
StoreRequest.fresh(key)
|
StoreRequest.fresh(key)
|
||||||
).filterNot {
|
).filterNot {
|
||||||
|
|
|
@ -33,8 +33,25 @@ import kotlinx.coroutines.flow.flow
|
||||||
@ExperimentalCoroutinesApi
|
@ExperimentalCoroutinesApi
|
||||||
interface StoreBuilder<Key, Output> {
|
interface StoreBuilder<Key, Output> {
|
||||||
fun build(): Store<Key, Output>
|
fun build(): Store<Key, Output>
|
||||||
|
/**
|
||||||
|
* A store multicasts same [Output] value to many consumers (Similar to RxJava.share()), by default
|
||||||
|
* [Store] will open a global scope for management of shared responses, if instead you'd like to control
|
||||||
|
* the scope that sharing/multicasting happens in you can pass a @param [scope]
|
||||||
|
*
|
||||||
|
* @param scope - scope to use for sharing
|
||||||
|
*/
|
||||||
fun scope(scope: CoroutineScope): StoreBuilder<Key, Output>
|
fun scope(scope: CoroutineScope): StoreBuilder<Key, Output>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* controls eviction policy for a store cache, use [MemoryPolicy.MemoryPolicyBuilder] to configure a TTL
|
||||||
|
* or size based eviction
|
||||||
|
* Example: MemoryPolicy.builder().setExpireAfterWrite(10).setExpireAfterTimeUnit(TimeUnit.SECONDS).build()
|
||||||
|
*/
|
||||||
fun cachePolicy(memoryPolicy: MemoryPolicy?): StoreBuilder<Key, Output>
|
fun cachePolicy(memoryPolicy: MemoryPolicy?): StoreBuilder<Key, Output>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* by default a Store caches in memory with a default policy of max items = 16
|
||||||
|
*/
|
||||||
fun disableCache(): StoreBuilder<Key, Output>
|
fun disableCache(): StoreBuilder<Key, Output>
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -50,7 +67,22 @@ interface StoreBuilder<Key, Output> {
|
||||||
): StoreBuilder<Key, NewOutput>
|
): StoreBuilder<Key, NewOutput>
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Connects a ([Flow]) source of truth that is accessed via [reader], [writer] and [delete].
|
* Connects a ([kotlinx.coroutines.flow.Flow]) source of truth that is accessed via [reader], [writer] and [delete].
|
||||||
|
*
|
||||||
|
* A source of truth is usually backed by local storage. It's purpose is to eliminate the need
|
||||||
|
* for waiting on network update before local modifications are available (via [Store.stream]).
|
||||||
|
*
|
||||||
|
* @param [com.dropbox.android.external.store4.Persister] reads records from the source of truth
|
||||||
|
* WARNING: Delete operation is not supported when using a legacy [com.dropbox.android.external.store4.Persister],
|
||||||
|
* please use another override
|
||||||
|
*/
|
||||||
|
|
||||||
|
fun nonFlowingPersisterLegacy(
|
||||||
|
persister: Persister<Output, Key>
|
||||||
|
): StoreBuilder<Key, Output>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Connects a ([kotlinx.coroutines.flow.Flow]) source of truth that is accessed via [reader], [writer] and [delete].
|
||||||
*
|
*
|
||||||
* For maximal flexibility, [writer]'s record type ([Output]] and [reader]'s record type
|
* For maximal flexibility, [writer]'s record type ([Output]] and [reader]'s record type
|
||||||
* ([NewOutput]) are not identical. This allows us to read one type of objects from network and
|
* ([NewOutput]) are not identical. This allows us to read one type of objects from network and
|
||||||
|
@ -124,6 +156,20 @@ private class BuilderImpl<Key, Output>(
|
||||||
} ?: builder
|
} ?: builder
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun withLegacySourceOfTruth(
|
||||||
|
sourceOfTruth: PersistentNonFlowingSourceOfTruth<Key, Output, Output>
|
||||||
|
) = BuilderWithSourceOfTruth(fetcher, sourceOfTruth).let { builder ->
|
||||||
|
if (cachePolicy == null) {
|
||||||
|
builder.disableCache()
|
||||||
|
} else {
|
||||||
|
builder.cachePolicy(cachePolicy)
|
||||||
|
}
|
||||||
|
}.let { builder ->
|
||||||
|
scope?.let {
|
||||||
|
builder.scope(it)
|
||||||
|
} ?: builder
|
||||||
|
}
|
||||||
|
|
||||||
override fun scope(scope: CoroutineScope): BuilderImpl<Key, Output> {
|
override fun scope(scope: CoroutineScope): BuilderImpl<Key, Output> {
|
||||||
this.scope = scope
|
this.scope = scope
|
||||||
return this
|
return this
|
||||||
|
@ -153,6 +199,18 @@ private class BuilderImpl<Key, Output>(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun nonFlowingPersisterLegacy(
|
||||||
|
persister: Persister<Output, Key>
|
||||||
|
): BuilderWithSourceOfTruth<Key, Output, Output> {
|
||||||
|
val sourceOfTruth: PersistentNonFlowingSourceOfTruth<Key, Output, Output> =
|
||||||
|
PersistentNonFlowingSourceOfTruth(
|
||||||
|
realReader = { key -> persister.read(key) },
|
||||||
|
realWriter = { key, input -> persister.write(key, input) },
|
||||||
|
realDelete = { key -> error("Delete is not implemented in legacy persisters") }
|
||||||
|
)
|
||||||
|
return withLegacySourceOfTruth(sourceOfTruth)
|
||||||
|
}
|
||||||
|
|
||||||
override fun <NewOutput> persister(
|
override fun <NewOutput> persister(
|
||||||
reader: (Key) -> Flow<NewOutput?>,
|
reader: (Key) -> Flow<NewOutput?>,
|
||||||
writer: suspend (Key, Output) -> Unit,
|
writer: suspend (Key, Output) -> Unit,
|
||||||
|
@ -217,4 +275,7 @@ private class BuilderWithSourceOfTruth<Key, Input, Output>(
|
||||||
writer: suspend (Key, Output) -> Unit,
|
writer: suspend (Key, Output) -> Unit,
|
||||||
delete: (suspend (Key) -> Unit)?
|
delete: (suspend (Key) -> Unit)?
|
||||||
): StoreBuilder<Key, NewOutput> = error("Multiple persisters are not supported")
|
): StoreBuilder<Key, NewOutput> = error("Multiple persisters are not supported")
|
||||||
|
|
||||||
|
override fun nonFlowingPersisterLegacy(persister: Persister<Output, Key>): StoreBuilder<Key, Output> =
|
||||||
|
error("Multiple persisters are not supported")
|
||||||
}
|
}
|
||||||
|
|
|
@ -15,42 +15,54 @@
|
||||||
*/
|
*/
|
||||||
package com.dropbox.android.external.store4
|
package com.dropbox.android.external.store4
|
||||||
|
|
||||||
|
/**
|
||||||
|
* data class to represent a single store request
|
||||||
|
* @param key a unique identifier for your data
|
||||||
|
* @param skippedCaches List of cache types that should be skipped when retuning the response see [CacheType]
|
||||||
|
* @param refresh If set to true [Store] will always get fresh value from fetcher while also
|
||||||
|
* starting the stream from the local [com.dropbox.android.external.store4.impl.SourceOfTruth] and memory cache
|
||||||
|
*
|
||||||
|
*/
|
||||||
data class StoreRequest<Key> private constructor(
|
data class StoreRequest<Key> private constructor(
|
||||||
/**
|
|
||||||
* The key for the request
|
|
||||||
*/
|
|
||||||
val key: Key,
|
val key: Key,
|
||||||
/**
|
|
||||||
* List of cache types that should be skipped when retuning the response
|
|
||||||
*/
|
|
||||||
private val skippedCaches: Int,
|
private val skippedCaches: Int,
|
||||||
/**
|
|
||||||
* If set to with stream requests, Store will always get fresh value from fetcher while also
|
|
||||||
* starting the stream from the local data (disk and/or memory cache)
|
|
||||||
*/
|
|
||||||
val refresh: Boolean = false
|
val refresh: Boolean = false
|
||||||
) {
|
) {
|
||||||
|
|
||||||
internal fun shouldSkipCache(type: CacheType) = skippedCaches.and(type.flag) != 0
|
internal fun shouldSkipCache(type: CacheType) = skippedCaches.and(type.flag) != 0
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Factories for common store requests
|
||||||
|
*/
|
||||||
companion object {
|
companion object {
|
||||||
private val allCaches = CacheType.values().fold(0) { prev, next ->
|
private val allCaches = CacheType.values().fold(0) { prev, next ->
|
||||||
prev.or(next.flag)
|
prev.or(next.flag)
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO figure out if any of these helper methods make sense
|
/**
|
||||||
|
* Create a Store Request which will skip all caches and hit your fetcher (filling your caches)
|
||||||
|
*/
|
||||||
fun <Key> fresh(key: Key) = StoreRequest(
|
fun <Key> fresh(key: Key) = StoreRequest(
|
||||||
key = key,
|
key = key,
|
||||||
skippedCaches = allCaches,
|
skippedCaches = allCaches,
|
||||||
refresh = true
|
refresh = true
|
||||||
)
|
)
|
||||||
|
/**
|
||||||
|
* Create a Store Request which will return data from memory/disk caches
|
||||||
|
* @param refresh if true then return fetcher (new) data as well (updating your caches)
|
||||||
|
*/
|
||||||
fun <Key> cached(key: Key, refresh: Boolean) = StoreRequest(
|
fun <Key> cached(key: Key, refresh: Boolean) = StoreRequest(
|
||||||
key = key,
|
key = key,
|
||||||
skippedCaches = 0,
|
skippedCaches = 0,
|
||||||
refresh = refresh
|
refresh = refresh
|
||||||
)
|
)
|
||||||
|
/**
|
||||||
|
* Create a Store Request which will return data from disk cache
|
||||||
|
* @param refresh if true then return fetcher (new) data as well (updating your caches)
|
||||||
|
*/
|
||||||
fun <Key> skipMemory(key: Key, refresh: Boolean) = StoreRequest(
|
fun <Key> skipMemory(key: Key, refresh: Boolean) = StoreRequest(
|
||||||
key = key,
|
key = key,
|
||||||
skippedCaches = CacheType.MEMORY.flag,
|
skippedCaches = CacheType.MEMORY.flag,
|
||||||
|
|
|
@ -1,23 +1,12 @@
|
||||||
package com.dropbox.android.external.store4.legacy
|
package com.dropbox.android.external.store4.legacy
|
||||||
|
|
||||||
import com.dropbox.android.external.store4.Persister
|
|
||||||
import java.io.Serializable
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* [Barcode][BarCode] is used as a unique
|
* Barcode is used as an example unique
|
||||||
* identifier for a particular [Store]
|
* identifier for a particular [com.dropbox.android.external.store4.Store]
|
||||||
*
|
*
|
||||||
*
|
*
|
||||||
* Barcode will be passed to Fetcher
|
* Barcode will be passed to Fetcher
|
||||||
* and [Persister]
|
* and [com.dropbox.android.external.store4.impl.SourceOfTruth]
|
||||||
*/
|
*/
|
||||||
@Deprecated("here for testing")
|
@Deprecated("here for testing")
|
||||||
data class BarCode(val type: String, val key: String) : Serializable {
|
data class BarCode(val type: String, val key: String)
|
||||||
companion object {
|
|
||||||
private val EMPTY_BARCODE = BarCode("", "")
|
|
||||||
|
|
||||||
fun empty(): BarCode {
|
|
||||||
return EMPTY_BARCODE
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
Loading…
Reference in a new issue