Skip to content

Commit 2a560d8

Browse files
committed
Add LRU Cache (Memory)
1 parent 75d1075 commit 2a560d8

15 files changed

Lines changed: 387 additions & 175 deletions

File tree

.github/workflows/gradle.yml

Lines changed: 0 additions & 52 deletions
This file was deleted.

.github/workflows/publish.yml

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,5 @@
11
name: Publish
22
on:
3-
release:
4-
types: [released, prereleased]
53
push:
64
tags:
75
- '*'
@@ -15,8 +13,8 @@ jobs:
1513
- name: Set up JDK 21
1614
uses: actions/setup-java@v4
1715
with:
18-
distribution: 'zulu'
19-
java-version: 21
16+
distribution: 'microsoft'
17+
java-version: 24
2018
- name: Publish to MavenCentral
2119
run: ./gradlew publishToMavenCentral --no-configuration-cache
2220
env:

.github/workflows/test.yml

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
name: Tests
2+
3+
on:
4+
push:
5+
branches: [ "master" ]
6+
pull_request:
7+
branches: [ "master" ]
8+
9+
permissions:
10+
contents: read
11+
12+
jobs:
13+
test:
14+
runs-on: ubuntu-latest
15+
steps:
16+
- name: Checkout repository
17+
uses: actions/checkout@v4
18+
19+
- name: Set up JDK 21
20+
uses: actions/setup-java@v4
21+
with:
22+
distribution: 'microsoft'
23+
java-version: 21.0.2
24+
25+
- name: Set up Gradle
26+
uses: gradle/actions@v3
27+
28+
- name: Run Tests
29+
run: ./gradlew :reflow:cleanJvmTest :reflow:jvmTest

