Xamarin.FormsのMVVM
Xamarin Advent Calendar 2016 (その1) 3日目(12月3日)です。現在アメリカにいて日付の感覚が合っていないかもしれませんが、こちらはまだ1日前(12月2日)です。(言い訳w)
半年ほどXamarin.Formsで開発してみました。ほぼ、業務アプリ要件であれば何でもできるようになりました。できないことは多分ないです。共通化・生産性を考えれば、これ以上はないほど気に入りました。
アドベントカレンダーネタとして何を書こうか考えていたのですが、海外にいてあまり時間がなかったので、Xamarin.FormsのMVVM周りの話を超かんたんレベルで書いてみます。
色々試して、実用レベルのアプリを作った経験でのお話です。個人の好みが入りますが、ご了承ください。
MVVM Framewrok for Xamarin
Xamarin向けのMVVM Frameworkって結構色々あって皆さん悩んでいるんじゃないでしょうか。
- Prism
- ReactiveUI
- MvvmCross
- ReactiveProperty
などなどが有名どころですか。
それぞれ、ある程度試したり、実装見てやってみました。
最終的に落ち着いたところは、何も使わないです。
意外と知られていないかもしれませんが、Xamarin.Forms自体がMVVMのサポートをほぼ満たしてくれるんですね。
MVVM Framewrokを使うメリット
- 使い方によっては生産性上がり便利
- MVVM以外の非機能要件で使えるものもある(RX, i18n, 非同期など)
- 実装方針がある程度決まる
MVVM Framewrokを使うデメリット
- モバイルアプリのサイズがでかくなる
- 他のライブラリーとの依存関係が合わない、バージョン依存
- 最新アップデートにすぐ対応されないことも
- 作り方に制約ができる、自分の理想と違うコードになったり
- 機能がtoo much、モバイル開発なんで使わない機能なんか入れたくない
今回は、デメリットの方が遥かにでかかったのと、実際に使ってみてパッとしなかったので何も使わない方針としました。
結果、全く問題なかったです。性能良く、バイナリーサイズの小さいアプリができました。
実装レイヤーを増やして複雑になるよりは、シンプルなMVVM階層とし、ステップ数を減らす努力をしたほうが良いです。(個人的に)
例えば、ReactivePropertyなんかはPrimitiveなデータ構造捨てて、超Framework密結合な作りになってしまいますね。個人的にPrimitiveかつOpenで汎用性の高い作りが好きです。もちろんReactiveでAsynchronousなコードを書きたい時は、RXのみ導入するのは全然ありです。
生Xamarin.FormsのMVVM階層
- Model VMのラッパーか、データソースか
- View 絶対にXamlで書いた方がいいです。ConverterやFormatterは標準の機能でほぼ足ります
- ViewModel WebAPIとSerializableであればModelと一緒の1階層でもOKに思います。余計なDTOを使うのはナンセンス(Framewrokとしても)です
基本データバインディングなので、Xamlを駆使してViewModelの見せ方を変えます。
ここまでのところ、Xamarin.Formsの持つSDKの範囲内で問題なく実装できます。問題は、WPFやWinFormsでもお馴染みのINotifiyPropertyChangedの実装をどうするかという点です。
昔からこの問題に対しては色々な対策が考えらています。
10年以上C#開発やってきて、現時点でベストだと思う方法があります。以下Fodyを使います。
Fody.PropertyChanged
FodyはILのアセンブリ操作ライブラリーです。
公式によると、
Manipulating the IL of an assembly as part of a build requires a significant amount of plumbing code. This plumbing code involves knowledge of both the MSBuild and Visual Studio APIs. Fody attempts to eliminate that plumbing code through an extensible add-in model.
ということだそうです。
その中で有名な拡張としてFody.PropertyChangedがあります。要は、ビルド時にいい感じにしてくれるってことです。
普通のINotifyPropertyChanged実装
Xamariの公式ヘルプからINotifyPropertyChangedのコードを引用します。
using System;
using System.ComponentModel;
using Xamarin.Forms;
namespace XamlSamples
{
class ClockViewModel : INotifyPropertyChanged
{
DateTime dateTime;
public event PropertyChangedEventHandler PropertyChanged;
public ClockViewModel()
{
this.DateTime = DateTime.Now;
Device.StartTimer(TimeSpan.FromSeconds(1), () =>
{
this.DateTime = DateTime.Now;
return true;
});
}
public DateTime DateTime
{
set
{
if (dateTime != value)
{
dateTime = value;
if (PropertyChanged != null)
{
PropertyChanged(this,
new PropertyChangedEventArgs("DateTime"));
}
}
}
get
{
return dateTime;
}
}
}
}
何というクソコードでしょう。実践でこんなクソコード見たことがありません。
Fody.PropertyChangedを使った実装
それでは、Fody.PropertyChangedを使った場合はどうでしょう。これも公式からそのまま引用したので、クラスやPropertyが違いますが気にしないでください。
[ImplementPropertyChanged]
public class Person
{
public string GivenNames { get; set; }
public string FamilyName { get; set; }
public string FullName
{
get
{
return string.Format("{0} {1}", GivenNames, FamilyName);
}
}
}
何ということでしょう。PropertyChangedの処理が一切無くなって、PrimitiveかつOpenで汎用性の高いデータクラスです。
唯一の実装は、以下のAttributesのみです。
[ImplementPropertyChanged]
Fody.PropertyChangedを使った実装をデコンパイルした中身
それではコンパイルされたアセンブリー(DLL)の中身はどうなっているでしょうか。適当にdotPeekなどのデコンパイラーで確認できます。
public class Person : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
string givenNames;
public string GivenNames
{
get { return givenNames; }
set
{
if (value != givenNames)
{
givenNames = value;
OnPropertyChanged("GivenNames");
OnPropertyChanged("FullName");
}
}
}
string familyName;
public string FamilyName
{
get { return familyName; }
set
{
if (value != familyName)
{
familyName = value;
OnPropertyChanged("FamilyName");
OnPropertyChanged("FullName");
}
}
}
public string FullName
{
get
{
return string.Format("{0} {1}", GivenNames, FamilyName);
}
}
public virtual void OnPropertyChanged(string propertyName)
{
var propertyChanged = PropertyChanged;
if (propertyChanged != null)
{
propertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
}
}
実装は、ちゃんとpropertyChangedに展開されています。そして注目は、FullNameに対して、GivenNamesとFamilyNameのsetterでOnPropertyChangedが呼ばれています。有能ですね。
まとめ
Xamarin.Formsは、それ自身がMVVMの機能を持っています。Frameworkで迷うぐらいであれば使わないという選択肢もありです。
要件に合わせ、SimpleでLightweightなモバイルアプリを作りましょう。Framework非依存なコードにしておけば後から拡張も容易です。