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
62 changes: 62 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,16 @@ The showcase iOS application targets iOS 17.2. This value is the host applicatio
| `rememberResultAsState` | Converts delivered results into read-only Compose state. |
| `rememberSaveableResultAsState` | Converts delivered results into read-only saveable Compose state with automatic or custom saving. |

Read the contracts in order: [key identity](gbeewa/src/commonMain/kotlin/com/quantipixels/gbeewa/ResultKey.kt)
→ [store lifetime](gbeewa/src/commonMain/kotlin/com/quantipixels/gbeewa/ResultStore.kt)
and [presence](gbeewa/src/commonMain/kotlin/com/quantipixels/gbeewa/PendingResult.kt)
→ [consumer](gbeewa/src/commonMain/kotlin/com/quantipixels/gbeewa/ResultConsumer.kt)
→ [effect](gbeewa/src/commonMain/kotlin/com/quantipixels/gbeewa/ResultEffect.kt)
→ [state helpers](gbeewa/src/commonMain/kotlin/com/quantipixels/gbeewa/RememberResultAsState.kt).
The focused [store tests](gbeewa/src/commonTest/kotlin/com/quantipixels/gbeewa/ResultStoreTest.kt)
and [Compose runtime tests](gbeewa/src/commonTest/kotlin/com/quantipixels/gbeewa/ResultEffectTest.kt)
show these contracts in use.

## Define a typed key

Key equality defines the result lane. An object is suitable for one shared lane. A data class is suitable when an identifier is part of the lane.
Expand All @@ -194,6 +204,20 @@ The key type controls the payload type at normal call sites.

Create one `ResultStore` at the application or navigation root. Keep it for the communication lifetime that the app needs. Gbéewá does not require the store to be exposed to screen content.

### Transient handoffs and restored navigation

`ResultStore` lives in memory and is not serialized or restored. Restoring a navigation destination
does not restore its pending result. If a destination requires a transient handoff, choose its
missing-value recovery before starting dependent work. For example, return to an earlier step or
close the destination when the handoff is missing. Check and consume the handoff in an event
handler, effect, or app-owned entry boundary, never during composition or in a `remember` initializer.
Save durable identifiers through the app's own navigation or storage contract.

`rememberSaveableResultAsState` saves a receiver's UI value, not a pending handoff. Restoring that
value does not establish that a new result was delivered.

### Store exposure

The app chooses how far each capability travels:

- Keep `ResultStore` at the root and capture it in route-owned callbacks.
Expand Down Expand Up @@ -318,6 +342,37 @@ Direct consumption removes the pending result before it returns. Do not call it
because removal changes observable Compose state. Use `ResultEffect`, `rememberResultAsState`, or
`rememberSaveableResultAsState` for composition-owned delivery.

### Cancel or abandon a request

Use the existing `ResultConsumer.consume(key)` overload to remove a retired request's pending
value without invoking a result callback. Cleanup affects only that key, so other requests keep
their pending values. Run it on the UI or main context that drives Compose consumption, such as
the Cancel event handler:

```kotlin
import com.quantipixels.gbeewa.ResultConsumer
import com.quantipixels.gbeewa.ResultKey

data class EditNoteKey(val requestId: String) : ResultKey<String?>

fun cancelNoteEdit(
consumer: ResultConsumer,
key: EditNoteKey,
dismiss: () -> Unit,
) {
consumer.consume(key)
dismiss()
}
```

The return value can be ignored for cleanup. If it matters, `PendingResult.Value(null)` means a
present nullable value was removed; `PendingResult.Missing` means no pending value existed.
Cleanup does not cancel the producer or reject later publications. The app must stop or ignore
late producer callbacks for a retired request and use distinct keys for overlapping requests.
No discard API, registry, session manager, or navigation policy is needed for this cleanup.

### Receive as state

Use `rememberResultAsState` when the result directly updates local Compose state. The `AsState` suffix distinguishes the returned `State<T>` from the root-owned `ResultStore`. Extra remember keys reset the initial value when their context changes:

```kotlin
Expand Down Expand Up @@ -428,6 +483,13 @@ the key has no pending result.
- The saveable helper can restore a receiver's delivered UI value. It does not restore pending results.
- Any app layer can own the store or call `put`, including a `ViewModel` or navigation component. The call must execute on the UI or main context that drives Compose consumption. The library does not switch contexts or synchronize concurrent access.

### Same-key publication inside a callback

Do not publish another value for the same key from inside its result callback. The replacement,
including an equal payload, can remain pending until the receiver leaves and re-enters composition.
Publishing twice retains only the latest pending value. There is no queue, automatic redelivery,
or retry guarantee.

## Contributing

Open an issue before a behavior change or new platform target. Keep navigation policy outside the library, add tests for contract changes, and update `CHANGELOG.md` for public behavior changes. Before a pull request, run:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,11 @@ public class ResultConsumer public constructor(
* Direct consumption is intended for event handlers, effects, and non-Compose code. Do not
* call it during composition because removal changes observable Compose state.
*
* To clean up an abandoned request, call this overload and ignore its return value. It removes
* only this key's pending result and invokes no callback. It does not prevent a later producer
* from publishing another result. Consume on the UI or main context that drives Compose
* consumption; the store does not switch contexts or synchronize concurrent access.
*
* @param key the typed result lane to consume.
* @return the removed value or [PendingResult.Missing].
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,8 @@ public val LocalResultConsumer: ProvidableCompositionLocal<ResultConsumer> =
*
* Do not publish another value for [key] from inside [onResult]. The current keyed effect has
* completed, so that value can remain pending until this effect leaves and re-enters composition.
* This includes a payload equal to the consumed value; callback republishing has no automatic
* redelivery or retry guarantee.
*
* @param key the typed result lane to observe.
* @param onResult the latest callback that receives a pending result.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,13 @@ import androidx.compose.runtime.mutableStateMapOf
*
* The owner publishes results through [put]. The store retains each value until a
* [ResultConsumer] consumes it, a later value replaces it, or the owner discards this store.
* The store does not address a screen or inspect navigation direction. A consumer backed by this
* store can consume an equal key on the current screen or on a screen reached after a pop, push,
* replacement, or other navigation change. Any application layer can call [put], but the call
* must execute on the UI or main context that drives Compose consumption. This store does not
* switch contexts or synchronize concurrent access.
* This store lives in memory and is not serialized or restored. Restoring navigation does not
* restore pending results. If a destination requires a transient handoff, the caller must choose
* how to recover from a missing result before starting work that depends on it.
*
* Shared store ownership, equal keys, and consumption timing select the receiver; navigation
* direction does not. Publish and consume on the UI or main context that drives Compose
* consumption. This store does not switch contexts or synchronize concurrent access.
*/
@Stable
public class ResultStore public constructor() {
Expand Down
Loading