Skip to content

Project Kotlin suspend APIs as Task<T> instead of raw IContinuation #1456

@tipa

Description

@tipa

Android framework version

net10.0-android

Affected platform version

.NET 10 / Xamarin.AndroidX.Health.Connect.ConnectClient 1.1.0.2

Description

The AndroidX Health Connect binding exposes Kotlin suspend APIs as methods that require passing a raw Kotlin.Coroutines.IContinuation.

Example from the binding:

client.PermissionController.GetGrantedPermissions(IContinuation continuation)
client.ReadRecords(ReadRecordsRequest request, IContinuation continuation)

This is technically callable, but difficult to use correctly from .NET. A .NET consumer has to hand-write a coroutine bridge using TaskCompletionSource, detect IntrinsicsKt.COROUTINE_SUSPENDED, implement IContinuation, unwrap Kotlin Result.Failure, map Java exceptions into CLR exceptions, and optionally bridge cancellation.

For .NET consumers, these APIs should ideally be projected as Task / Task methods.

Current required workaround:

static Task<T> Await<T>(Func<IContinuation, object> action, CancellationToken cancelToken)
{
    var continuation = new Continuation<T>(cancelToken);
    var result = action(continuation);
    if (result != IntrinsicsKt.COROUTINE_SUSPENDED) { continuation.SetResult(result); }
    return continuation.Task;
}

sealed class Continuation<T> : Java.Lang.Object, IContinuation
{
    readonly TaskCompletionSource<T> tcs = new(TaskCreationOptions.RunContinuationsAsynchronously);
    readonly CancellationTokenRegistration ctr;

    public Continuation(CancellationToken cancelToken) =>
        ctr = cancelToken.Register(() => tcs.TrySetCanceled(cancelToken));

    public Task<T> Task => tcs.Task;
    public ICoroutineContext Context => EmptyCoroutineContext.Instance;

    public void ResumeWith(Java.Lang.Object result) => SetResult(result);

    public void SetResult(object result)
    {
        if (result is Java.Lang.Object { Class.Name: "kotlin.Result$Failure" } failure)
        {
            var field = failure.Class.GetDeclaredField("exception");
            field.Accessible = true;
            tcs.TrySetException(new Exception(field.Get(failure).ToString()));
        }
        else { tcs.TrySetResult((T)result); }

        ctr.Dispose();
    }
}

Expected behavior:
Kotlin suspend APIs should have generated .NET-friendly overloads such as:

Task<ICollection<string>> GetGrantedPermissionsAsync(CancellationToken cancellationToken = default);
Task<ReadRecordsResponse> ReadRecordsAsync(ReadRecordsRequest request, CancellationToken cancellationToken = default);

Actual behavior:
Only the raw continuation-based API is exposed.

Relevant official documentation:
Google’s Health Connect APIs are documented as Kotlin suspend functions. For example:
https://developer.android.com/reference/kotlin/androidx/health/connect/client/HealthConnectClient#readRecords(androidx.health.connect.client.request.ReadRecordsRequest)

Steps to Reproduce:

  1. Create a .NET Android project targeting net10.0-android36.0.
  2. Add Xamarin.AndroidX.Health.Connect.ConnectClient version 1.1.0.2.
  3. Try to call Health Connect suspend APIs such as GetGrantedPermissions or ReadRecords.
  4. Observe that the API requires IContinuation instead of returning Task.

Workaround:
Manually implement an IContinuation bridge with TaskCompletionSource, detect COROUTINE_SUSPENDED, unwrap kotlin.Result$Failure, and map the Java throwable to a CLR Exception.

Request:
Please consider projecting Kotlin suspend APIs in bound AndroidX packages as Task / Task methods, or provide an official helper/interop pattern for consuming suspend APIs from .NET.

Relevant log output

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions