はじめに
これは Xamarin Advent Calendar 2017 の 12 日目の記事です。
ここ半年くらいの趣味テーマだった "Xamarin.Forms の Platform 実装の方法" について、今まで理解してきた事をこの機会に一旦まとめさせてもらおうと思います。
私自身の実践の結果として Xamarin.Forms for Unity というプロジェクトをちょっとずつ進行中です。
また、最初に概要をつかむために Windows Forms をベースにざっくり実装しています。 Unity の方は結構実装が進んでしまっているので、逆に WinForms 版の方が「あ、こんな程度でも一応動くんだ」と初手の参考にはいいかもしれません。
Xamarin.Forms.WinForms ※2017/11/29版
つくりかた
@atsushieno さんの昨年の記事がとても詳しいです。
Xamarin.Formsに新しいプラットフォームを追加する: 前哨戦
こちらを参考にしてください。
以上!
・・・すいません。冗談です。
ただとても参考になる記事ですので、ぜひ読んでおいてください。
上記の記事を読むにあたって一点注意点ですが、 Platform 実装をする際に障害となる "基本インターフェース、クラスの internal 定義問題" は 2.4.0 で解消されています (public 化された) ので、対処は不要です。
Xamarin.Forms の構成、考え方
Xamarin.Forms によるアプリケーションは主に 3 つのパートに分かれています。
- a) Xamarin.Forms の Core ライブラリ (PCL / .Net Standard)
- b) Xamarin.Forms の Platform ライブラリ (各プラットフォーム専用)
- c) アプリケーションコード
Xamarin.Forms の研究を始める前までは Core ライブラリがフレームワークとしての枠組みを実装しており、 Platform は既定のインターフェースやクラスを実装し、枠にはめ込む事で基盤が動作する、と思ってました。
ですが、実態は Windows で例えると
- a) Win32 API
- b) Windows そのもの
- c) アプリケーション (.exe)
と見た方が適切でした。よくよく考えたら Platform なのですから当然だったのですが。
全ての主体は b) の Platform 実装が握っており、 b) では a) と c) の定義 (実装) をどのような手法で解釈、表現するかを全く自由に定義、実装する事が可能です。 Core で定義されている論理的な VisualElement, View をどのようにネイティブコードに反映させるかは Platform 実装の裁量です。 Core ライブラリで「このインターフェースを実装しなくてはならない (技術的に強制されているもの) 」というのものはそれほどありません。
ただ Xamarin.Forms の場合、ちょっと特殊な事情がありまして、 クラス、インターフェースの定義の仕方に技術的な制約がないところでも実際は暗黙のルールに従わなくてはならない というのがあります。
最たる例が VisualElementRenderer 。アプリでカスタムレンダラーを実装された方はご存知だと思いますが、どのプラットフォームでも VisualElementRenderer クラスを継承して IVisualElementRenderer を実装することになるかと思います。
VisualElementRenderer は UI コンポーネントを共有の Core ライブラリと各 Platform のネイティブ実装との間の糊付けをする基底クラス、インターフェースですが、ネイティブがからむ以上 Core ライブラリでは定義できないので各 Platform アセンブリ毎に 同じ名前で異なる内容の定義をしています。
Android 版:
public interface IVisualElementRenderer : IRegisterable, IDisposable
{
VisualElement Element { get; }
VisualElementTracker Tracker { get; }
AView View { get; }
event EventHandler<VisualElementChangedEventArgs> ElementChanged;
event EventHandler<PropertyChangedEventArgs> ElementPropertyChanged;
SizeRequest GetDesiredSize(int widthConstraint, int heightConstraint);
void SetElement(VisualElement element);
void SetLabelFor(int? id);
void UpdateLayout();
}
※ ViewGroup プロパティは Obsolte なのでここでは消しました。
iOS 版:
public interface IVisualElementRenderer : IDisposable, IRegisterable
{
VisualElement Element { get; }
NativeView NativeView { get; }
NativeViewController ViewController { get; }
event EventHandler<VisualElementChangedEventArgs> ElementChanged;
SizeRequest GetDesiredSize(double widthConstraint, double heightConstraint);
void SetElement(VisualElement element);
void SetElementSize(Size size);
}
この二つを見比べると Element プロパティなど Core ライブラリ側の定義型は共通に定義されている一方、それ以外のほとんどは異なる定義になっています。異なるといっていも全く異なるところから雰囲気的に似ているところもあります。
ネイティブ層がからむところは共通性がないので各 Platform が完全に自由にしてよいはずですが、できる範囲で共通性のある定義にしているのはカスタムレンダラーの実装などで一人のアプリケーション開発者が複数の Platform に依存したコードを書く事が想定され、その際に共通性をもたせておくことでアプリケーション開発者がなるべくスムーズに作業できるようにするためというのが大きいのではないかと思います。
このような形になっているのにも関わらず、 Platform の実装手法は (おそらく) ドキュメントが存在しないので、結局のところ 公式 Platform 実装を全て読んでその最大公約数的な共通項を見切る という事をしないといけません。単一の Platform を読んでも正解にはならない可能性が高いので最低 2 つ以上読む事をお勧めします。私の場合は WinRT 版と Android 版を基本に時々 iOS 版を読むって感じでした。
※ドキュメントがありましたら教えてください・・・
Platform 実装の構成
Platform 実装は慣例として "Xamarin.Forms.Platform.(プラットフォーム名)" 以下の名前空間に定義します。
Platform 実装は主に 3 つのパートに分かれます。
Platform 情報クラス
Platform の情報を定義するクラス群です。
この部分は比較的 Core ライブラリで規定されているインターフェースやクラスを実装しなくてはいけない部分が多いため、作業が明確で割と手のつけやすい領域です。
この辺のインターフェース、クラス実装が必要になります。
全てのメソッドを実装しなくてもとりあえず動くので、わからないところは throw new NotImplementedException(); で構いません。 Ticker, IIsolatedStorageFile は実装自体もなくてとりあえずは支障ないと思います。
- Xamarin.Forms.IPlatformServices
- Xamarin.Forms.IPlatform
- Xamarin.Forms.DeviceInfo
- Xamarin.Forms.ExpressionSearch
- Xamarin.Forms.Ticker
- Xamarin.Forms.IIsolatedStorageFile
スタートアップコード
各プラットフォームのエントリポイントに該当するところに記述する初期化コードです。
上記の情報クラスや Application クラスの初期化をし、最後に Application.SendStart をすると Xamarin.Forms アプリケーションが開始されます。
情報クラスの初期化は慣例として Forms クラスを定義し、その中で static な Init メソッドを定義、実装することになっているようです。これは "Forms.Init であること" が重要で、パラメーターはそのプラットフォームでの初期化に必要なものを自由に指定して OK です。
実際の処理フローはプラットフォームに依存して混沌としてしまう傾向にありますが、この部分はアプリケーション開発者が直接みることは通常ないと思うので、要件さえ満たしていれば細かいところは気にしなくてもよいかもしれません。
VisualElementRenderer
Xamarin.Forms で定義されている抽象的な UI 要素 (Xamarin.Forms.VisualElement 継承クラス) をプラットフォーム固有の UI に変換、描画をする処理を実装します。
こちらは Core ライブラリで規定されているインターフェースはありませんが、ある程度共通の (暗黙) ルールに基づいた実装をするのが望ましい部分です。
私が把握している共通ルールとしては下記のようになります。
- IVisualElementRenderer インターフェースを定義する。
- IVisualElementRenderer を実装する VisualElementRenderer ジェネリクスクラスを定義する。 VisualElementRenderer は内部用に下記クラスを定義し、それを用いて実装する。
- VisualElementTracker 。 VisualElement のイベント、プロパティを監視し VisualElementRenderer 側に反映する処理を実装する。
- VisaulElementPackager 。 VisualElement の子要素 (Children) の追加、削除に追従する処理を実装する。
- VisualElementTracker 。 VisualElement のイベント、プロパティを監視し VisualElementRenderer 側に反映する処理を実装する。
- View クラスに特化した ViewRenderer ジェネリクスクラスを定義する。
- DefaultRenderer, LayoutRenderer を定義する。
- 各 VisualElement 継承クラスに対応する VisualElementRenderer を実装する。
上記で挙げたインターフェース、クラス名は Core に定義はありませんが、どの Platform 実装でもこの名前で定義がされていますので、これらについては名前を合わせるべきでしょう。また、プロパティやメソッドについても極力合わせるようにしてください。
終わりに
当初は具体的な実装についても言及しようかと思いましたが、あまりにも長くなりそうだったので概要でまとめる形にしました。
この記事の内容を踏まえてソースコードを読んでいただければ理解も早いのではないかと思います。また、最初に書きましたが私の実装は不完全でありますが、その分実装量も公式のものより少ないですので、逆に参考にしやすいのではないかと思います。