Follow-up to #66 (Phases 1 & 2 are landing in #75; Phase 3 = RememberSaveable follow-up issue).
Problem
With Compose.Remember<T> now slot-table-correct, the next-most-asked-for primitives are the side-effect APIs that schedule work tied to the same slot lifetime:
LaunchedEffect(key1, key2, ...) { coroutineBlock } — start a coroutine when the effect enters composition; cancel + relaunch on key change; cancel when the effect leaves composition.
DisposableEffect(key1, ...) { onDispose { ... } } — run setup on enter / re-key, run a teardown lambda on leave / re-key.
SideEffect { block } — run a synchronous block on every successful composition (no keys, no disposal).
These are needed for any non-trivial app: starting/stopping a Flow, registering a sensor listener, kicking off a snackbar coroutine, etc.
Proposal
Add three static entry points in Compose (or a new Effects.cs):
public static void LaunchedEffect(object? key1, ..., Func<Task> block, ...);
public static void DisposableEffect(object? key1, ..., Func<DisposableEffectScope, IDisposable> block, ...);
public static void SideEffect(Action block, ...);
For LaunchedEffect, route C# async Task → Kotlin suspend () -> Unit through whatever Kotlin-coroutines interop the binding exposes (likely needs an adapter — investigate first; this is the hardest of the three).
For DisposableEffect, IDisposable.Dispose() → the onDispose callback. The DisposableEffectScope parameter exists purely so the C# call shape mirrors the Kotlin DSL (onDispose { ... }); we may collapse it to a plain Func<IDisposable>.
SideEffect is the easiest — just a no-key composer call.
Acceptance
LaunchedEffect(channelId, async () => await StartListeningTo(channelId)) cancels and re-launches when channelId changes; cancels on screen exit; surfaces exceptions through the normal C# Task channel.
DisposableEffect(Unit) { register sensor; return DisposableAction(() => unregister) } correctly unregisters on screen exit.
SideEffect runs after every successful recomposition, never on a skipped one.
Out of scope
produceState, derivedStateOf, snapshotFlow — separate follow-ups, less commonly needed.
- Full Kotlin-coroutines interop ergonomics (e.g. surfacing
CoroutineScope cancellation tokens). Start with Task + cancellation on dispose; expand later.
Follow-up to #66 (Phases 1 & 2 are landing in #75; Phase 3 =
RememberSaveablefollow-up issue).Problem
With
Compose.Remember<T>now slot-table-correct, the next-most-asked-for primitives are the side-effect APIs that schedule work tied to the same slot lifetime:LaunchedEffect(key1, key2, ...) { coroutineBlock }— start a coroutine when the effect enters composition; cancel + relaunch on key change; cancel when the effect leaves composition.DisposableEffect(key1, ...) { onDispose { ... } }— run setup on enter / re-key, run a teardown lambda on leave / re-key.SideEffect { block }— run a synchronous block on every successful composition (no keys, no disposal).These are needed for any non-trivial app: starting/stopping a
Flow, registering a sensor listener, kicking off a snackbar coroutine, etc.Proposal
Add three static entry points in
Compose(or a newEffects.cs):For
LaunchedEffect, route C#async Task→ Kotlinsuspend () -> Unitthrough whatever Kotlin-coroutines interop the binding exposes (likely needs an adapter — investigate first; this is the hardest of the three).For
DisposableEffect,IDisposable.Dispose()→ theonDisposecallback. TheDisposableEffectScopeparameter exists purely so the C# call shape mirrors the Kotlin DSL (onDispose { ... }); we may collapse it to a plainFunc<IDisposable>.SideEffectis the easiest — just a no-key composer call.Acceptance
LaunchedEffect(channelId, async () => await StartListeningTo(channelId))cancels and re-launches whenchannelIdchanges; cancels on screen exit; surfaces exceptions through the normal C#Taskchannel.DisposableEffect(Unit){ register sensor; return DisposableAction(() => unregister) } correctly unregisters on screen exit.SideEffectruns after every successful recomposition, never on a skipped one.Out of scope
produceState,derivedStateOf,snapshotFlow— separate follow-ups, less commonly needed.CoroutineScopecancellation tokens). Start withTask+ cancellation on dispose; expand later.