Xamarin.Android API 设计原则

除了作为 Mono 一部分提供的核心基类库之外,Xamarin.Android 还附带了各种 Android API 的绑定,以便开发人员能够使用 Mono 创建本机 Android 应用程序。

Xamarin.Android 的核心是一个互操作引擎,它连接 C# 世界与 Java 世界,并为开发人员提供从 C# 或其他 .NET 语言访问 Java API 的功能。

设计原理

下面是有关 Xamarin.Android 绑定的一些设计原则

  • 遵守 .NET Framework 设计准则

  • 允许开发人员子类化 Java 类。

  • 子类应与 C# 标准构造一起使用。

  • 从现有类派生。

  • 调用要链接的基构造函数。

  • 替代方法应使用 C# 的替代系统完成。

  • 让常见的 Java 任务变得简单,让困难的 Java 任务成为可能。

  • 将 JavaBean 属性公开为 C# 属性。

  • 公开强类型 API:

    • 提高类型安全性。

    • 最大限度地减少运行时错误。

    • 获取有关返回类型的 IDE IntelliSense。

    • 允许用于 IDE 弹出文档。

  • 建议对 API 进行 IDE 浏览:

    • 利用框架替代方案最大程度地减少 Java 类库公开。

    • 在适当且适用的情况下,公开 C# 委托(lambda、匿名方法和 System.Delegate)而不是单一方法接口。

    • 提供一种调用任意 Java 库 (Android.Runtime.JNIEnv) 的机制。

程序集

Xamarin.Android 包含许多构成 MonoMobile 配置文件的程序集程序集页面包含更多信息。

与 Android 平台的绑定包含在 Mono.Android.dll 程序集中。 此程序集包含用于使用 Android API 以及与 Android 运行时 VM 通信的完整绑定。

绑定设计

集合

Android API 广泛利用 java.util 集合来提供列表、集合与映射。 我们在绑定中使用 System.Collections.Generic 接口公开这些元素。 基本映射包括:

我们提供了帮助器类来促进这些类型的更快无复制封送。 如果可能,我们建议使用这些提供的集合,而不是框架提供的实现,例如 List<T>Dictionary<TKey, TValue>Android.Runtime 实现在内部使用本机 Java 集合,因此在传递给 Android API 成员时不需要在本机集合之间进行复制。

可以将任何接口实现传递给接受该接口的 Android 方法,例如将 List<int> 传递给 ArrayAdapter<int>(Context, int, IList<int>) 构造函数。 但是,对于除 Android.Runtime 实现之外的所有实现,这都涉及到将列表从 Mono VM 复制到 Android 运行时 VM 中。 如果稍后在 Android 运行时内更改此列表(例如,通过调用 ArrayAdapter<T>.Add(T) 方法),这些更改将不会在托管代码中可见。 如果使用 JavaList<int>,这些更改将可见。

换言之,不属于上面列出的帮助器类之一的集合接口实现仅封送 [In]

// This fails:
var badSource  = new List<int> { 1, 2, 3 };
var badAdapter = new ArrayAdapter<int>(context, textViewResourceId, badSource);
badAdapter.Add (4);
if (badSource.Count != 4) // true
    throw new InvalidOperationException ("this is thrown");

// this works:
var goodSource  = new JavaList<int> { 1, 2, 3 };
var goodAdapter = new ArrayAdapter<int> (context, textViewResourceId, goodSource);
goodAdapter.Add (4);
if (goodSource.Count != 4) // false
    throw new InvalidOperationException ("should not be reached.");

属性

在适当的情况下,Java 方法将转换为属性:

  • Java 方法对 T getFoo()void setFoo(T) 将转换为 Foo 属性。 示例:Activity.Intent

  • Java 方法 getFoo() 将转换为只读的 Foo 属性。 示例:Context.PackageName

  • 不会生成仅限 Set 的属性。

  • 如果属性类型是数组,则不会生成属性

事件和侦听器

Android API 基于 Java,其组件遵循 Java 模式来挂接事件侦听器。 此模式往往很麻烦,因为它要求用户创建一个匿名类并声明要重写的方法,例如,这就是在 Android 中使用 Java 执行操作的方式:

final android.widget.Button button = new android.widget.Button(context);

button.setText(this.count + " clicks!");
button.setOnClickListener (new View.OnClickListener() {
    public void onClick (View v) {
        button.setText(++this.count + " clicks!");
    }
});

使用事件的 C# 中的等效代码是:

var button = new Android.Widget.Button (context) {
    Text = string.Format ("{0} clicks!", this.count),
};
button.Click += (sender, e) => {
    button.Text = string.Format ("{0} clicks!", ++this.count);
};

请注意,上述两种机制均可用于 Xamarin.Android。 可以实现侦听器接口并将其附加到 View.SetOnClickListener,也可以将通过任何常用 C# 范例创建的委托附加到 Click 事件。

当侦听器回调方法返回 void 时,我们会根据 EventHandler<TEventArgs> 委托创建 API 元素。 我们为这些侦听器类型生成一个类似于以上示例的事件。 但是,如果侦听器回调返回非 void 且非布尔的值,则不会使用事件和事件处理程序。 而是为回调的签名生成一个特定的委托,并添加属性而不是事件。 原因是要处理委托调用顺序和返回处理。 此方法反映了使用 Xamarin.iOS API 完成的操作。

仅当 Android 事件注册方法满足以下条件时,才会自动生成 C# 事件或属性:

  1. 具有 set 前缀,例如 setOnClickListener

  2. 具有 void 返回类型。

  3. 只接受一个参数,参数类型为接口,该接口只有一个方法,接口名称以 Listener 结尾,例如 View.OnClick Listener

此外,如果侦听器接口方法的返回类型为布尔而不是 void,则生成的 EventArgs 子类将包含 Handled 属性。 Handled 属性的值用作 Listener 方法的返回值,默认为 true

例如,Android View.setOnKeyListener() 方法接受 View.OnKeyListener 接口,View.OnKeyListener.onKey(View, int, KeyEvent) 方法具有布尔返回类型。 Xamarin.Android 生成相应的 View.KeyPress 事件,该事件是一个 EventHandler<View.KeyEventArgs>。 而 KeyEventArgs 类又具有 View.KeyEventArgs.Handled 属性,该属性用作 View.OnKeyListener.onKey() 方法的返回值

我们打算为其他方法和构造函数添加重载以公开基于委托的连接。 此外,具有多个回调的侦听器需要一些额外的检查来确定实现单个回调是否合理,因此我们在识别它们时对其进行转换。 如果没有相应的事件,则必须在 C# 中使用侦听器,但请将你认为可能具有委托用法的任何事件提请我们的注意。 我们还对不带“Listener”后缀的接口进行了一些转换,因为很明显它们将受益于委托替代方案。

由于绑定的实现细节,所有侦听器接口都实现 Android.Runtime.IJavaObject 接口,因此侦听器类必须实现该接口。 这可以通过在 Java.Lang.Object 的子类或任何其他包装的 Java 对象(例如 Android 活动)中实现侦听器接口来完成。

可运行对象

Java 利用 java.lang.Runnable 接口提供委托机制。 java.lang.Thread 类是该接口的重要使用者。 Android 也采用了 API 中的接口。 Activity.runOnUiThread()View.post() 就是典型的示例。

Runnable 接口包含单个 void 方法 run()。 因此,它适合在 C# 中作为 System.Action 委托进行绑定。 我们在绑定中提供了重载,这些重载为所有在本机 API 中使用 Runnable 的 API 成员接受 Action 参数,例如 Activity.RunOnUiThread()View.Post()

我们保留了 IRunnable 重载而不是替换它们,因为有几个类型实现该接口,因此它们可以直接作为可运行对象传递。

内部类

Java 有两种不同类型的嵌套类:静态嵌套类和非静态类。

