Skip to content

Add RememberCoroutineScope for launching suspends from event handlers #231

@jonathanpeppers

Description

@jonathanpeppers

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

  1. Public RememberCoroutineScope(IComposer) returning a
    wrapper that holds the Kotlin CoroutineScope peer.
  2. scope.Launch(Func<CancellationToken, Task>) that bridges
    into Kotlin's CoroutineScope.launch and respects scope
    cancellation when the composition leaves.
  3. 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.
  4. 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions