XamarinでもF#
序文
こちらはXamarin Advent Calender 2014の20日目の記事です。
こちらでも触れたように今年はガッツリXamarinを実案件に投入してみました。前半に調査だったり他の仕事してたりしてるうちにはや年末でプロジェクト絶賛佳境中です(-_-;)
振り返ってみると当初のツールのバグの多さもだんだんと収まり、今ではかなり安定して開発できるようになりました。今でもバージョンアップはためらいがちですし、VSからのデバッグも一回は必ず失敗する、VSからだと実機になぜかデプロイできないなど問題なくはないですが、そんなに大きな問題は起きていません。
そんな感じの一年でしたが、振り返ってみるとXamarinの素晴らしさもさることながらXamarinでも大手を振って使えるようになったF#の生産性の高さでだいぶ救われたことがありました。
のでこの記事ではXamarinでF#を使うとどんな風に書けるかについて書きたいと思いますw
MVVMとF#
MVVMはUIとロジックを分けるのに使うアーキテクチャとして自分的にはだいぶしっくりしてます。
ModelとViewを分けてその間をつなぐものとしてVMがあり、債務をうまく分けるられることで複雑になりがちなUIプログラミングをだいぶすっきりさせることができます。
けれどMVVMといえど、UIとモデル含めたやり取りが複雑になってくるとVMがややこしくなったり状態管理が煩雑になってきたりします。
ましてや非同期が絡んでくるとなおさらです。
そんなときの解決策の一つとして、F#ではasynchronous workflowという仕組みを用いてUIとのインタラクションとModelとの非同期的なやり取りを状態遷移的なものに即して記述することができます。
上のリンクでも若干触れてるのですが、ちと詳しく。
asynchronous worksflowとは非同期処理を綺麗に書くための記述法でモナド的なcomputation expressionsの一応用です。C#のasync/awaitと似たような感じですが、async/awaitよりも若干高機能だと思います。詳しくはこちら。
それでは実際のコード交えてみてみましょう。
作るもの
Xamarin for iOSを使ってiPhone向けのプロジェクトにしてみました。
MVVMでVM側はF#のPCLプロジェクト、View側はC#、Modelを模したものとして呼び出して3秒たつと非同期的に値が返ってくるものをF#で作りました。素から作ろうとしたらMvvmCrossのNugetでなぜか失敗したり諸々有ったので、今絶賛開発中のものの軒先借りてなんとか動くようにしたのでコード上げられずすみません…のでコードとスクリーンショットで説明したいと思います。
ViewModel
F#で作ったVMです。ベースクラスや記法など不明なものや見慣れないものもあると思いますが、本筋ではないのでスルーします。
//Viewとのインタラクションのために送られるメッセージ type ViewMsg= |VMConfirm of message:string*(bool->unit) type VM() as this= inherit MvxViewModelBase() //viewにメッセージを伝えるためのイベント let _vMsgEvt=Event<_>() //Viewから発火されるイベント let _startEvt=Event<_>() let _startE=_startEvt.Publish //UILabelにバインドされるstring型のTextプロパティ let _txt,_setTxt=this.ToProp "---" "Text" //UIButtonにバインドされるコマンド let _startCmd=toEverCmd(fun _-> _setTxt "---" _startEvt.Trigger()) //状態遷移のループ let rec normal n= async{do! Async.AwaitObservable _startE do! confirmExec n return! normal n } and confirmExec n= async{let! b=Async.FromContinuations(fun(cont,econt,ccont)-> _vMsgEvt.Trigger<|VMConfirm ("猫みたいな生き物助ける?",cont)) if b then let! rs=sayHellow "まどか" n _setTxt rs return! normal<|n+1 else return! normal n } do normal 1|>Async.StartImmediate //外部公開メンバ [<CLIEvent>] member this.VMsgEvt=_vMsgEvt.Publish member this.StartCmd=_startCmd member this.Text= !_txt
ざっくり説明するとUIとのインタラクションで状態遷移するループ部をもち、UIにメッセージを送るためのMessengerの代わりの_vMsgEvt、UIのボタン押下で発火される_startEvtがあります。
ループの最初でstartされるのを待ち、発火されたらコンファーム待ちに遷移します。OKであればモデル(を模したもの)に非同期でリクエストして戻り値の文字をTextプロパティにセットします。
View
VMクラスに対応するUIViewControllerです。
class VCont : MvxViewControllerBase { private UILabel _lbl=new UILabel() { TextColor=UIColor.White, TextAlignment=UITextAlignment.Center, Lines=0, Frame=new RectangleF(0,150,320,70) }; private UIButton _btn = new UIButton() { Frame = new RectangleF(130, 400, 60, 20) }; public new Test.Test.VM ViewModel { get { return (Test.Test.VM)base.ViewModel; } set { base.ViewModel = value; } } public override void ViewDidLoad() { base.ViewDidLoad(); View.BackgroundColor = UIColor.Black; new UIView[]{_lbl,_btn}.ForEach(View.AddSubview); _btn.SetTitle("Start",UIControlState.Normal); ViewModel.VMsgEvt += ViewModel_VMsgEvt; var set= this.CreateBindingSet<VCont, Test.Test.VM>(); set.Bind(_btn).To(vm => vm.StartCmd); set.Bind(_lbl).To(vm => vm.Text); set.Apply(); } void ViewModel_VMsgEvt(object sender, Test.Test.ViewMsg vmsg) { var v = new UIAlertView("", vmsg.message, null, "Cancel", new[] { "OK" }); v.Clicked += (_, e) => { vmsg.Item2.Invoke(e.ButtonIndex == 1); }; v.Canceled += (_, e) => { vmsg.Item2.Invoke(false); }; v.Show(); } }
文字列表示用のラベルとStartするためのボタンを持ちます。
MvvmCross使ってバインドし、ViewModelからのViewMessageに応じてアラートビューを出すハンドラをレジストします。
ハンドラではアラートビューで押されたボタンによってtrue/falseのフラグとともに残りの処理を継続するよう呼び出します。
Model(を模したもの)
//Modelを模した関数3秒たったらレスポンスを返す。 let sayHellow name n= async{do! Async.Sleep 3000 return sprintf "%s、僕と契約して\nF#プログラマーになってよ(´・ω・`)\n(%d回目)" name n }
非同期の要求応答を模して3秒まった後、引数に応じた文字列を返します。本来ならこの辺はアクターとして動くもので実装することが多いのですが、説明の簡略化のためこんな感じに。
どんな風に動いたか
動きを図にまとめると次のようになります。
相互再帰で書けると処理の流れを分岐など含めてそのまま記述できるのでコードの流れも非常に追いやすくなるのではないかと思います。次の状態に移るときにその時の状態コンテキストを引数として引きまわせるので他からの状態変更が入りこむこともなくバグも出にくいと思います。状態外部からの何らかのインタラクションにより状態遷移し、その過程で外部の値を変更したりイベントを上げるなどインタラクションを行います。
一つ欠点としては単純なプロパティの保存・再生では状態を再現できないことですが、逆に状態が変わるときにその状態に代わるときの引数を保存しておき、このコードではコンストラクタの最後にnormal 1|>Async.StartImmediateとしてnormal状態から始めているものを保存された状態と引数をもとにその状態から始めることなどで対処できます。むしろその方が明確に状態の再現ができるとも思います。
内部で状態遷移の流れが複数あるときはループを複数回して相互にインタラクションすることなども可能でしょう。必要であればインタラクションの際にRxを使うことなども有効だと思います。
そのような感じで最近はモデル側をアクターとし、VM内部での状態遷移をモデルとUIとのインタラクションに絡める、つなぎの必要なところでRxを使うという感じで作っていますが、割とうまくいっている気がします。
実際のコードではNavigationやモーダル表示など含めてViewModelからのViewMessageとして実装し動かしています。今のところは良い感じ。
このように作ったうえでモデルやUI側をモックとして作りDIなどでインジェクトすればUIとモデルへの応答含めたユニットテストもしやすいと思います。