dotnet / dotnet/android-libraries
Project Kotlin suspend APIs as Task<T> instead of raw IContinuation
- 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
Assessment
This issue has not been assessed yet.