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 = [ 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)
} }

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 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

View file

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

View file

@ -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?
} }

View file

@ -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
} }

View file

@ -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,

View file

@ -2,16 +2,17 @@ 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
*/ */
override suspend fun read(key: Key): Raw? 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.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 {

View file

@ -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")
} }

View file

@ -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,

View file

@ -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
}
}
}