Skip to content
Merged
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
40 changes: 40 additions & 0 deletions .github/workflows/test.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
name: Test the library

on:
pull_request:
branches:
- main

jobs:
test:
name: Run tests
runs-on: macos-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4

- name: Read cache
uses: actions/cache@v4
with:
path: |
~/.gradle/caches
~/.gradle/wrapper
key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }}
restore-keys: |
${{ runner.os }}-gradle-

- name: Setup Java
uses: actions/setup-java@v4
with:
distribution: 'corretto'
java-version: '21'

- name: Run tests
run: |
./gradlew stately:check

- name: Archive test report
uses: actions/upload-artifact@v4
with:
name: Test report
path: build/reports/tests/test
21 changes: 21 additions & 0 deletions LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
MIT License

Copyright (c) 2025 VOIR

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
185 changes: 185 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,185 @@
# Stately – Lightweight State Management for Async Operations in Kotlin Multiplatform

`Stately` is a **lightweight Kotlin Multiplatform library** that simplifies handling async data
flows like fetching and actions across platforms (Android, JVM, iOS).

Inspired by React hooks like `useSWR`, `useMutation`, `useQuery`, `Stately` aims to provide:

- **Consistent state management** for async flows (loading, error, data)
- Built-in revalidation (polling) for fetch
- Action management with loading/error callbacks
- Clean UI integration in Jetpack Compose

---

## Not Yet Published

This library is **not currently published** to any public Maven repository.

To use it in your own project:

---

#### Step 1: Publish to Maven Local

Run the following command from the root of the project:

```bash
./gradlew publishToMavenLocal
```

This will install the library to your local Maven cache (`~/.m2`).

---

#### Step 2: Add `mavenLocal()` to your build

In your `build.gradle.kts` or `build.gradle`:

```kotlin
repositories {
mavenLocal()
// other repositories like mavenCentral(), google(), etc.
}
```

---

#### Step 3: Declare the dependency

In your module's dependencies block:

```kotlin
dependencies {
implementation("dev.voir:stately:<version>")
}
```

> Replace `<version>` with the version from your `build.gradle.kts`.

---

#### Note

Make sure to rerun `publishToMavenLocal` every time you change the library.

## Modules

### `stately`

Includes:

- `StatelyFetch<Data, Payload>`: declarative data fetching with polling and payload support
- `StatelyAction<Payload, Response>`: single-shot mutation-like execution
- `StatelyFetchResult` and `StatelyActionResult`: unified state containers

### `sample`

A sample Jetpack Compose app with:

- Navigation
- Examples for `StatelyFetch`, `StatelyAction`
- Demo with `StatelyFetchContent`, `StatelyFetchBoundary`
- Dynamic config panel to toggle revalidation, lazy load, errors

---

## Core Concepts

### StatelyFetch

```kotlin
val fetch = StatelyFetch(
fetcher = { payload -> api.loadData(payload) },
revalidateInterval = 5000L,
lazy = false,
initialData = null
)
```

- `.state`: exposes loading, error, and data
- `.revalidate(payload?)`: triggers a new fetch
- Revalidates periodically if `revalidateInterval` is set

### StatelyAction

```kotlin
val action = StatelyAction(
action = { payload -> api.sendSomething(payload) },
onSuccess = { result -> println("✅ Success") },
onError = { error -> println("❌ Failed") }
)

action.execute(payload)
```

---

## Testing

Tests are written using **pure `kotlin.test`**, `kotlinx.coroutines.test`

### Run all tests

```bash
./gradlew stately:check
```

---

## Helper UI Components

### `StatelyFetchContent`

Composable for rendering based on `StatelyFetchResult`:

```kotlin
StatelyFetchContent(
state = state,
loading = { Text("Loading") },
error = { e -> Text("Error: ${e.message}") },
content = { data -> Text("Data: $data") }
)
```

### `StatelyFetchBoundary`

A wrapper that handles everything:

```kotlin
StatelyFetchBoundary(
fetcher = { api.getSomething() },
content = { data -> Text(data) }
)
```

---

## Kotlin Multiplatform Targets

- ✅ Android
- ✅ iOS
- ✅ JVM

---

## Roadmap

- [ ] Auto cancellation
- [ ] Debouncing

---

## Contributing

This library is still under active development. PRs and feedback are welcome.

- File issues
- Suggest features
- Write sample apps for other targets

---

## License

MIT License – see `LICENSE` for full details.
9 changes: 9 additions & 0 deletions build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
plugins {
// this is necessary to avoid the plugins to be loaded multiple times
// in each subproject's classloader
alias(libs.plugins.androidApplication) apply false
alias(libs.plugins.androidLibrary) apply false
alias(libs.plugins.jetbrainsCompose) apply false
alias(libs.plugins.compose.compiler) apply false
alias(libs.plugins.kotlinMultiplatform) apply false
}
8 changes: 8 additions & 0 deletions gradle.properties
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
kotlin.code.style=official
#Gradle
org.gradle.jvmargs=-Xmx2048M -Dfile.encoding=UTF-8 -Dkotlin.daemon.jvm.options\="-Xmx2048M"
#Android
android.nonTransitiveRClass=true
android.useAndroidX=true
#Kotlin Multiplatform
kotlin.mpp.enableCInteropCommonization=true
25 changes: 25 additions & 0 deletions gradle/libs.versions.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
[versions]
agp = "8.11.0"
android-compileSdk = "36"
android-minSdk = "24"
android-targetSdk = "36"
compose-plugin = "1.8.2" # https://github.com/JetBrains/compose-multiplatform
kotlin = "2.2.0" # https://kotlinlang.org/docs/releases.html
kotlin-coroutines = "1.10.2" # https://github.com/Kotlin/kotlinx.coroutines
androidx-activityCompose = "1.10.1" # https://mvnrepository.com/artifact/androidx.activity/activity-compose
androidx-navigationCompose = "2.9.0-beta03" # https://mvnrepository.com/artifact/org.jetbrains.androidx.navigation/navigation-compose
appcash-turbine = "1.2.1" # https://github.com/cashapp/turbine

[libraries]
androidx-activity-compose = { module = "androidx.activity:activity-compose", version.ref = "androidx-activityCompose" }
androidx-navigation-compose = { module = "org.jetbrains.androidx.navigation:navigation-compose", version.ref = "androidx-navigationCompose" }
kotlin-coroutines = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "kotlin-coroutines" }
kotlinx-coroutines-test = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-test", version.ref = "kotlin-coroutines" }
appcash-turbine = { module = "app.cash.turbine:turbine", version.ref = "appcash-turbine" }

[plugins]
androidApplication = { id = "com.android.application", version.ref = "agp" }
androidLibrary = { id = "com.android.library", version.ref = "agp" }
jetbrainsCompose = { id = "org.jetbrains.compose", version.ref = "compose-plugin" }
compose-compiler = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" }
kotlinMultiplatform = { id = "org.jetbrains.kotlin.multiplatform", version.ref = "kotlin" }
Binary file added gradle/wrapper/gradle-wrapper.jar
Binary file not shown.
7 changes: 7 additions & 0 deletions gradle/wrapper/gradle-wrapper.properties
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.13-bin.zip
networkTimeout=10000
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
Loading
Loading