JindongJindong

The Reactive Contract

How Jindong decides when to play and which values it reads

The Reactive Contract

Jindong behaves like LaunchedEffect: it plays the pattern in its content block, and a change to any key restarts it. Three rules describe exactly which values it reads and when it fires. Everything about reactive Jindong usage follows from them.

Rule 1: values in content are frozen at compile time

When a key changes, Jindong compiles the content block into a pattern once and plays it. The compilation runs in a composition that produces the pattern in a single pass and is then torn down, so there is no recomposition. A state value the block reads is read once, at that compile, and does not update on its own afterward.

var level by remember { mutableStateOf(1) }
 
// `level` is read once, when the current key last changed.
Jindong(trigger) {
    RepeatWithIndex(level) { index ->
        Haptic(50.ms)
    }
}

If level changes but trigger does not, the played pattern keeps the level from the last time trigger changed.

Rule 2: keys are the playback trigger

A key change is what fires playback. Put a value in the keys exactly when its change should play. Values that only shape the pattern belong inside the block as parameters.

The distinction matters most with continuous input. A slider value passed as a key fires a vibration on every drag step:

// Fires on every value change while dragging.
Jindong(sliderValue) {
    Haptic((sliderValue * 100).toInt().ms)
}

The same value as a parameter shapes the next playback without firing on each step:

// Reads sliderValue when `commit` changes; does not fire mid-drag.
Jindong(commit) {
    Haptic((sliderValue * 100).toInt().ms)
}

To make a value both shape the pattern and re-fire when it changes, pass it as a key and read it inside the block:

Jindong(level) {
    RepeatWithIndex(level) { index ->
        Haptic(50.ms, HapticIntensity.Custom(1f - index * 0.1f))
    }
}

Prebuilt patterns follow the same rule. Clip captures its pattern at compile time, so thread the pattern through the keys to re-fire on change: Jindong(pattern) { Clip(pattern) }. See Clip.

Rule 3: cancel-and-restart is best effort

A key change cancels the in-flight playback before starting the new one. The manager serializes this with a state lock, so a fast sequence of key changes cancels in order and the last one wins.

The cancellation itself is best effort. Neither Vibrator.cancel() on Android nor CHHapticPatternPlayer.stop on iOS reports when the hardware has actually stopped, so a few milliseconds of physical overlap between the old and new playback are possible. Patterns that change keys rapidly should not assume a clean cut between them.

Choosing keys

ValueWhere it goesEffect
Should fire playback when it changesKeyEach change plays
Shapes the pattern, read at the next fireParameter in the blockFrozen until a key changes
Should both shape and fireKey and read in the blockEach change plays with the new value

See also

On this page