Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Meet Store 5 DroidconSF 2023

Meet Store 5 DroidconSF 2023

Avatar for Mike Nakhimovich

Mike Nakhimovich

June 10, 2023
Tweet

More Decks by Mike Nakhimovich

Other Decks in Technology

Transcript

  1. Store is a Java library for effortless, reactive data loading.

    Guarantee things [you don’t want to think about] are done in the same way What is Store?
  2. Store Version 1.0 Api 2017 2019 Store [was] a Java

    library for effortless, reactive data loading built with RxJava Store<Article> Store = ParsingStoreBuilder.<BufferedSource, String>builder() .fetcher(this::ResponseAsSource) .persister(SourcePersisterFactory.create(context.getFilesDir()) .parser(GsonParserFactory.createSourceParser(gson, Article.class)) .open(); Barcode barcode = new Barcode("Article", "42"); store.get(barcode).subscribe(onNext, onError, onComplete)
  3. 2017 2019 Fresh == skip memory/persister go directly to fetcher

    store.fresh(param).subscribe(onNext, onError, onComplete) Fresh Skip Caches
  4. 3.0 Api Rxjava2 RxSingle instead of Observable Store<Article> Store =

    ParsingStoreBuilder.<BufferedSource,ArticleParam, String>builder() .fetcher(this::ResponseAsSource) //responseBody.source() .persister(SourcePersisterFactory.create(context.getFilesDir()) .parser(GsonParserFactory.createSourceParser(gson, Article.class)) .open(); ArticleParam param = new ArticleParam("42"); store.get(param).subscribe(onNext, onError) 2017 2019 Store
  5. Hello Kotlin 2017 2019 2021 Store4 StoreBuilder.from { api.fetchSubreddit(it, "10")}

    .sourceOfTruth( reader = db.postDao()::loadPosts, writer = db.postDao()::insertPosts, delete = db.postDao()::clearFeed) .cachePolicy(MemoryPolicy) .build() Api
  6. Streaming as first class citizen 2017 2019 2021 fun stream(request:

    StoreRequest<Key>): Flow<StoreResponse>Output>> lifecycleScope.launchWhenStarted { store.stream(StoreRequest.cached(3, refresh = false)) .collect{ } store.stream(StoreRequest.get(3)) .collect{ storeResponse -> } Store4 Api
  7. Loading|Content|Error Return Types 2017 2019 2021 Store 4 store.stream(StoreRequest.cached(key =

    key, refresh=true)).collect { response -> when(response) { is StoreResponse.Loading -> showLoadingSpinner() is StoreResponse.Data -> { if (response.origin == ResponseOrigin.Fetcher) hideLoadingSpinner() updateUI(response.value) } is StoreResponse.Error -> { if (response.origin == ResponseOrigin.Fetcher) hideLoadingSpinner() showError(response.error) }}}} LCE
  8. You create a Store using a builder. The only requirement

    is to include a Fetcher which is just a typealias to a function that returns a Flow<FetcherResult<ReturnType>>. //build it val store = StoreBuilder.from( Fetcher.of { key: Unit -> userApi.noti fi cations( authHeader = " Bearer ${oauthRepository.getCurrent()}", offset = null) }).build() //use it store.stream(StoreRequest.cached(Unit, refresh = true)).map { it.dataOrNull() } .collect{response-> } Fetcher as a Function 2017 2019 2021
  9. Source of Truth suspend fun fetcher(key:String): Token = api.createAccessToken(key) fun

    sourceOfTruth = SourceOfTruth.of<String, Token, String>( reader = {dataStore.data.map { it.currentUser?.accessToken }}, writer = { _, token -> dataStore.updateData { it.user.copy(accessToken = token.accessToken) }}) private val userTokenStore: Store<String, String> = StoreBuilder.from( fetcher = Fetcher.of { key: String -> fetcher(key) }, sourceOfTruth = sourceOfTruth ).build() 2017 2019 2021
  10. Mike: What is the bene fi t of starting over?

    Matt: Hmm Mike: Is it easier to add old to new or new to old?
  11. Some time later… (half hour) Matt: safeguards will be easier

    to add to new Matt: because I don’t understand old Mike: SGTM as long as all tests are migrated, I also don’t understand old Matt: Sure how hard can it be?
  12. More time passes Mike: My spider sense is tingling, I

    found more gaps in new Mike: let’s sit together and go through Store4 code
  13. Few weeks later… Matt:Just finished last test cases everything passes

    Mike: Everything? Matt: Yup in iOS, android, JVM and JS, I took all the work https://github.com/aclassen did and got it to pass tests
  14. interface Updater<Key, Output, Response> { suspend fun post(key: Key, value:

    Output): UpdaterResult val onCompletion: OnUpdaterCompletion<Response>?} Store 5 New Mutation Api Creating an Updater (like a fetcher)
  15. val updater = Updater.by( post = { _, hike ->

    try { val latest = api.updateHike(hike) UpdaterResult.Success.Typed(latest) } catch (error: Throwable) { UpdaterResult.Error.Exception(error) }}) Store 5 Updater Factory
  16. class RealHikeManager @Inject constructor( private val mutableStore: MutableStore<Int, Hike>, private

    val location:locationRepository, private val map:mapRepository ): HikeManager { private suspend fun track() { combine(location.current(), map.current()) { location, map -> val request = createStoreWriteRequest(location, map) val response = mutableStore.write(request) handleResponse(response) }}} Store 5 Usage of MutableStore
  17. class RealHikeManager @Inject constructor( private val mutableStore: MutableStore<Int, Hike>, private

    val location:locationRepository, private val map:mapRepository ): HikeManager { private suspend fun track() { combine(location.current(), map.current()) { location, map -> val request = createStoreWriteRequest(location, map) val response = mutableStore.write(request) handleResponse(response) }}} Store 5 Usage of MutableStore
  18. Store 5 Bookkeeper Api interface Bookkeeper<Key : Any> { suspend

    fun getLastFailedSync(key: Key): Long? suspend fun setLastFailedSync(key: Key, timestamp: Long): Boolean suspend fun clear(key: Key): Boolean suspend fun clearAll(): Boolean}
  19. Store 5 - Any Conflicts? RealMutableStore private suspend fun con

    fl ictsMightExist(key: Key): Boolean { val lastFailedSync = bookkeeper?.getLastFailedSync(key) return lastFailedSync != null || writeRequestsQueueIsEmpty(key).not() }
  20. On each write request 1. Init thread safety (key-scoped) 2.

    Add request to stack (key-scoped) 3. Write to memory cache + SOT!!! 4. Try to update network (posting latest) Mutations In the Weeds
  21. On each write request 1. Init thread safety (key-scoped) 2.

    Add request to stack (key-scoped) 3. Write to memory cache + SOT!!! 4. Try to update network (posting latest) In the Weeds Mutations
  22. On each write request 1. Init thread safety (key-scoped) 2.

    Add request to stack (key-scoped) 3. Write to memory cache + SOT!!! 4. Try to update network (posting latest) In the Weeds Mutations
  23. On each write request 1. Init thread safety (key-scoped) 2.

    Add request to stack (key-scoped) 3. Write to memory cache + SOT!!! 4. Try to update network (posting latest) In the Weeds Mutations
  24. When network response 1. Success 1. Reset stack 2. Reset

    bookkeeper 2. Failure 1. Log with bookkeeper Try to update network
  25. When network response 1. Success 1. Reset stack 2. Reset

    bookkeeper 2. Failure 1. Log with bookkeeper Try to update network
  26. When network response 1. Success 1. Reset stack 2. Reset

    bookkeeper 2. Failure 1. Log with bookkeeper Try to update network
  27. When network response 1. Success 1. Reset stack 2. Reset

    bookkeeper 2. Failure 1. Log with bookkeeper Try to update network
  28. When network response 1. Success 1. Reset stack 2. Reset

    bookkeeper 2. Failure 1. Log with bookkeeper Try to update network
  29. Store 5 Fetcher Api fun <Key : Any, Network :

    Any> of( name: String? = null, fetch: suspend (key: Key) -> Network ): Fetcher<Key, Network> = ofFlow(name, fetch.asFlow())
  30. fun <Key : Any, Network : Any> of( name: String?

    = null, fallback: Fetcher<Key, Network>, fetch: suspend (key: Key) -> Network ): Fetcher<Key, Network> = ofFlowWithFallback(name, fallback, fetch.asFlow()) Store 5 Fetcher Api with fallback
  31. data class FeatureFlag( val key: String, val name: String, val

    description: String, val kind: Kind, val version: Int, val creationDate: Long, ) : Identi fi able<String> { enum class Kind { Boolean, Multivariate}} Trails Feature Flag Model
  32. Trails Feature Flag Status Model sealed class FeatureFlagStatus: Identi fi

    able<String> { data class Multivariate( val key: String, val value: FeatureFlagVariation, val lastRequested: Long, val links: Links)}
  33. Network Feature Flag Status Fetcher With Fallback val networkFetcher =

    Fetcher.ofWithFallback( name = “networkFetcher”, fallback = hardcodedFetcher ) { key -> when (key) { is Collection -> fetchFeatureFlagStatuses(key.userId) is Single -> fetchFeatureFlagStatus(key.userId, key. fl agId)}}
  34. Hardcoded Feature Flag Status Fetcher val hardcodedFetcher = Fetcher.of( name

    = “hardcodedFetcher” ) { key -> when (key) { is Collection -> loadFeatureFlagStatuses(key.userId) is Single -> loadFeatureFlagStatus(key.userId, key. fl agId)}}
  35. Build Store as normal val store = StoreBuilder.from( fetcher =

    networkFetcher, sourceOfTruth = sourceOfTruth, memoryCache = multiCache ).toMutableStoreBuilder<FeatureFlagStatusData, FeatureFlagStatusData>().build( updater = updater, bookkeeper = bookkeeper)