Summary
Expose androidx.compose.runtime.rememberCoroutineScope() (or a
.NET-shaped equivalent) so callers can launch suspend work from
non-@Composable event handlers like onClick.
Motivation
An audit of 586 Kotlin files across Google's compose-samples
(Jetchat, Jetnews, Jetcaster, Owl, Crane, Reply) +
android/nowinandroid counted 30 call sites for
rememberCoroutineScope. The canonical pattern is:
val scope = rememberCoroutineScope()
Button(onClick = { scope.launch { snackbar.showSnackbar("...") } })
Without it, callers can't fire any suspend from an event
handler (snackbar show, animated scroll, drawer open, sheet
expand, network request that the ViewModel didn't wrap…). Today
the only available pattern is LaunchedEffect(key) { ... },
which can't be triggered from onClick.
Shape options
Two natural ways to surface this in C#:
Option A — pass-through CoroutineScope
public static IJavaObject RememberCoroutineScope(this IComposer composer);
public static Task LaunchAsync(this IJavaObject scope, Func<CancellationToken, Task> body);
Closer to Kotlin's shape but requires also wrapping
CoroutineScope.launch and friends.
Option B — Task-shaped façade (preferred)
public sealed class CoroutineScope : IDisposable { ... }
public static CoroutineScope RememberCoroutineScope(IComposer composer);
// usage:
new Button(onClick: () => scope.Launch(async ct => {
await snackbar.ShowAsync("Hello", cancellationToken: ct);
}))
scope.Launch(Func<CancellationToken, Task>) returns a Task
that's cancelled when the scope leaves the composition. Aligns
naturally with the existing *Async patterns in
SuspendBridge.cs and the CancellationToken cancellationToken = default convention documented in AGENTS.md →
"Suspend / async bridges".
Pick Option B unless there's a strong reason otherwise.
Scope
- Public
RememberCoroutineScope(IComposer) returning a
wrapper that holds the Kotlin CoroutineScope peer.
scope.Launch(Func<CancellationToken, Task>) that bridges
into Kotlin's CoroutineScope.launch and respects scope
cancellation when the composition leaves.
- Update at least one existing
*Async gallery demo (e.g. the
snackbar demo) to drive the suspend from an onClick via
RememberCoroutineScope instead of a LaunchedEffect-on-flag
workaround. Don't skip the demo update.
- Public API tracking + regenerated
docs/api-coverage.md.
Reference
- Kotlin source:
androidx.compose.runtime.rememberCoroutineScope in
runtime-android-1.11.2.
AGENTS.md → "Suspend / async bridges" — same JCW + GCHandle
family of plumbing.
src/Microsoft.AndroidX.Compose/SuspendBridge.cs and
SuspendContinuation — pattern for tying a JVM coroutine
identity to a Task.
- Current coverage: see
docs/api-coverage.md § runtime —
rememberCoroutineScope is listed as not covered.
Audit methodology
rg --count-matches 'rememberCoroutineScope' across
android/compose-samples (Jetchat, Jetnews, Jetcaster, Owl,
Crane, Reply) + android/nowinandroid, scanning all .kt files.
Summary
Expose
androidx.compose.runtime.rememberCoroutineScope()(or a.NET-shaped equivalent) so callers can launch suspend work from
non-
@Composableevent handlers likeonClick.Motivation
An audit of 586 Kotlin files across Google's
compose-samples(Jetchat, Jetnews, Jetcaster, Owl, Crane, Reply) +
android/nowinandroidcounted 30 call sites forrememberCoroutineScope. The canonical pattern is:Without it, callers can't fire any
suspendfrom an eventhandler (snackbar show, animated scroll, drawer open, sheet
expand, network request that the ViewModel didn't wrap…). Today
the only available pattern is
LaunchedEffect(key) { ... },which can't be triggered from
onClick.Shape options
Two natural ways to surface this in C#:
Option A — pass-through
CoroutineScopeCloser to Kotlin's shape but requires also wrapping
CoroutineScope.launchand friends.Option B — Task-shaped façade (preferred)
scope.Launch(Func<CancellationToken, Task>)returns aTaskthat's cancelled when the scope leaves the composition. Aligns
naturally with the existing
*Asyncpatterns inSuspendBridge.csand theCancellationToken cancellationToken = defaultconvention documented inAGENTS.md→"Suspend / async bridges".
Pick Option B unless there's a strong reason otherwise.
Scope
RememberCoroutineScope(IComposer)returning awrapper that holds the Kotlin
CoroutineScopepeer.scope.Launch(Func<CancellationToken, Task>)that bridgesinto Kotlin's
CoroutineScope.launchand respects scopecancellation when the composition leaves.
*Asyncgallery demo (e.g. thesnackbar demo) to drive the suspend from an
onClickviaRememberCoroutineScopeinstead of aLaunchedEffect-on-flagworkaround. Don't skip the demo update.
docs/api-coverage.md.Reference
androidx.compose.runtime.rememberCoroutineScopeinruntime-android-1.11.2.AGENTS.md→ "Suspend / async bridges" — sameJCW + GCHandlefamily of plumbing.
src/Microsoft.AndroidX.Compose/SuspendBridge.csandSuspendContinuation— pattern for tying a JVM coroutineidentity to a
Task.docs/api-coverage.md§runtime—rememberCoroutineScopeis listed as not covered.Audit methodology
rg --count-matches 'rememberCoroutineScope'acrossandroid/compose-samples(Jetchat, Jetnews, Jetcaster, Owl,Crane, Reply) +
android/nowinandroid, scanning all.ktfiles.