Skip to content
Closed
Show file tree
Hide file tree
Changes from 1 commit
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
66 changes: 66 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,66 @@
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

interface Fetcher<Key, Output> {
suspend operator fun invoke(key: Key): Flow<FetcherResponse<Output>>

companion object {
fun <Key, Output> fromValueFetcher(
doFetch: (key: Key) -> Flow<Output>
): Fetcher<Key, Output> = FlowingValueFetcher(doFetch)

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)

fun <Key, Output> fromNonFlowingFetcher(
doFetch: suspend (key: Key) -> FetcherResponse<Output>
): Fetcher<Key, Output> = NonFlowingFetcher(doFetch)
}
}
Copy link
Contributor

Choose a reason for hiding this comment

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

This would simplify the builder indeed (taking the number of builder create function from 8 to 2 in exchange for adding 4 entry points for the fetcher). I would add some builder extension functions to make the common case easy though.

I would like to keep the fetch and transform as separate functions though. The reasons are:

  1. The user already needs to transform their internal library results to FetcherResult so it's no additional burden
  2. we can enforce that the transform is not a suspend function (it should be quick, the heavy operations should be in the fetcher)
  3. less code to maintain (but we can add an extension function if we want the other api back)

How about something like this?
https://gist.github.com/eyalgu/3d03420f72bc0abed76f88be111ce6d2

Copy link
Contributor

Choose a reason for hiding this comment

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

this looks good to me

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Hmm i'm not sure how much we benefit from doUnwrap being explicit. It is all part of user's code so whether is a suspend or not seems unrelated (re: reason 2)? Maybe i'm not fully getting what you mean there.
In fact, i think it would be more annoyance in terms of API if they needed to provide two separate functions for something they could be just handling within their function.


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<Output>(it))
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package com.dropbox.android.external.store4

sealed class FetcherResponse<T> {
class Value<T>(
val value: T
) : FetcherResponse<T>()

// 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.FetcherStore3
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: FetcherStore3<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.FetcherStore3
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: FetcherStore3<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.FetcherStore3
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: FetcherStore3<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.FetcherStore3
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: FetcherStore3<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 : FetcherStore3<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: FetcherStore3<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
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@
*/
package com.dropbox.android.external.store4

import com.dropbox.android.external.store4.ResponseOrigin.Fetcher
import com.dropbox.android.external.store4.StoreResponse.Data
import com.dropbox.android.external.store4.impl.FetcherController
import com.google.common.truth.Truth.assertThat
Expand All @@ -42,7 +41,7 @@ class FetcherControllerTest {
fun simple() = testScope.runBlockingTest {
val fetcherController = FetcherController<Int, Int, Int>(
scope = testScope,
realFetcher = { key: Int ->
realFetcher = Fetcher.fromValueFetcher{ key: Int ->
flow {
emit(key * key)
}
Expand All @@ -57,7 +56,7 @@ class FetcherControllerTest {
assertThat(received).isEqualTo(
Data(
value = 9,
origin = Fetcher
origin = ResponseOrigin.Fetcher
)
)
assertThat(fetcherController.fetcherSize()).isEqualTo(0)
Expand All @@ -68,7 +67,7 @@ class FetcherControllerTest {
var createdCnt = 0
val fetcherController = FetcherController<Int, Int, Int>(
scope = testScope,
realFetcher = { key: Int ->
realFetcher = Fetcher.fromValueFetcher{ key: Int ->
createdCnt++
flow {
// make sure it takes time, otherwise, we may not share
Expand All @@ -93,7 +92,7 @@ class FetcherControllerTest {
assertThat(it.await()).isEqualTo(
Data(
value = 9,
origin = Fetcher
origin = ResponseOrigin.Fetcher
)
)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@ package com.dropbox.android.external.store4
* @param <Raw> data type before parsing
</Raw> */
@Deprecated("used in tests")
interface Fetcher<Raw, Key> {
// TODO cleanup
interface FetcherStore3<Raw, Key> {

/**
* @param key Container with Key and Type used as a request param
Expand Down
Loading