Hacktoberfest 2026: những issue maintainer đã đánh dấu cho tháng Mười, đang mở và phù hợp người mới. Xem issue Hacktoberfest

Kotlin `object` companion field not surfaced on outer class binding (KeyboardOptions.Companion, TextStyle.Companion, ...)

Đang mở
#1,467 0 bình luận 1 reaction 2 người được giao Xem trên GitHub

@jonathanpeppers đang làm issue này rồi.

Từ ngày 10/6/2026.

  • #1468 của @copilot-swe-agent — đang mở

Đánh giá

Issue này chưa được đánh giá.

Mô tả

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:

public class KeyboardOptions /* ... */ {
    public companion object {
        public val Default: KeyboardOptions = KeyboardOptions(/* ... */)
    }
}

Kotlin lowers that to bytecode:

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:

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:

<!-- androidx.compose.foundation/Transforms/Metadata.xml -->
<attr path="/api/package[@name='androidx.compose.foundation.text']/class[@name='KeyboardOptions']/field[@name='Companion']"
      name="visible">true</attr>

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:

  • <attr ... name="managedName">CompanionInstance</attr> to rename the property, or
  • <remove-node> + <add-node> 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 — 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.
Ngôn ngữ chính
C#
Star
318
Fork
73
Merge trung bình
1 ngày 22 giờ
Pull request đã merge (30 ngày)
8

Chuẩn bị môi trường

Bắt đầu từ đâu

  1. Đọc hết issue, rồi đọc hướng dẫn đóng góp của dự án.
  2. Bình luận trên issue rằng bạn sẽ nhận — tránh hai người làm cùng một việc.
  3. Fork repository và làm thay đổi trên một nhánh.
  4. Mở pull request có tham chiếu số hiệu của issue.

Issue khác của dotnet/android-libraries

Tất cả issue của dotnet/android-libraries

Issue tương tự

Thêm issue về C#

Nhận issue mới trong hộp thư của bạn

Bản tóm tắt ngắn những issue GitHub phù hợp với người mới.