Three years ago Store4, the little library that could, was released at KotlinConf'19. Store has been simplifying data loading on Android for close to a decade and was supercharged by being 100% Kotlin. Today we're here to talk about the next paradigm shift in data loading Store5 - a Kotlin Multiplatform solution for reading, writing and resolving data conflicts on any platform that Kotlin supports (Android, iOS, Web and Desktop). The Android community has embraced Store for close to a decade, Kotlin is making it possible to adopt the same patterns on other mobile platforms and beyond.
With the addition of support for updating remote sources, network resilience, pain free conflict resolution, and a highly extensible api - Store5 aims to make reading and writing data effortless on all Kotlin platforms. This talk will focus on Store5 foundational concepts and usage in production and at scale. We will be covering adopting KMP, applying Google's offline-first guiding principles beyond Android and how we hope to establish a seamless way for all apps, regardless of platform, to work with local and remote data. This talk is not to be missed for folks (like us) who have battle scars from years of working on hard to fix bugs in offline first applications.
3. 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?
8. 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)
9. 2017
2019
Fresh == skip memory/persister go directly to fetcher
store.fresh(param).subscribe(onNext, onError, onComplete)
Fresh Skip Caches
10. Store Why You Need User Research
2.0 Released 1 month later (oops!)
2017
2019
11. 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
15. 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
16. 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
17. Using Store 4
Examples are from Firefly for Mastodon
https://github.com/digitalbuddha/Firefly
2017
2019
2021
18. 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
45. 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?
46. 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?
50. 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
63. Can’t do with Store 4
Source of Truth to
Repository
Data Flow
64. 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)
82. What we think you should do
Resolve conflicts on remote
83. 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
84. 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
85. 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
86. 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
87. When network response
1. Success
1. Reset stack
2. Reset bookkeeper
2. Failure
1. Log with bookkeeper
Try to update network
88. When network response
1. Success
1. Reset stack
2. Reset bookkeeper
2. Failure
1. Log with bookkeeper
Try to update network
89. When network response
1. Success
1. Reset stack
2. Reset bookkeeper
2. Failure
1. Log with bookkeeper
Try to update network
90. When network response
1. Success
1. Reset stack
2. Reset bookkeeper
2. Failure
1. Log with bookkeeper
Try to update network
91. When network response
1. Success
1. Reset stack
2. Reset bookkeeper
2. Failure
1. Log with bookkeeper
Try to update network
97. 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
98. 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)}
101. 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)}}
102. 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)}}
103. Build Store as normal
val store = StoreBuilder.from(
fetcher = networkFetcher,
sourceOfTruth = sourceOfTruth,
memoryCache = multiCache
).toMutableStoreBuilder<FeatureFlagStatusData,
FeatureFlagStatusData>().build(
updater = updater,
bookkeeper = bookkeeper)