Java 静态嵌套类与 C# 嵌套类型相同。

非静态嵌套类(也称为内部类)有显著不同。 它们包含对其封闭类型实例的隐式引用,并且不能包含静态成员(以及本概述文章范围以外的其他差异)。

在涉及到绑定和 C# 用法时,静态嵌套类被视为普通嵌套类型。 同时,内部类有两个显著的差别:

  1. 对包含类型的隐式引用必须显式提供为构造函数参数。

  2. 从内部类继承时,内部类必须嵌套在从基内部类的包含类型继承的类型中,并且派生类型必须提供类型与 C# 包含类型相同的构造函数

例如,请考虑 Android.Service.Wallpaper.WallpaperService.Engine 内部类。 由于它是一个内部类,WallpaperService.Engine() 构造函数采用对 WallpaperService 实例的引用(与 Java WallpaperService.Engine() 构造函数进行比较,后者不带任何参数)。

内部类的一个示例派生是 CubeWallpaper.CubeEngine:

class CubeWallpaper : WallpaperService {
    public override WallpaperService.Engine OnCreateEngine ()
    {
        return new CubeEngine (this);
    }

    class CubeEngine : WallpaperService.Engine {
        public CubeEngine (CubeWallpaper s)
                : base (s)
        {
        }
    }
}

请注意 CubeWallpaper.CubeEngine 如何嵌套在 CubeWallpaper 中,CubeWallpaperWallpaperService.Engine 的包含类继承,而 CubeWallpaper.CubeEngine 有一个构造函数,该构造函数采用声明类型(在本例中为 CubeWallpaper),上面指定了所有这些元素。

接口

Java 接口可以包含三组成员,其中两组会导致 C# 出现问题:

  1. 方法

  2. 类型

  3. 字段

Java 接口将转换为两种类型:

  1. 一个(可选)接口,其中包含方法声明。 此接口与 Java 接口同名,但它还有一个“I”前缀

  2. 一个(可选)静态类,其中包含 Java 接口中声明的任何字段。

嵌套类型被“重新定位”到封闭接口的同级,而不是嵌套类型,并以封闭接口名称作为前缀。

例如,请考虑 android.os.Parcelable 接口。 Parcelable 接口包含方法、嵌套类型和常量。 Parcelable 接口方法放置在 Android.OS.IParcelable 接口中。 Parcelable 接口常量放置在 Android.OS.ParcelableConsts 类型中。 嵌套的 android.os.Parcelable.ClassLoaderCreator<T>android.os.Parcelable.Creator<T> 类型目前未绑定,因为我们的泛型支持存在限制;如果它们受支持,它们将作为 Android.OS.IParcelableClassLoaderCreator 和 Android.OS.IParcelableCreator 接口出现。 例如,嵌套的 android.os.IBinder.DeathRecipient 接口绑定为 Android.OS.IBinderDeathRecipient 接口。

注意

从 Xamarin.Android 1.9 开始,Java 接口常量将被复制,以简化 Java 代码的移植。 这有助于改进依赖于 android 提供程序接口常量的 Java 代码的移植。

除上述类型外,还有四种变化:

  1. 生成与 Java 接口同名的类型来包含常量。

  2. 包含接口常量的类型还包含来自已实现的 Java 接口的所有常量。

  3. 实现包含常量的 Java 接口的所有类都会获得一个新的嵌套 InterfaceConsts 类型,其中包含来自所有已实现的接口的常量。

  4. Consts 类型现已过时

对于 android.os.Parcelable 接口,这意味着现在有一个 Android.OS.Parcelable 类型用于包含常量。 例如,Parcelable.CONTENTS_FILE_DESCRIPTOR 常量绑定为 Parcelable.ContentsFileDescriptor 常量,而不是绑定为 ParcelableConsts.ContentsFileDescriptor 常量

