Xamarin.Android API の設計原則

Xamarin.Android には、Mono に含まれる中心的基底クラス ライブラリに加え、開発者が Mono でネイティブ Android アプリケーションを作成できるようにする各種 Android API 向けのバインドが付属しています。

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 Classlib の公開を最小限に抑える。

    • 必要に応じて、単一メソッド インターフェイスの代わりに C# デリゲート (ラムダ、匿名メソッド、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

  • セットのみのプロパティは生成されません。

  • プロパティ型が配列の場合、プロパティは生成 "されません"。

イベントとリスナー

Android API は Java 上に構築され、そのコンポーネントは、イベント リスナーをフックするための Java パターンに従っています。 このパターンは、ユーザーが匿名クラスを作成し、オーバーライドするメソッドを宣言する必要があるため、煩雑になる傾向があります。たとえば、Java を使用して Android で行われる処理は以下のようになります。

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 でも ブール値 でもない値を返す場合は、イベントと EventHandler は使用されません。 代わりに、コールバックのシグネチャに対して特定のデリゲートを生成し、イベントの代わりにプロパティを追加します。 その理由は、デリゲートの呼び出し順序と戻り値の処理に対処するためです。 このアプローチは、Xamarin.iOS API で行われる内容を反映しています。

C# イベントまたはプロパティが自動的に生成されるのは、Android イベント登録メソッドが次の場合のみです。

  1. setOnClickListener など、set プレフィックスを持つ。

  2. 戻り値の型が void である。

  3. パラメータを 1 つだけ受け入れ、そのパラメータ型がインターフェイスであり、そのインターフェイスにメソッドが 1 つだけ存在し、インターフェイス名の末尾が 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 アクティビティなど) にリスナー インターフェイスを実装します。

Runnable

Java は、java.lang.Runnable インターフェイスを利用して委任メカニズムを提供します。 java.lang.Thread クラスは、このインターフェイスの注目すべきコンシューマーです。 Android では、API でもこのインターフェイスが採用されています。 Activity.runOnUiThread()View.post() は顕著な例です。

Runnable インターフェイスには、単一の void メソッドである run() が含まれています。 そのため、C# で System.Action デリゲートとしてバインドするのに役立ちます。. Activity.RunOnUiThread()View.Post() など、ネイティブ API で Runnable を利用するすべての API メンバーに対して Action パラメータを受け入れるオーバーロードをバインドに提供しました。

IRunnable オーバーロードは、置き換えることなくそのままの状態にしています。これは、いくつかの型がこのインターフェイスを実装しているため、直接 Runnable として渡すことができるためです。

内部クラス

Java には、静的な入れ子にされたクラスと非静的クラスの 2 種類の入れ子にされたクラスがあります。

Java の静的な入れ子にされたクラスは、C# の入れ子にされた型と同じです。

非静的な入れ子にされたクラスは、"内部クラス" とも呼ばれ、大きく異なります。 これらには、外側の型のインスタンスへの暗黙的な参照が含まれ、静的メンバーを含めることはできません (この概要の範囲外の他の相違点の中でも特に異なる点です)。

バインドと C# の使用に関しては、静的な入れ子にされたクラスは、通常の入れ子にされた型として扱われます。 一方、内部クラスには以下の 2 つの大きな違いがあります。

  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.CubeEngineCubeWallpaper の中で入れ子になっているか、CubeWallpaperWallpaperService.Engine の包含クラスを継承しているか、CubeWallpaper.CubeEngine が宣言型を取るコンストラクター (ここでは、CubeWallpaper) を持っているかに注意してください (これらはすべて上記で指定されています)。

インターフェイス

Java インターフェイスには 3 つのメンバー セットを含めることができます。そのうちの 2 つで C# から問題が発生します。

  1. メソッド

  2. フィールド

Java インターフェイスは、次の 2 つの型に変換されます。

  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 インターフェイスとしてバインドされます。

Note

Xamarin.Android 1.9 以降では、Java コードの移植を簡素化するために、Java インターフェイス定数が "複製" されています。 このことは、android provider インターフェイス定数に依存する Java コードの移植を改善するのに役立ちます。

上記の型に加えて、さらに 4 つの変更があります。

  1. Java インターフェイスと同じ名前の型が、定数を含めるために生成されます。

  2. インターフェイス定数を含む型には、実装された Java インターフェイスに由来するすべての定数も含まれます。

  3. 定数を含む Java インターフェイスを実装するすべてのクラスは、実装されているすべてのインターフェイスの定数を含む新しい入れ子にされた InterfaceConsts 型を取得します。

  4. Consts 型は廃止されました。

android.os.Parcelable インターフェイスの場合、このことは、定数を含めるための Android.OS.Parcelable 型が存在することを意味します。 たとえば、Parcelable.CONTENTS_FILE_DESCRIPTOR 定数は、ParcelableConsts.ContentsFileDescriptor 定数としてではなく、Parcelable.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_DESCRIPTORBundle.InterfaceConsts.ContentsFileDescriptor に変換できるようになります。

最後に、Android.OS.ParcelableConsts などの Consts サフィックスを持つ型は、新しく導入された InterfaceConsts の入れ子にされた型以外は廃止されました。 これらは Xamarin.Android 3.0 で削除される予定です。

リソース

画像、レイアウトの説明、バイナリ BLOB、文字列辞書は、リソース ファイルとしてアプリケーションに含めることができます。 さまざまな Android API は、画像、文字列、バイナリ BLOB を直接処理するのではなく、リソース ID を操作するように設計されています。

たとえば、ユーザー インターフェイス レイアウト (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 というクラスを生成します。 たとえば、上記の Resources レイアウトでは、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;
    }
}

これにより、drawable/icon.png ファイルを参照するには Resource.Drawable.icon を、layout/main.xml ファイルを参照するには Resource.Layout.main を、辞書ファイル values/strings.xml の最初の文字列を参照するには Resource.String.first_string を使用します。

定数と列挙体

ネイティブ Android API には、int を取得したり返したりするメソッドが数多くあり、その int の意味を判断するために定数フィールドにマップする必要があります。 これらのメソッドを使用するには、ドキュメントを参照して、どの定数が適切な値であるかを確認する必要がありますが、これは理想的な方法とは言えません。

たとえば、Activity.requestWindowFeature(int featureID) について考えてみましょう。

このような場合は、関連する定数を .NET 列挙型にグループ化し、代わりに列挙型を取るようにメソッドを再マップするようにしています。 これにより、考えられる値の IntelliSense による選択を提供できます。

上記の例は、Activity.RequestWindowFeature(WindowFeatures featureId) のようになります。

これは、どの定数が一緒にまとめられ、どの API がこれらの定数を使用するかを把握するための非常に手動的なプロセスであることに注意してください。 列挙型としてより適切に表現される、API で使用されている定数についてのバグを報告してください。