README.md

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
# Reflow
2+
[![Maven Central](https://github.com/AraujoJordan/Reflow/actions/workflows/publish.yml/badge.svg)](https://github.com/AraujoJordan/Reflow/actions/workflows/publish.yml) [![Tests](https://github.com/AraujoJordan/Reflow/actions/workflows/test.yml/badge.svg)](https://github.com/AraujoJordan/Reflow/actions/workflows/test.yml)
23

34
A Kotlin Multiplatform Flow that simplifies data fetching with automatic retry logic, loading state management, and flexible caching strategies.
45
It is designed to integrate seamlessly with Jetpack Compose.
@@ -13,11 +14,11 @@ class MyViewModel : ViewModel() {
1314
}
1415
```
1516

16-
you can also cache it in disk with:
17+
you can also cache it with:
1718

1819
```kotlin
1920
class MyViewModel : ViewModel() {
20-
val uiState = reflow(cacheSource = CacheSource.Disk()) {
21+
val uiState = reflow(cacheSource = CacheSource.Disk()) { // Or CacheSource.Memory() for a LRU cache
2122
api.fetchData()
2223
}
2324
}
@@ -83,7 +84,7 @@ fun UserListScreen(viewModel: MyViewModel) {
8384
## Features
8485

8586
- 🌐 **Kotlin Multiplatform** - Works on Android, iOS, and JVM
86-
- 🎨 **Flexible Configuration** - Customizable loading states, error handling and retry attempts
87+
- 🎨 **Flexible Configuration** - Customizable loading states, error handling, and retry attempts
8788
- ✈️ **Offline-first Support** - Reflow can cache the fetched data on disk for offline-first apps support
8889
- 🔌 **ViewModel Integration** - Extension functions for seamless ViewModel usage
8990
- 🧩 **Compose Ready** - First-class support for Jetpack Compose with animated reactive state

gradle/libs.versions.toml

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -8,32 +8,35 @@ android-compileSdk = "36"
88
kotlinxCoroutinesCore = "1.10.2"
99
kotlinxIoCore = "0.8.2"
1010
kotlinxSerializationProtobuf = "1.9.0"
11+
androidxCollection = "1.5.0"
1112
lifecycleViewmodel = "2.9.6"
1213
vanniktechMavenPublish = "0.35.0"
13-
compose-plugin = "1.7.3"
14+
material3 = "1.9.0"
15+
compose = "1.9.3"
1416

1517
[libraries]
1618
datastore = { module = "androidx.datastore:datastore", version.ref = "datastorePreferencesCore" }
1719
datastore-preferences-core = { module = "androidx.datastore:datastore-preferences-core", version.ref = "datastorePreferencesCore" }
1820
kermit = { module = "co.touchlab:kermit", version.ref = "kermit" }
1921
kotlin-test = { module = "org.jetbrains.kotlin:kotlin-test", version.ref = "kotlin" }
20-
compose-runtime = { module = "org.jetbrains.compose.runtime:runtime", version.ref = "compose-plugin" }
21-
compose-foundation = { module = "org.jetbrains.compose.foundation:foundation", version.ref = "compose-plugin" }
22-
compose-material = { module = "org.jetbrains.compose.material:material", version.ref = "compose-plugin" }
23-
compose-material3 = { module = "org.jetbrains.compose.material3:material3", version.ref = "compose-plugin" }
24-
compose-ui = { module = "org.jetbrains.compose.ui:ui", version.ref = "compose-plugin" }
25-
compose-components-resources = { module = "org.jetbrains.compose.components:components-resources", version.ref = "compose-plugin" }
22+
compose-runtime = { module = "org.jetbrains.compose.runtime:runtime", version.ref = "compose" }
23+
compose-foundation = { module = "org.jetbrains.compose.foundation:foundation", version.ref = "compose" }
24+
compose-material = { module = "org.jetbrains.compose.material:material", version.ref = "compose" }
25+
compose-material3 = { module = "org.jetbrains.compose.material3:material3", version.ref = "material3" }
26+
compose-ui = { module = "org.jetbrains.compose.ui:ui", version.ref = "compose" }
27+
compose-components-resources = { module = "org.jetbrains.compose.components:components-resources", version.ref = "compose" }
2628
kotlinx-coroutines-test = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-test", version.ref = "kotlinxCoroutinesCore" }
2729
kotlinx-io-core = { module = "org.jetbrains.kotlinx:kotlinx-io-core", version.ref = "kotlinxIoCore" }
2830
protobuf-kotlin = { module = "org.jetbrains.kotlinx:kotlinx-serialization-protobuf", version.ref = "kotlinxSerializationProtobuf" }
2931
lifecycle-viewmodel = { module = "org.jetbrains.androidx.lifecycle:lifecycle-viewmodel", version.ref = "lifecycleViewmodel" }
3032
kotlinx-coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "kotlinxCoroutinesCore" }
3133
lifecycle-viewmodel-compose = { module = "org.jetbrains.androidx.lifecycle:lifecycle-viewmodel-compose", version.ref = "lifecycleViewmodel" }
34+
androidxCollection = { module = "androidx.collection:collection", version.ref = "androidxCollection" }
3235

3336
[plugins]
3437
android-kotlin-multiplatform-library = { id = "com.android.kotlin.multiplatform.library", version.ref = "agp" }
3538
kotlinMultiplatform = { id = "org.jetbrains.kotlin.multiplatform", version.ref = "kotlin" }
3639
vanniktech-mavenPublish = { id = "com.vanniktech.maven.publish", version.ref = "vanniktechMavenPublish" }
37-
jetbrainsCompose = { id = "org.jetbrains.compose", version.ref = "compose-plugin" }
40+
jetbrainsCompose = { id = "org.jetbrains.compose", version.ref = "compose" }
3841
compose-compiler = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" }
3942
serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" }

reflow/build.gradle.kts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ kotlin {
5454
implementation(libs.protobuf.kotlin)
5555

5656
implementation(libs.kermit)
57+
implementation(libs.androidxCollection)
5758
}
5859

5960
commonTest.dependencies {

reflow/src/commonMain/kotlin/CacheSource.kt

Lines changed: 0 additions & 68 deletions
This file was deleted.

reflow/src/commonMain/kotlin/Reflow.kt renamed to reflow/src/commonMain/kotlin/io/github/araujojordan/Reflow.kt

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@ import androidx.compose.animation.togetherWith
1010
import androidx.compose.foundation.layout.Arrangement
1111
import androidx.compose.foundation.layout.Box
1212
import androidx.compose.foundation.layout.Column
13-
import androidx.compose.foundation.layout.fillMaxSize
1413
import androidx.compose.material.Button
1514
import androidx.compose.material.CircularProgressIndicator
1615
import androidx.compose.material.Text
@@ -22,6 +21,8 @@ import androidx.compose.ui.Modifier
2221
import androidx.compose.ui.unit.dp
2322
import androidx.lifecycle.ViewModel
2423
import androidx.lifecycle.viewModelScope
24+
import io.github.araujojordan.cache.CacheSource
25+
import io.github.araujojordan.model.Resulting
2526
import io.github.araujojordan.reflow.generated.resources.Res
2627
import io.github.araujojordan.reflow.generated.resources.generic_error
2728
import io.github.araujojordan.reflow.generated.resources.retry
@@ -88,7 +89,7 @@ fun <T> ViewModel.reflow(
8889
dispatcher: CoroutineDispatcher = Dispatchers.IO,
8990
initial: Resulting<T> = Resulting.loading(),
9091
shouldLoadingOnRefresh: Boolean = true,
91-
cacheSource: CacheSource<T> = CacheSource.Memory(),
92+
cacheSource: CacheSource<T> = CacheSource.None(),
9293
maxRetries: Int = MAX_RETRIES,
9394
retryDelay: Long = RETRY_DELAY,
9495
shouldRetry: (Throwable) -> Boolean = { it is IOException },
@@ -109,7 +110,7 @@ fun <T> ViewModel.reflow(
109110
dispatcher: CoroutineDispatcher = Dispatchers.IO,
110111
initial: Resulting<T> = Resulting.loading(),
111112
shouldLoadingOnRefresh: Boolean = true,
112-
cacheSource: CacheSource<T> = CacheSource.Memory(),
113+
cacheSource: CacheSource<T> = CacheSource.None(),
113114
maxRetries: Int = MAX_RETRIES,
114115
retryDelay: Long = RETRY_DELAY,
115116
shouldRetry: (Throwable) -> Boolean = { it is IOException },
@@ -132,7 +133,7 @@ fun <T> reflowIn(
132133
dispatcher: CoroutineDispatcher = Dispatchers.IO,
133134
initial: Resulting<T> = Resulting.loading<T>(),
134135
shouldLoadingOnRefresh: Boolean = true,
135-
cacheSource: CacheSource<T> = CacheSource.Memory(),
136+
cacheSource: CacheSource<T> = CacheSource.None(),
136137
maxRetries: Int = MAX_RETRIES,
137138
retryDelay: Long = RETRY_DELAY,
138139
shouldRetry: (Throwable) -> Boolean = { it is IOException },
@@ -147,13 +148,16 @@ fun <T> reflowIn(
147148
var isFirstEmission = true
148149

149150
val cached = when (cacheSource) {
150-
is CacheSource.Disk<T> -> cacheSource.data.map { it?.let { Resulting.content<T>(it) } ?: Resulting.loading() }
151-
is CacheSource.Memory -> flowOf(Resulting.loading())
151+
is CacheSource.Store<T> -> cacheSource.data.map { it?.let { Resulting.content<T>(it) } ?: Resulting.loading() }
152+
is CacheSource.None -> flowOf(Resulting.loading())
152153
}.filter { !(it.isLoading && !isFirstEmission && !shouldLoadingOnRefresh) }.flowOn(dispatcher)
153154

154155
val fetched = fetchFlow
155156
.map {
156-
scope.launch(Dispatchers.IO) { (cacheSource as? CacheSource.Disk<T>)?.store(it) }
157+
when (cacheSource) {
158+
is CacheSource.Store<T> -> scope.launch(Dispatchers.IO) { cacheSource.store(it) }
159+
is CacheSource.None -> {}
160+
}
157161
Resulting.content(it)
158162
}.retryWhen { cause, attempt ->
159163
val canRetry = (attempt + 1) < maxRetries && shouldRetry(cause)

0 commit comments

Comments
 (0)