对于包含常量的接口(这些接口又实现包含更多常量的其他接口),现在会生成所有常量的并集。 例如,android.provider.MediaStore.Video.VideoColumns 接口实现 android.provider.MediaStore.MediaColumns 接口。 但是,在 1.9 之前,Android.Provider.MediaStore.Video.VideoColumnsConsts 类型无法访问 Android.Provider.MediaStore.MediaColumnsConsts 中声明的常量。 因此,Java 表达式 MediaStore.Video.VideoColumns.TITLE 需要绑定到 C# 表达式 MediaStore.Video.MediaColumnsConsts.Title,如果不阅读大量 Java 文档,就很难发现这一点。 在 1.9 中,等效的 C# 表达式为 MediaStore.Video.VideoColumns.Title

此外,请考虑实现 Java Parcelable 接口的 android.os.Bundle 类型。 由于它实现该接口,因此该接口上的所有常量都可以“通过”Bundle 类型进行访问,例如,Bundle.CONTENTS_FILE_DESCRIPTOR 是一个完全有效的 Java 表达式。 以前,若要将此表达式移植到 C#,需要查看已实现的所有接口,以了解 CONTENTS_FILE_DESCRIPTOR 来自哪个类型。 从 Xamarin.Android 1.9 开始,实现包含常量的 Java 接口的类具有嵌套的 InterfaceConsts 类型,该类型将包含所有继承的接口常量。 这样就可以将 Bundle.CONTENTS_FILE_DESCRIPTOR 转换为 Bundle.InterfaceConsts.ContentsFileDescriptor

最后,带有 Consts 后缀的类型(例如 Android.OS.ParcelableConsts)现已过时,新引入的 InterfaceConsts 嵌套类型除外。 在 Xamarin.Android 3.0 中会删除这些类型。

资源

图像、布局描述、二进制 Blob 和字符串字典可以作为资源文件包含在应用程序中。 各种 Android API 旨在对资源 ID 进行操作,而不是直接处理图像、字符串或二进制 Blob。

例如,包含用户界面布局 (main.axml)、国际化表字符串 (strings.xml) 和一些图标 (drawable-*/icon.png) 的示例 Android 应用会将其资源保留在应用程序的“Resources”目录中:

Resources/
    drawable-hdpi/
        icon.png

    drawable-ldpi/
        icon.png

    drawable-mdpi/
        icon.png

    layout/
        main.axml

    values/
        strings.xml

本机 Android API 不直接对文件名进行操作,而是对资源 ID 进行操作。 当你编译使用资源的 Android 应用程序时,生成系统将打包资源以供分发,并生成一个名为 Resource 的类,其中包含所包含的每个资源的令牌。 例如,对于上述资源布局,R 类公开的内容如下:

public class Resource {
    public class Drawable {
        public const int icon = 0x123;
    }

    public class Layout {
        public const int main = 0x456;
    }

    public class String {
        public const int first_string = 0xabc;
        public const int second_string = 0xbcd;
    }
}

然后,可以使用 Resource.Drawable.icon 来引用 drawable/icon.png 文件,或使用 Resource.Layout.main 来引用 layout/main.xml 文件,或使用 Resource.String.first_string 来引用字典文件 values/strings.xml 中的第一个字符串。

常量和枚举

本机 Android API 有许多方法可以获取或返回 int,必须将其映射到常量字段才能确定 int 的含义。 若要使用这些方法,用户需要查阅文档来了解哪些常量是合适的值,这不太理想。

例如,请考虑 Activity.requestWindowFeature(int featureID)

在这种情况下,我们需要努力将相关常量组合到 .NET 枚举中,并重新映射方法以采用枚举。 这样,我们就可以提供潜在值的 IntelliSense 选择。

上述示例变成了:Activity.RequestWindowFeature(WindowFeatures featureId)

请注意,此过程需要大量的人工操作,因为需要确定哪些常量属于同一组,以及哪些 API 使用这些常量。 请针对 API 中使用的、可以最好地表达为枚举的任何常量报告 bug。