この記事は、[学生さん・初心者さん大歓迎!]Xamarin Advent Calendar 2016 の9日目の記事です。
はじめに
Androidなんかでは、アプリ設計をどのようにするかは結構白熱した議論がされており、MVPだMVCだCleanArchitectureだみたいな感じでビシっと決まっていないのが現状です。
一方で、Xamarin.FormsではMVVMを用いるのがほぼスタンダードとなっており、Prism for Xamarin.Forms や MVVM Light Toolkit といったライブラリによってMVVMでやれば楽という環境が整っています。
この記事では、そんな中でなんとかReactiveExtensions(Rx)+Fluxを使っていい感じにできないかというのを実践してみた記事になります。
Rxとは
ReactiveExtensions(Rx)は、C#のLINQを更に進化させたすごいやつです。
C#では結構知名度はあるとは思いますが、最近ではRxJavaやRxSwiftといった各言語へのポートが充実し始め、熱が高まっています。
こわくないReactive Extensions超入門
http://qiita.com/acple@github/items/6cfee916f09632037a6e
Fluxとは
Facebookが提唱しているUI設計のためのアーキテクチャの1つです。
もともとはWeb向けに提唱されているものです。
データが1方向にしか流れなくなるので、流れが把握しやすくなります。
Rxと非常に相性がいい(個人的感想)です。
解説の前に
今回は、以下のAndroid向けの導入記事をかなり参考にさせていただいております。
RxJava + Flux (+ Kotlin)によるAndroidアプリ設計
http://qiita.com/satorufujiwara/items/cbf304891daec87ba5b7
コードや構成なども比較的似ているので、Androidわかる方は見ておくといいかもしれません。
作ったもの
Githubユーザー名を入力してもらい、Getボタンをおすことでリポジトリ一覧が表示されるものです。
今回のソースコードを置いているリポジトリはこちらです。
必要なNuGetパッケージ
-
Microsoft.Net.Http
- ネットワーク通信
-
Newtonsoft.Json
- JSONのパース
-
PCLStorage
- SQLiteのデータベースの保管
-
sqlite-net-pcl
- データベース
-
System.Reactive
- ReactiveExtensions(Rx)
-
Unity
- Dependency Injection
名前空間
XamarinFormsFlux
├─Data : 外部APIとのやり取り
├─Db : SQLiteのコネクションなど
├─Model : モデル
├─Ui : PageやAction, Store, Dispatcherなど
└─Util : ユーティリティー
Unityに関して
Unity(ゲームエンジンじゃない方)は、Xamarin上で使用できるDIコンテナです。
XamarinにはすでにDependencyServiceという依存性注入の仕組みがありますが、今回の用途では使いにくい部分が多かったので、こちらを採用しました。
詳しくは以下にまとめられています。
Xamarin の救世主 Unity!
http://qiita.com/matatabi_ux/items/b7fc1cf828bfe84364cd
今回は、MainComponent.csにおいて、依存性解決を行うUnityContainerを生成、型の登録を行っています。
後述のMainActionやMainStoreなどは、全てDIで生成させています。
クラス解説
MainPage.xaml.cs
今回表示させるPageです。
コンストラクタ内でMainComponent経由でMainStoreとMainActionを生成しています。
MainComponent自身は遅延生成されるようになっています。
Lazy<MainComponent> component = new Lazy<MainComponent>(() => Init());
public MainStore mainStore { get; set; }
public MainAction mainAction { get; set; }
public MainPage()
{
InitializeComponent();
component.Value.Inject(this);
}
InitData
関数内で、Storeから各種データの取得を行います。
データはRxのObservable上に流れ、何かしら変更があった場合はこのObservableにデータが流れてくることになります。
流れてきたデータをViewに対して表示させるか、何かしらのイベントを発火させるかのみにします。
void InitData()
{
mainStore.Repos()
.Subscribe((items) =>
{
Device.BeginInvokeOnMainThread(() => { listView.ItemsSource = items; });
});
mainStore.Errors()
.Subscribe((exception) =>
{
Device.BeginInvokeOnMainThread(() => { listView.ItemsSource = exception.ToString(); });
});
mainStore.Name()
.Subscribe(async (name) =>
{
await mainAction.Refresh();
});
}
InitListener
関数内では、ボタンクリックイベントに対してActionを呼び起こすだけに留めます。
void InitListener()
{
button.Clicked += async (sender, e) =>
{
await mainAction.Name(entry.Text);
};
}
Page内で行っているのは「何かしらのイベントが起きたらActionを呼ぶ」ことと、「データがやってきたら反映させる」の2つの動作の登録のみです。
MainAction.cs
Actionとして用意されているのは「アイテムをAPIから取ってくるRefresh」と「取得させるユーザー名を変更させるName」
共に、作業の結果はDispatcherに対して投げられます。
Refresh
関数では、APIからデータを取得した上で、データベースに追加する作業を行います。
public async Task Refresh()
{
await Task.Factory.StartNew(() =>
{
dispatcher.nameSubject
.Take(1) // 取得するべきユーザー名を取得する
.SelectMany((name) => { return repository.GetRepos(name); }) // リポジトリから取得する(RxのFlatMapを使用している)
.Subscribe((items) => { dispatcher.Dispatch(items); }, // DBにデータを投げる
(e) => { dispatcher.errorSubjecct.OnNext(e); }); // エラーだったとき
});
}
Name
関数は、受け取った文字列をそのままDispatcherへ渡します。
(ここでは、RxのSubjectを使っています。詳しくはDispatcherのところでやります。)
public async Task Name(string name)
{
await Task.Factory.StartNew(() => { dispatcher.nameSubject.OnNext(name); });
}
Action内では、イベントに応じて、データを作成する機構を提供します。
MainStore.cs
次は、Pageでデータの取得口となるStoreについて見ていきます。
しかし、StoreでやっているのはDispatcherで提供されるObservable/Subjectを返しているだけです。
public IObservable<IList<Item>> Repos()
{
return dispatcher.Repos();
}
public IObservable<string> Name()
{
return dispatcher.nameSubject.AsObservable();
}
public IObservable<Exception> Errors()
{
return dispatcher.errorSubjecct.AsObservable();
}
それでは、ActionとStore両方から見られているDispatcherとは何者なのか見ていきましょう。
MainDispatcher.cs
MainDispatcherでは、主に以下の4つの機能があります。
- 取得させるユーザー名を保持する
- エラーを保持する
- DBに対してAPIから取得したデータを保管させる
- DBに変更があったらユーザー名にマッチするデータを流す
この内、上2つの保持する部分には、Rxで提供されるSubjectを使用しています。
Subjectに関しては、自分の書いた記事を見ていただければと思います。
Rxで知っておくと便利なSubjectたち
http://qiita.com/ralph/items/f7205c8171826cc2153b
(記事はRxJavaに関してですが、基本の操作は同じです)
今回、nameSubjetはBehaviorSubject
、errorSubjectはSubject
(RxJavaでのPublishSubject)です。
MainActionのName
関数でこのnameSubject
にOnNext
で新しい名前を渡し、MainStoreではName
関数がObservableとしてSubjectを返します。
それをMainPage内でSubscribeしているので、結果としてはMainPageのボタンをクリックすることでmainAction.Refresh
関数が呼ばれるということになります。
ボタンがクリックされる
↓
MainPage#mainAction.Name()が実行される
↓
MainDispatcher#nameSubject.OnNext()が実行される
↓
(MainDispatcher#nameSubjectをMainStore.Name()でObservableとして返している)
↓
(そのObservableをMainPageないでSubscribeしている)
↓
mainAction.Refresh()が実行される
下2つの機能に関してはDBが絡んできます。
まず、指定したユーザーのアイテムのリストを取得するためのRepo
関数についてです。
public IObservable<IList<Item>> Repos()
{
return triggers.AsObservable() // triggersにOnNextされるたびに発火する
.SelectMany(_ => nameSubject.Take(1)) // 最新のユーザー名を取得
.Select((name) =>
{
IList<Item> items = new List<Item>();
using (SQLiteConnection connection = db.CreateConnection())
{
foreach (Item item in (from x in connection.Table<Item>() where x.UserName == name orderby x.Id select x))
{
items.Add(item);
}
}
return items;
});
}
triggersというSubjectをSubscribeしており、このtriggersが発火するごとに最新のユーザー名を取得してその名前でDBへアクセスしています。
このtriggersへのOnNextを呼び出しているのが、データ保管を行うDispatch
関数です。
public int Dispatch(IList<Item> items)
{
int count = 0;
using (SQLiteConnection connection = db.CreateConnection())
{
connection.BeginTransaction();
foreach (var i in items)
{
count += connection.InsertOrReplace(i);
}
connection.Commit();
triggers.OnNext("");
}
return count;
}
引数として保管するアイテムのリストを渡すことで、DBに保管してくれます。
そして、DBへのトランザクションの終了と同時にtriggers.OnNext("");
を呼び、Reposの方のtriggers
を発火させます。
このDispatch
関数は、MainActionのRefresh
関数で呼ばれているので、
MainActionのRefreshを呼ぶ
↓
APIから取得
↓
DBに保存
↓
ReposのObservableが発火
↓
MainActionのReposをSubscribeしているところにデータがやってくる
という一連の流れが実現されています。
この仕組みは、上で紹介したAndroidの方のサンプルで使用されているSQLBriteというライブラリで採用されているテーブルの更新通知システムを簡易的に模倣したものです。
SQLBriteがテーブル変更を通知する仕組み
http://qiita.com/ara_tack/items/6d1b75ab28e86a8b1477
問題点
Action→Dispatcher→Store→Viewの流れは上のようにすることで実現できました。
しかし、おそらくC#でのRxに慣れていなかったり、設計そのもののせいで色々問題点があります。
問題1
Dispatcher→Actionのデータの流れが含まれている
コードを見ていてあれ?と思った方もいるかもしれませんが、取得するユーザー名を入力し、それに対応するリポジトリ一覧が返るように実装したところ、MainAction内でDispatcherのSubjectから値を取り出す必要が出てしまいました。
問題2
Thread本当にこれでいいのか問題
本来、RxにはSubscribeOn
とObserveOn
という関数で実行するスレッドを切り替える機能があります。
SubscribeOnとObserveOnの使い分け
http://qiita.com/Temarin/items/a0525d73e8981489dd3e
しかし、何故かすべてがUIスレッド上で実行されてしまうため、Actionの関数をすべてasync
にすることで強制的にスレッドを移動させ、Subscribeした中でDevice.BeginInvokeOnMainThread
を使うことでUIスレッド上での実行を行っています。
まとめ
- 素直にMVVM使ったほうが楽だと思います。
- しかしチャレンジしたい方は採用するのもアリだと思います。
- Rxすごい!便利!
参考にさせていただいたサイト
Xamarin.FormsでSQLiteをPCL内で完結して使う方法
http://www.nuits.jp/entry/2016/06/27/191636