dotnet / dotnet/android-libraries
Kotlin `object` companion field not surfaced on outer class binding (KeyboardOptions.Companion, TextStyle.Companion, ...)
- Dominant language
- C#
- Stars
- 317
- Forks
- 73
- Avg merge
- 1d 8h
- Merged PRs (30d)
- 17
Description
### Summary
Kotlin `object` companion fields aren't surfaced as public properties on the outer class binding, so consumers can't reach the singleton through the binding alone. They have to fall back to raw JNI (`FindClass` + `GetStaticFieldID("Companion")` + `GetStaticObjectField`) to bootstrap the companion peer.
### Repro
Pick any AndroidX type with a Kotlin `object` companion. `androidx.compose.foundation.text.KeyboardOptions` is the case I just hit:
```kotlin
public class KeyboardOptions /* ... */ {
public companion object {
public val Default: KeyboardOptions = KeyboardOptions(/* ... */)
}
}
```
Kotlin lowers that to bytecode:
```text
public final class androidx.compose.foundation.text.KeyboardOptions {
public static final androidx.compose.foundation.text.KeyboardOptions$Companion Companion;
public static final androidx.compose.foundation.text.KeyboardOptions$Companion access$getCompanion$cp();
...
}
```
After binding through `Xamarin.AndroidX.Compose.Foundation 1.11.2.2`:
* `AndroidX.Compose.Foundation.Text.KeyboardOptions` — bound (sealed class)
* `KeyboardOptions.Companion` nested type — bound with `[Register("androidx/compose/foundation/text/KeyboardOptions$Companion", DoNotGenerateAcw = true)]`
* `Companion.Default` — bound (instance getter, returns `KeyboardOptions`)
* `KeyboardOptions.Copy(int, Boolean?, int, int, ...)` — bound
What's missing:
* **No `KeyboardOptions.Companion` static property/field accessor on the outer class.** Decompiled the binding's outer `KeyboardOptions` and the `Companion` field is dropped — there's no `public static KeyboardOptions.Companion Companion { get; }` member to call.
* No public ctor (stripped — `@JvmInline value class` params hit the `-d9O1mEE` mangling).
* No `KeyboardOptionsKt` static helper.
So even though the entire downstream API (`Companion.Default`, `Copy(...)`) is bound, there's no binding-only way to *get* the `Companion` instance to start the call chain.
### Current workaround
Hand-roll the singleton bootstrap once per affected type. Verbatim from [`Microsoft.AndroidX.Compose/src/Microsoft.AndroidX.Compose/KeyboardOptionsCompanion.cs`](https://github.com/jonathanpeppers/Microsoft.AndroidX.Compose/blob/main/src/Microsoft.AndroidX.Compose/KeyboardOptionsCompanion.cs):
```csharp
IntPtr cls = JNIEnv.FindClass("androidx/compose/foundation/text/KeyboardOptions");
IntPtr field = JNIEnv.GetStaticFieldID(cls, "Companion",
"Landroidx/compose/foundation/text/KeyboardOptions$Companion;");
IntPtr local = JNIEnv.GetStaticObjectField(cls, field);
s_companion_ref = JNIEnv.NewGlobalRef(local);
JNIEnv.DeleteLocalRef(local);
return new Companion(s_companion_ref, JniHandleOwnership.DoNotTransfer);
```
Everything downstream (`Default`, `Copy(...)`) is the binding — only the static-field read needs JNI. We already have the same workaround for `androidx.compose.ui.text.TextStyle.Companion` (`TextStyleCompanion.cs`) and need it for several more (`ContentScale.Companion`, `Alignment.Companion`, `FontWeight.Companion`, etc.).
### Proposed fix
Single Metadata.xml fix-up per `androidx.compose.*` transform that has a Kotlin `object` companion. For `KeyboardOptions`:
```xml
true
```
After which the binder should emit `public static KeyboardOptions.Companion Companion { get; }` on the outer class, and consumers can write `KeyboardOptions.Companion.Default` directly. We'd then delete the hand-rolled JNI bootstrap files.
### Possible complication
The binder sometimes hides Companion fields because the property name collides with the nested type name (you end up with two `Companion` members at the same scope — one type, one property). C# handles that fine, but the generator may have proactively dropped the field to avoid the conflict. If that's the cause, the fix becomes either:
* `CompanionInstance` to rename the property, or
* `` + `` injecting an explicit property body.
Both still live in Metadata.xml — no generator code change needed.
### Affected packages
Every Compose package that exposes a Kotlin `object` companion. Most-impactful ones I've already had to bootstrap by hand or know I'll need to:
* `Xamarin.AndroidX.Compose.Foundation` — `KeyboardOptions.Companion`
* `Xamarin.AndroidX.Compose.UI.Text` — `TextStyle.Companion`, `TextDecoration.Companion`, `FontWeight.Companion`, `FontFamily.Companion`, `FontStyle.Companion`
* `Xamarin.AndroidX.Compose.UI` — `Alignment.Companion`, `Modifier.Companion`, `ContentScale.Companion`
* `Xamarin.AndroidX.Compose.UI.Graphics` — `Color.Companion`, `Shape.Companion` family
* `Xamarin.AndroidX.Compose.Material3` — `MaterialTheme.Companion`, many of the `*Defaults` types
Not Compose-specific — the same gap should exist on any Kotlin-authored AndroidX type with an `object` companion.
### Related
* [`jonathanpeppers/Microsoft.AndroidX.Compose#246`](https://github.com/jonathanpeppers/Microsoft.AndroidX.Compose/pull/246) — most recent occurrence (`KeyboardOptionsCompanion.cs`).
* `TextStyleCompanion.cs` in the same repo — first occurrence; we hit the identical pattern in Phase 1 and shipped the same JNI workaround.
Contributor guide
Assessment
This issue has not been assessed yet.