fix dokka, update comments, integrate persister (#56)

* fix dokka, update comments, integrate persister
This commit is contained in:
Mike Nakhimovich 2020-01-07 14:08:53 -05:00 committed by GitHub
parent 0cd21be2ee
commit 267f803db0
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
11 changed files with 163 additions and 48 deletions

View file

@ -9,7 +9,7 @@ buildscript {
}
ext.versions = [
androidGradlePlugin : '3.6.0-rc01',
androidGradlePlugin : '4.0.0-alpha07',
dexcountGradlePlugin: '1.0.2',
kotlin : '1.3.61',
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.LibraryPlugin).whenPluginAdded(preDexClosure)
}

View file

@ -1,6 +1,6 @@
#Sat Sep 14 18:42:21 PDT 2019
#Tue Jan 07 09:58:49 EST 2020
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
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

View file

@ -54,3 +54,9 @@ compileTestKotlin {
jvmTarget = "1.8"
}
}
dokka {
outputFormat = 'javadoc'
outputDirectory = "$buildDir/dokkaHtml"
}

View file

@ -1,5 +1,14 @@
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> {
/**
* @param key identifier for data
* @return data for a [key]
*/
suspend fun read(key: Key): Raw?
}

View file

@ -1,10 +1,15 @@
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> {
/**
* @param key to use to get data from persister
* If data is not available implementer needs to
* either return Observable.empty or throw an exception
* @param key to use to get data from a persistent source.
* If data is not available implementer needs to throw an exception
* @return true if data was successfully written
*/
suspend fun write(key: Key, raw: Raw): Boolean
}

View file

@ -3,18 +3,15 @@ package com.dropbox.android.external.store4
import java.util.concurrent.TimeUnit
/**
* MemoryPolicy holds all required info to create MemoryCache and
* [NoopPersister]
* MemoryPolicy holds all required info to create MemoryCache
*
*
* 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.
*
*
* MemoryPolicy is used by a [Store]
* and defines the in-memory cache behavior. It is also used by
* [NoopPersister]
* to define a basic caching mechanism.
* and defines the in-memory cache behavior.
*/
class MemoryPolicy internal constructor(
val expireAfterWrite: Long,

View file

@ -2,16 +2,17 @@ package com.dropbox.android.external.store4
/**
* 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> */
interface Persister<Raw, Key> : DiskRead<Raw, Key>, DiskWrite<Raw, Key> {
/**
* @param key to use to get data from persister
*
* If data is not available implementer needs to
* either return null or throw an exception
* either return null or throw an exception
*/
override suspend fun read(key: Key): Raw?

View file

@ -6,13 +6,40 @@ import kotlinx.coroutines.flow.filterNot
import kotlinx.coroutines.flow.first
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
// Store and also clarify that it still needs to be built/open? (how do we ensure?)
/**
* A Store is responsible for managing a particular data request.
*
* 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> {
/**
* Return a flow for the given key
* @param request - see [StoreRequest] for configurations
*/
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(
StoreRequest.cached(key, refresh = false)
).filterNot {
it is StoreResponse.Loading
}.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(
StoreRequest.fresh(key)
).filterNot {

View file

@ -33,8 +33,25 @@ import kotlinx.coroutines.flow.flow
@ExperimentalCoroutinesApi
interface StoreBuilder<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>
/**
* 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>
/**
* by default a Store caches in memory with a default policy of max items = 16
*/
fun disableCache(): StoreBuilder<Key, Output>
/**
@ -50,7 +67,22 @@ interface StoreBuilder<Key, Output> {
): 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
* ([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
}
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> {
this.scope = scope
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(
reader: (Key) -> Flow<NewOutput?>,
writer: suspend (Key, Output) -> Unit,
@ -217,4 +275,7 @@ private class BuilderWithSourceOfTruth<Key, Input, Output>(
writer: suspend (Key, Output) -> Unit,
delete: (suspend (Key) -> Unit)?
): 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")
}

View file

@ -15,42 +15,54 @@
*/
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(
/**
* The key for the request
*/
val key: Key,
/**
* List of cache types that should be skipped when retuning the response
*/
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
) {
internal fun shouldSkipCache(type: CacheType) = skippedCaches.and(type.flag) != 0
/**
* Factories for common store requests
*/
companion object {
private val allCaches = CacheType.values().fold(0) { prev, next ->
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(
key = key,
skippedCaches = allCaches,
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(
key = key,
skippedCaches = 0,
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(
key = key,
skippedCaches = CacheType.MEMORY.flag,

View file

@ -1,23 +1,12 @@
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
* identifier for a particular [Store]
* Barcode is used as an example unique
* identifier for a particular [com.dropbox.android.external.store4.Store]
*
*
* Barcode will be passed to Fetcher
* and [Persister]
* Barcode will be passed to Fetcher
* and [com.dropbox.android.external.store4.impl.SourceOfTruth]
*/
@Deprecated("here for testing")
data class BarCode(val type: String, val key: String) : Serializable {
companion object {
private val EMPTY_BARCODE = BarCode("", "")
fun empty(): BarCode {
return EMPTY_BARCODE
}
}
}
data class BarCode(val type: String, val key: String)