LoginSignup
18
11

More than 5 years have passed since last update.

Xamarin.FormsでRx + Fluxでの実装をやってみる

Last updated at Posted at 2016-12-09

この記事は、[学生さん・初心者さん大歓迎!]Xamarin Advent Calendar 2016 の9日目の記事です。


はじめに

Androidなんかでは、アプリ設計をどのようにするかは結構白熱した議論がされており、MVPだMVCだCleanArchitectureだみたいな感じでビシっと決まっていないのが現状です。
一方で、Xamarin.FormsではMVVMを用いるのがほぼスタンダードとなっており、Prism for Xamarin.FormsMVVM 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つです。

c730efea-a42f-4ba7-931d-3acc8919ac32.png

もともとはWeb向けに提唱されているものです。
データが1方向にしか流れなくなるので、流れが把握しやすくなります。

Rxと非常に相性がいい(個人的感想)です。

解説の前に

今回は、以下のAndroid向けの導入記事をかなり参考にさせていただいております。

RxJava + Flux (+ Kotlin)によるAndroidアプリ設計
http://qiita.com/satorufujiwara/items/cbf304891daec87ba5b7

コードや構成なども比較的似ているので、Androidわかる方は見ておくといいかもしれません。

作ったもの

Githubユーザー名を入力してもらい、Getボタンをおすことでリポジトリ一覧が表示されるものです。

device-2016-12-09-120325.png

今回のソースコードを置いているリポジトリはこちらです。

 必要なNuGetパッケージ

 名前空間

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自身は遅延生成されるようになっています。

MainPage.xaml.cs
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に対して表示させるか、何かしらのイベントを発火させるかのみにします。

MainPage.xaml.cs
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を呼び起こすだけに留めます。

MainPage.xaml.cs
void InitListener()
{
    button.Clicked += async (sender, e) =>
    {
        await mainAction.Name(entry.Text);
    };
}

Page内で行っているのは「何かしらのイベントが起きたらActionを呼ぶ」ことと、「データがやってきたら反映させる」の2つの動作の登録のみです。

MainAction.cs

Actionとして用意されているのは「アイテムをAPIから取ってくるRefresh」と「取得させるユーザー名を変更させるName」
共に、作業の結果はDispatcherに対して投げられます。

Refresh関数では、APIからデータを取得した上で、データベースに追加する作業を行います。

MainAction.cs
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のところでやります。)

MainAction.cs
public async Task Name(string name)
{
    await Task.Factory.StartNew(() => { dispatcher.nameSubject.OnNext(name); });
}

Action内では、イベントに応じて、データを作成する機構を提供します。

MainStore.cs

次は、Pageでデータの取得口となるStoreについて見ていきます。
しかし、StoreでやっているのはDispatcherで提供されるObservable/Subjectを返しているだけです。

MainStore.cs
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関数でこのnameSubjectOnNextで新しい名前を渡し、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関数についてです。

MainDispatcher.cs
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関数です。

MainDispatcher.cs
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にはSubscribeOnObserveOnという関数で実行するスレッドを切り替える機能があります。

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

18
11
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
18
11