dotnet / dotnet/android-libraries

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

Open
#1,456 2 comments 1 reaction 0 assignees View on GitHub
Dominant language
C#
Stars
317
Forks
73
Avg merge
1d 8h
Merged PRs (30d)
17

Description

### 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:

```csharp
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:

```csharp
static Task Await(Func action, CancellationToken cancelToken)
{
var continuation = new Continuation(cancelToken);
var result = action(continuation);
if (result != IntrinsicsKt.COROUTINE_SUSPENDED) { continuation.SetResult(result); }
return continuation.Task;
}

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

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

public Task 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:

```csharp
Task> GetGrantedPermissionsAsync(CancellationToken cancellationToken = default);
Task 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

```shell

```

Contributor guide

Open the contributing guide

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.