diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 96515e7..b993502 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -14,16 +14,16 @@ jobs: runs-on: macos-15 steps: - name: Check out source - uses: actions/checkout@v4 + uses: actions/checkout@v6 - name: Set up JDK 17 - uses: actions/setup-java@v4 + uses: actions/setup-java@v5 with: distribution: temurin java-version: 17 - name: Set up Gradle - uses: gradle/actions/setup-gradle@v4 + uses: gradle/actions/setup-gradle@v6 - name: Run Android and Apple Silicon checks run: ./gradlew clean check apiCheck koverVerify --stacktrace @@ -53,16 +53,16 @@ jobs: runs-on: macos-15-intel steps: - name: Check out source - uses: actions/checkout@v4 + uses: actions/checkout@v6 - name: Set up JDK 17 - uses: actions/setup-java@v4 + uses: actions/setup-java@v5 with: distribution: temurin java-version: 17 - name: Set up Gradle - uses: gradle/actions/setup-gradle@v4 + uses: gradle/actions/setup-gradle@v6 - name: Run Intel simulator checks run: | diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 819f14e..ae320b1 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -13,16 +13,16 @@ jobs: runs-on: macos-15-intel steps: - name: Check out source - uses: actions/checkout@v4 + uses: actions/checkout@v6 - name: Set up JDK 17 - uses: actions/setup-java@v4 + uses: actions/setup-java@v5 with: distribution: temurin java-version: 17 - name: Set up Gradle - uses: gradle/actions/setup-gradle@v4 + uses: gradle/actions/setup-gradle@v6 - name: Run Intel simulator checks run: | @@ -37,7 +37,7 @@ jobs: runs-on: macos-15 steps: - name: Check out source - uses: actions/checkout@v4 + uses: actions/checkout@v6 with: fetch-depth: 0 @@ -47,13 +47,13 @@ jobs: git merge-base --is-ancestor "$GITHUB_SHA" origin/main - name: Set up JDK 17 - uses: actions/setup-java@v4 + uses: actions/setup-java@v5 with: distribution: temurin java-version: 17 - name: Set up Gradle - uses: gradle/actions/setup-gradle@v4 + uses: gradle/actions/setup-gradle@v6 - name: Extract version id: version @@ -98,4 +98,4 @@ jobs: ORG_GRADLE_PROJECT_signingInMemoryKey: ${{ secrets.SIGNING_KEY }} ORG_GRADLE_PROJECT_signingInMemoryKeyId: ${{ secrets.SIGNING_KEY_ID }} ORG_GRADLE_PROJECT_signingInMemoryKeyPassword: ${{ secrets.SIGNING_KEY_PASSWORD }} - run: ./gradlew publishAllPublicationsToMavenCentralRepository -PVERSION_NAME=${{ steps.version.outputs.value }} --stacktrace + run: ./gradlew publishAndReleaseToMavenCentral -PVERSION_NAME=${{ steps.version.outputs.value }} --stacktrace diff --git a/README.md b/README.md index 0110363..10b5ff8 100644 --- a/README.md +++ b/README.md @@ -10,10 +10,23 @@ Gbéewá is the library name. Its repository and artifact use `gbeewa`, and its Gbéewá delivers typed, transient results to app-wired Compose Multiplatform consumers. It does not own navigation, identify a previous destination, or select a destination. A mounted consumer can receive a result on the same screen or after any navigation change while the shared result store remains alive. +The library uses a common runtime API. It does not require KSP, code generation, or a navigation-framework compiler plugin. + Quanti Pixels authors, maintains, and publishes this library. The library originated from the result mechanism in Yétúndé The Alárìná. Version `0.1.0` supports Android and iOS. Future platform targets are added by request. +## Why Gbéewá + +- **Omni-directional delivery:** Deliver current-to-previous, current-to-next, or current-to-current. Shared store ownership, key equality, and consumption timing select the receiver. +- **Typed result lanes:** Use `ResultKey` instead of shared string keys and manual payload casts. +- **Parameterized keys:** Use data-class keys to isolate concurrent requests or screen instances. +- **Pure runtime API:** Add no KSP processor or generated navigation wrapper. +- **Framework agnostic:** Use the same contract with Navigation 2, Navigation 3, Decompose, Voyager, or app-owned navigation. +- **Compose Multiplatform:** Use the same common API on Android and iOS. + +Gbéewá provides transient, one-consumer result delivery. It is not a broadcast channel or a replacement for durable application state. + ## Navigation direction `Result` describes the outcome of an action. It does not define a navigation direction. @@ -21,11 +34,22 @@ Version `0.1.0` supports Android and iOS. Future platform targets are added by r `ResultStore.put` stores one pending value under a key. It does not address a screen. The first eligible consumer that uses a `ResultConsumer` backed by the same store and an equal key consumes that value. Therefore, a publication can deliver in these directions: - current to previous: publish, then pop; -- current to current: publish while the consumer remains mounted; +- current to current: publish while the consumer remains mounted, including between sibling composables that share the store; - current to next: publish, then push a screen that consumes the key. Other route relationships use the same rules. Navigation direction does not select the receiver. Shared `ResultStore` ownership, key equality, and consumption timing select it. Delivery is not a broadcast. If more than one consumer can observe an equal key, only the first consumer receives the pending value. A mounted consumer can consume it before a later screen enters composition. Use request-specific or parameterized keys when the receiver must be unambiguous. +```text +Producer A ── put(ResultKey, value) ──► ResultStore + │ + equal key + first eligible consumer + │ + ▼ + Consumer B or a later destination +``` + +The producer and consumer can be on the same screen, on different mounted screens, or on screens separated by a navigation change. The diagram shows one-result consumption. It does not show a broadcast channel. + ## Samples The runnable Android and iOS showcase uses two flows derived from Yétúndé: @@ -221,6 +245,30 @@ class AppResultViewModel : ViewModel() { Provide `resultConsumer` through `LocalResultConsumer`. Capture `resultStore` in route callbacks, or pass it to producers when the application chooses direct exposure. A `ViewModel` preserves this in-memory owner across Android configuration changes. It does not restore pending results after process death. +Apps that use Compose Runtime 1.10.0 or later can instead keep a composition-owned store with [`retain`](https://developer.android.com/reference/kotlin/androidx/compose/runtime/retain/retain.composable). Add the `androidx.compose.runtime:runtime-retain` artifact to the app, then retain the store at the application or navigation root: + +```kotlin +import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.runtime.remember +import androidx.compose.runtime.retain.retain +import com.quantipixels.gbeewa.LocalResultConsumer +import com.quantipixels.gbeewa.ResultConsumer +import com.quantipixels.gbeewa.ResultStore + +@Composable +fun App() { + val resultStore = retain { ResultStore() } + val resultConsumer = remember(resultStore) { ResultConsumer(resultStore) } + + CompositionLocalProvider(LocalResultConsumer provides resultConsumer) { + AppNavigation(resultStore) + } +} +``` + +On Android, Compose installs a lifecycle-aware retained-values store at the composition root. It keeps `resultStore` in memory when an activity is recreated for a configuration change. Use a `ViewModel` when the store belongs to an app-owned lifecycle outside composition. Neither option restores pending results after process death. + ## Receive a result The application also chooses where the receive API appears. Install `ResultEffect` in a route when screen content must remain unaware of Gbéewá. Install it directly in a screen when that coupling is useful: diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index af7ddbd..243b4f7 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -12,7 +12,7 @@ kover = "0.9.8" navigation2 = "2.9.0-beta03" voyager = "1.1.0-beta03" activity-compose = "1.10.1" -vanniktech-maven-publish = "0.34.0" +vanniktech-maven-publish = "0.35.0" [libraries] decompose = { module = "com.arkivanov.decompose:decompose", version.ref = "decompose" } diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index 7f93135..9bbc975 100644 Binary files a/gradle/wrapper/gradle-wrapper.jar and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index bc6b57a..37f853b 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,7 +1,6 @@ -#Mon Apr 07 21:32:33 IST 2025 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.11.1-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.13-bin.zip networkTimeout=10000 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME diff --git a/gradlew b/gradlew index 1aa94a4..faf9300 100755 --- a/gradlew +++ b/gradlew @@ -15,6 +15,8 @@ # See the License for the specific language governing permissions and # limitations under the License. # +# SPDX-License-Identifier: Apache-2.0 +# ############################################################################## # @@ -55,7 +57,7 @@ # Darwin, MinGW, and NonStop. # # (3) This script is generated from the Groovy template -# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt # within the Gradle project. # # You can find Gradle at https://github.com/gradle/gradle/. @@ -84,7 +86,7 @@ done # shellcheck disable=SC2034 APP_BASE_NAME=${0##*/} # Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) -APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit +APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit # Use the maximum available, or set MAX_FD != -1 to use that value. MAX_FD=maximum @@ -203,7 +205,7 @@ fi DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' # Collect all arguments for the java command: -# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, # and any embedded shellness will be escaped. # * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be # treated as '${Hostname}' itself on the command line. diff --git a/gradlew.bat b/gradlew.bat index 93e3f59..9d21a21 100644 --- a/gradlew.bat +++ b/gradlew.bat @@ -13,6 +13,8 @@ @rem See the License for the specific language governing permissions and @rem limitations under the License. @rem +@rem SPDX-License-Identifier: Apache-2.0 +@rem @if "%DEBUG%"=="" @echo off @rem ########################################################################## @@ -43,11 +45,11 @@ set JAVA_EXE=java.exe %JAVA_EXE% -version >NUL 2>&1 if %ERRORLEVEL% equ 0 goto execute -echo. -echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. +echo. 1>&2 +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 goto fail @@ -57,11 +59,11 @@ set JAVA_EXE=%JAVA_HOME%/bin/java.exe if exist "%JAVA_EXE%" goto execute -echo. -echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. +echo. 1>&2 +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 goto fail