Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ buildscript {
}

ext.versions = [
androidGradlePlugin : '4.0.0-beta01',
androidGradlePlugin : '3.6.1',
kotlin : '1.3.70',
dokkaGradlePlugin : '0.10.0',
ktlintGradle : '9.1.1',
Expand Down
25 changes: 25 additions & 0 deletions store/api/store.api
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,30 @@ public abstract interface class com/dropbox/android/external/store4/DiskWrite {
public abstract interface annotation class com/dropbox/android/external/store4/ExperimentalStoreApi : java/lang/annotation/Annotation {
}

public abstract interface class com/dropbox/android/external/store4/Fetcher {
public static final field Companion Lcom/dropbox/android/external/store4/Fetcher$Companion;
public abstract fun invoke (Ljava/lang/Object;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
}

public final class com/dropbox/android/external/store4/Fetcher$Companion {
public final fun fromNonFlowingFetcher (Lkotlin/jvm/functions/Function2;)Lcom/dropbox/android/external/store4/Fetcher;
public final fun fromNonFlowingValueFetcher (Lkotlin/jvm/functions/Function2;)Lcom/dropbox/android/external/store4/Fetcher;
public final fun fromValueFetcher (Lkotlin/jvm/functions/Function1;)Lcom/dropbox/android/external/store4/Fetcher;
}

public abstract class com/dropbox/android/external/store4/FetcherResponse {
}

public final class com/dropbox/android/external/store4/FetcherResponse$Error : com/dropbox/android/external/store4/FetcherResponse {
public fun <init> (Ljava/lang/Throwable;)V
public final fun getError ()Ljava/lang/Throwable;
}

public final class com/dropbox/android/external/store4/FetcherResponse$Value : com/dropbox/android/external/store4/FetcherResponse {
public fun <init> (Ljava/lang/Object;)V
public final fun getValue ()Ljava/lang/Object;
}

public final class com/dropbox/android/external/store4/MemoryPolicy {
public static final field Companion Lcom/dropbox/android/external/store4/MemoryPolicy$Companion;
public static final field DEFAULT_SIZE_POLICY J
Expand Down Expand Up @@ -65,6 +89,7 @@ public abstract interface class com/dropbox/android/external/store4/StoreBuilder
}

public final class com/dropbox/android/external/store4/StoreBuilder$Companion {
public final fun from (Lcom/dropbox/android/external/store4/Fetcher;)Lcom/dropbox/android/external/store4/StoreBuilder;
public final fun from (Lkotlin/jvm/functions/Function1;)Lcom/dropbox/android/external/store4/StoreBuilder;
public final fun fromNonFlow (Lkotlin/jvm/functions/Function2;)Lcom/dropbox/android/external/store4/StoreBuilder;
}
Expand Down
85 changes: 85 additions & 0 deletions store/src/main/java/com/dropbox/android/external/store4/Fetcher.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
package com.dropbox.android.external.store4

import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.map
import java.util.concurrent.CancellationException

/**
* The interface that defines a Fetcher, which is responsible to fetch data from a remote data
* source. (e.g. make API calls).
*
* To create a fetcher, use the convenience methods ([fromValueFetcher], [fromNonFlowingFetcher],
* [fromNonFlowingValueFetcher]).
*/
interface Fetcher<Key, Output> {
suspend operator fun invoke(key: Key): Flow<FetcherResponse<Output>>

companion object {
/**
* Creates a [Fetcher] from the given flow generating function. If the returned [Flow] emits
* an error, it will be wrapped in a [FetcherResponse.Error].
*/
fun <Key, Output> fromValueFetcher(
doFetch: (key: Key) -> Flow<Output>
): Fetcher<Key, Output> = FlowingValueFetcher(doFetch)

/**
* Creates a [Fetcher] from the given function. If it throws an error, the response will be
* wrapped in a [FetcherResponse.Error].
*/
fun <Key, Output> fromNonFlowingValueFetcher(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: fromNonFlowingResponseFetcher sounds better imo

doFetch: suspend (key: Key) -> Output
): Fetcher<Key, Output> = NonFlowingValueFetcher(doFetch)

/**
* Creates a [Fetcher] that returns only 1 value (e.g. a single web request, not a stream).
* An exception thrown from this function will not be caught by Store.
*/
fun <Key, Output> fromNonFlowingFetcher(
doFetch: suspend (key: Key) -> FetcherResponse<Output>
): Fetcher<Key, Output> = NonFlowingFetcher(doFetch)
}
}

internal class NonFlowingFetcher<Key, Output>(
private val doFetch: suspend (key: Key) -> FetcherResponse<Output>
) : Fetcher<Key, Output> {
override suspend fun invoke(key: Key): Flow<FetcherResponse<Output>> {
return flow {
emit(doFetch(key))
}
}
}

internal class NonFlowingValueFetcher<Key, Output>(
private val doFetch: suspend (key: Key) -> Output
) : Fetcher<Key, Output> {
override suspend fun invoke(key: Key): Flow<FetcherResponse<Output>> {
return flow {
try {
emit(FetcherResponse.Value(doFetch(key)))
} catch (th: Throwable) {
if (th is CancellationException) {
throw th
}
emit(
FetcherResponse.Error(th)
)
}
}
}
}

internal class FlowingValueFetcher<Key, Output>(
private val doFetch: (key: Key) -> Flow<Output>
) : Fetcher<Key, Output> {
override suspend fun invoke(key: Key): Flow<FetcherResponse<Output>> {
return doFetch(key).map {
FetcherResponse.Value(it) as FetcherResponse<Output>
}.catch {
emit(FetcherResponse.Error(it))
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package com.dropbox.android.external.store4

/**
* Value type emitted by the [Fetcher]'s `Flow`.
*/
sealed class FetcherResponse<T> {
/**
* Success result, should include a non-null value.
*/
class Value<T>(
val value: T
) : FetcherResponse<T>()

/**
* Error result, it should contain the error information
*/
// TODO support non-throwable errors ?
class Error<T>(
val error: Throwable
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what is the cost of supporting non throwable errors now

) : FetcherResponse<T>()
}
Original file line number Diff line number Diff line change
Expand Up @@ -118,13 +118,14 @@ interface StoreBuilder<Key : Any, Output : Any> {
* @param fetcher a function for fetching network records.
*/
@OptIn(ExperimentalTime::class)
@Deprecated(message = "Creating a flow from a function is deprecated, use Fetcher",
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

eyal@ is this the one you want to keep instead of deprecating ?

replaceWith = ReplaceWith(
expression = "StoreBuilder.from(Fetcher.fromNonFlowingValueFetcher(fetcher))",
imports = ["com.dropbox.android.external.store4.Fetcher"]
))
fun <Key : Any, Output : Any> fromNonFlow(
fetcher: suspend (key: Key) -> Output
): StoreBuilder<Key, Output> = BuilderImpl { key: Key ->
flow {
emit(fetcher(key))
}
}
): StoreBuilder<Key, Output> = BuilderImpl(Fetcher.fromNonFlowingValueFetcher(fetcher))

/**
* Creates a new [StoreBuilder] from a [Flow] fetcher.
Expand All @@ -135,9 +136,18 @@ interface StoreBuilder<Key : Any, Output : Any> {
* @param fetcher a function for fetching a flow of network records.
*/
@OptIn(ExperimentalTime::class)
@Deprecated(message = "Creating a flow from a function is deprecated, use Fetcher",
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this as well. cc: @eyalgu

replaceWith = ReplaceWith(
expression = "StoreBuilder.from(Fetcher.fromValueFetcher(fetcher))",
imports = ["com.dropbox.android.external.store4.Fetcher"]
))
fun <Key : Any, Output : Any> from(
fetcher: (key: Key) -> Flow<Output>
): StoreBuilder<Key, Output> = BuilderImpl(fetcher)
): StoreBuilder<Key, Output> = BuilderImpl(Fetcher.fromValueFetcher(fetcher))

fun <Key : Any, Output : Any> from(
fetcher: Fetcher<Key, Output>
) : StoreBuilder<Key, Output> = BuilderImpl(fetcher)
}
}

Expand All @@ -146,7 +156,7 @@ interface StoreBuilder<Key : Any, Output : Any> {
@ExperimentalStdlibApi
@ExperimentalCoroutinesApi
private class BuilderImpl<Key : Any, Output : Any>(
private val fetcher: (key: Key) -> Flow<Output>
private val fetcher: Fetcher<Key, Output>
) : StoreBuilder<Key, Output> {
private var scope: CoroutineScope? = null
private var cachePolicy: MemoryPolicy? = StoreDefaults.memoryPolicy
Expand Down Expand Up @@ -249,7 +259,7 @@ private class BuilderImpl<Key : Any, Output : Any>(
@ExperimentalStdlibApi
@ExperimentalCoroutinesApi
private class BuilderWithSourceOfTruth<Key : Any, Input : Any, Output : Any>(
private val fetcher: (key: Key) -> Flow<Input>,
private val fetcher: Fetcher<Key, Input>,
private val sourceOfTruth: SourceOfTruth<Key, Input, Output>? = null
) : StoreBuilder<Key, Output> {
private var scope: CoroutineScope? = null
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,14 +15,15 @@
*/
package com.dropbox.android.external.store4.impl

import com.dropbox.android.external.store4.Fetcher
import com.dropbox.android.external.store4.FetcherResponse
import com.dropbox.android.external.store4.ResponseOrigin
import com.dropbox.android.external.store4.StoreResponse
import com.dropbox.flow.multicast.Multicaster
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.FlowPreview
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.emitAll
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.map
Expand All @@ -46,7 +47,7 @@ internal class FetcherController<Key, Input, Output>(
/**
* The function that provides the actualy fetcher flow when needed
*/
private val realFetcher: (Key) -> Flow<Input>,
private val realFetcher: Fetcher<Key, Input>,
/**
* [SourceOfTruth] to send the data each time fetcher dispatches a value. Can be `null` if
* no [SourceOfTruth] is available.
Expand All @@ -65,12 +66,18 @@ internal class FetcherController<Key, Input, Output>(
scope = scope,
bufferSize = 0,
source = flow { emitAll(realFetcher(key)) }.map {
StoreResponse.Data(
it,
origin = ResponseOrigin.Fetcher
) as StoreResponse<Input>
}.catch {
emit(StoreResponse.Error(it, origin = ResponseOrigin.Fetcher))
when (it) {
is FetcherResponse.Value<Input> ->
StoreResponse.Data(
value = it.value,
origin = ResponseOrigin.Fetcher
) as StoreResponse<Input>
is FetcherResponse.Error<Input> ->
StoreResponse.Error(
error = it.error,
origin = ResponseOrigin.Fetcher
)
}
},
piggybackingDownstream = enablePiggyback,
onEach = { response ->
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ package com.dropbox.android.external.store4.impl
import com.dropbox.android.external.cache4.Cache
import com.dropbox.android.external.store4.CacheType
import com.dropbox.android.external.store4.ExperimentalStoreApi
import com.dropbox.android.external.store4.Fetcher
import com.dropbox.android.external.store4.MemoryPolicy
import com.dropbox.android.external.store4.ResponseOrigin
import com.dropbox.android.external.store4.Store
Expand All @@ -44,7 +45,7 @@ import kotlin.time.ExperimentalTime
@FlowPreview
internal class RealStore<Key : Any, Input : Any, Output : Any>(
scope: CoroutineScope,
fetcher: (Key) -> Flow<Input>,
fetcher: Fetcher<Key, Input>,
sourceOfTruth: SourceOfTruth<Key, Input, Output>? = null,
private val memoryPolicy: MemoryPolicy?
) : Store<Key, Output> {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
package com.dropbox.android.external.store3

import com.dropbox.android.external.cache4.Cache
import com.dropbox.android.external.store4.Fetcher
import com.dropbox.android.external.store4.Store3Fetcher
import com.dropbox.android.external.store4.Persister
import com.dropbox.android.external.store4.fresh
import com.dropbox.android.external.store4.get
Expand Down Expand Up @@ -38,7 +38,7 @@ class StoreTest(
) {
private val testScope = TestCoroutineScope()
private val counter = AtomicInteger(0)
private val fetcher: Fetcher<String, BarCode> = mock()
private val fetcher: Store3Fetcher<String, BarCode> = mock()
private var persister: Persister<String, BarCode> = mock()
private val barCode = BarCode("key", "value")

Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
package com.dropbox.android.external.store3

import com.dropbox.android.external.store4.Fetcher
import com.dropbox.android.external.store4.Store3Fetcher
import com.dropbox.android.external.store4.Persister
import com.dropbox.android.external.store4.get
import com.dropbox.android.external.store4.legacy.BarCode
Expand All @@ -26,7 +26,7 @@ class StoreThrowOnNoItems(
) {
private val testScope = TestCoroutineScope()
private val counter = AtomicInteger(0)
private val fetcher: Fetcher<String, BarCode> = mock()
private val fetcher: Store3Fetcher<String, BarCode> = mock()
private var persister: Persister<String, BarCode> = mock()
private val barCode = BarCode("key", "value")

Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
package com.dropbox.android.external.store3

import com.dropbox.android.external.store4.Fetcher
import com.dropbox.android.external.store4.Store3Fetcher
import com.dropbox.android.external.store4.Persister
import com.dropbox.android.external.store4.StoreRequest
import com.dropbox.android.external.store4.fresh
Expand Down Expand Up @@ -32,7 +32,7 @@ class StreamOneKeyTest(
private val storeType: TestStoreType
) {

val fetcher: Fetcher<String, BarCode> = mock()
val fetcher: Store3Fetcher<String, BarCode> = mock()
val persister: Persister<String, BarCode> = mock()
private val barCode = BarCode("key", "value")
private val barCode2 = BarCode("key2", "value2")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ package com.dropbox.android.external.store3

import com.dropbox.android.external.store3.util.KeyParser
import com.dropbox.android.external.store4.Fetcher
import com.dropbox.android.external.store4.Store3Fetcher
import com.dropbox.android.external.store4.MemoryPolicy
import com.dropbox.android.external.store4.Persister
import com.dropbox.android.external.store4.Store
Expand Down Expand Up @@ -44,7 +45,7 @@ data class TestStoreBuilder<Key : Any, Output : Any>(

fun <Key : Any, Output : Any> from(
scope: CoroutineScope,
fetcher: Fetcher<Output, Key>,
fetcher: Store3Fetcher<Output, Key>,
persister: Persister<Output, Key>? = null,
inflight: Boolean = true
): TestStoreBuilder<Key, Output> = from(
Expand All @@ -68,7 +69,7 @@ data class TestStoreBuilder<Key : Any, Output : Any>(
cached = cached,
cacheMemoryPolicy = cacheMemoryPolicy,
persister = persister,
fetcher = object : Fetcher<Output, Key> {
fetcher = object : Store3Fetcher<Output, Key> {
override suspend fun invoke(key: Key): Output = fetcher(key)
}
)
Expand All @@ -84,21 +85,15 @@ data class TestStoreBuilder<Key : Any, Output : Any>(
fetchParser: KeyParser<Key, Output, Output>? = null,
// parser that runs after get from db
postParser: KeyParser<Key, Output, Output>? = null,
fetcher: Fetcher<Output, Key>
fetcher: Store3Fetcher<Output, Key>
): TestStoreBuilder<Key, Output> {
return TestStoreBuilder(
buildStore = {
StoreBuilder
.from { key: Key ->
flow {
val value = fetcher.invoke(key = key)
if (fetchParser != null) {
emit(fetchParser.apply(key, value))
} else {
emit(value)
}
}
}
StoreBuilder.from(
Fetcher.fromNonFlowingValueFetcher {key : Key ->
val value = fetcher.invoke(key = key)
fetchParser?.apply(key, value) ?: value
})
.scope(scope)
.let {
if (cached) {
Expand Down
Loading