Android で RxJava を利用して Flux してみた


## Flux とは

Facebook が提唱したアーキテクチャの名称で、最近のフロントエンド(JavaScript)界隈でアプリケーションの状態の複雑さに対応する為のアプローチの一つです。

![flux.png](https://qiita-image-store.s3.amazonaws.com/0/35733/e2c4bc8e-0bee-0584-9a52-1300906991d2.png "flux.png")
> https://facebook.github.io/flux/docs/overview.html

旧来の MVC2 もしくは Observer パターンに `Action` `Dispatcher` `Sotre` というコンポーネント名を定義し Flux という名称をつけたもの、と考えてもよいかと思います。

> なぜフロントエンド(JavaScript)界隈で今このアーキテクチャが取り上げられているかと言うと [React](https://facebook.github.io/react/) の仮想DOMの登場がきっかけになっています。アプリケーション開発でおおざっぱに状態(Flux でいうと `Store`)から `View` 全体を再描画するコードを書いても、仮想DOM側で差分でレンダリングされるため描画コストをあまり気にしなくてよくなった、という背景があります。

## 本投稿について

Flux は上述したとおり元々はフロントエンド(JavaScript)界隈でのものですが、この投稿は Flux を Android ネイティブアプリでやってみたよ、という内容です。他のアーキテクチャと比較したり優劣に言及するものではありません。

## 今回、作ったもの

ボタンを押すと数字がカウントアップ/カウントダウンする、簡単なカウンターです。

![スクリーンショット 2016-10-24 22.56.05.png](https://qiita-image-store.s3.amazonaws.com/0/35733/4c3ea2a3-614a-cfaf-268a-6f1dd7c9d3ea.png "スクリーンショット 2016-10-24 22.56.05.png")

完成版のソースコードはこちら(GitHub)に置いてあります。
⇒ https://github.com/hkusu/android-rx-flux-example

各種イベントの伝搬には [RxJava](https://github.com/ReactiveX/RxJava) のストリームを利用しました。

## ソースコードの説明

> 前提として今回のコード例では [Retrolambda](https://github.com/orfjackal/retrolambda) によるラムダ式を利用しています(RxJava 周りのコードを簡易化する為)。もし手元のプロジェクトで導入される場合は、この投稿の後半の『今回、使用したライブラリ』をご参考に導入ください。

### 基底ライブラリ類

`lib` 配下に各画面で Flux を実現する為のライブラリ的なクラスが置いてあります。ここに置いてあるソースコードは普段の画面開発で触ることはあません。

![スクリーンショット 2016-10-24 22.43.56.png](https://qiita-image-store.s3.amazonaws.com/0/35733/19871998-d907-0842-ab17-21cfd1841ffb.png "スクリーンショット 2016-10-24 22.43.56.png")
> [https://github.com/hkusu/android-rx-flux-example/ ... /lib/flux](https://github.com/hkusu/android-rx-flux-example/tree/master/app/src/main/java/io/github/hkusu/rxflux/lib/flux)

- **Action.java**
	- ユーザアクション毎の内容を表すクラスで、アクションを識別する `key` を持ちます。アクションには引数として `value`(任意の型。Null可) も渡すことが出来ます。ユーザアクションの発生毎に Immutable なインスタンスが生成されます。
- **ActionCreator.java**
	- `ActionCreator` はユーザアクションから `Action` を作成するヘルパー的なものです(Android/Fragment 等から直で `Action` を作成するのでなく `ActionCreator` を介す)。ここに置いてあるのは各画面用の `ActionCreator` を作る為の基底クラスです。`Dispatcher` をインスタンス変数として保持し、`Dispatcher` に `Action` を流します。
- **Dispatcher.java**
	- ソースコードの中身を見てもらうと分かりますが、単に RxJava のストリームのハブ(RxJava 的に言うと `Subject`)を保持するだけのクラスです。
- **Store.java**
	- 各画面用の `Store` を作る為の基底クラスです。この `Store` には役目が２つあります。
		- `Dispatcher` を保持し、ストリームから流れてくる `Action` を受け取る。受け取った `Action` の内容により自身の `Store` の状態を更新する。
		- `Store` 自身が `Store` 特有の(`Store` 毎の) RxJava のストリームのハブを保持し、ストリームを購読している Activity/Fragment 等へ状態変更を通知する。

### 各画面の開発

例えば「メイン」画面の作成で `MainActivity.java` を用意する場合、追加で Flux 的に必要なのは `MainAction.java(キー定義)` `MainActionCreator.java` `MainStore.java` の３ファイルです。

> どういう単位で `Action(キー定義)` `ActionCreator` `Store` を作ってもよいと思いますが今回は Activity 単位で作ることとします。特に `Action(キー定義)` は単なる Enum なので、場合によっては他のクラスのインナークラスにしても構いません。

![スクリーンショット 2016-10-24 22.43.22.png](https://qiita-image-store.s3.amazonaws.com/0/35733/a463cd26-dc54-88a7-d194-dcac06969f0d.png "スクリーンショット 2016-10-24 22.43.22.png")
> [https://github.com/hkusu/android-rx-flux-example/ ... /ui/main](https://github.com/hkusu/android-rx-flux-example/tree/master/app/src/main/java/io/github/hkusu/rxflux/ui/main)

ちなみに今回のコードでは `Dispatcher` `○○ActionCreator` `○○Store` のインスタンスは Google の [Dagger(2)](https://github.com/google/dagger) を利用して生成しています。`Dispatcher` `○○Store` は `Application` と同じライフサイクルのシングルトン、`Dispatcher` のインスタンスは `○○ActionCreator` と `○○Store` へコンストラクタで注入(DI)という感じです。Dagger を使わない場合は、同様となるように自前でインスタンスを取り回せばよいかと思います。

#### MainAction.java の作成

`Action.Key` インタフェースを実装した Enum で作成します。画面の状態変更を起こす `Action` の `key` を、必要となるアクションの種類だけ列挙します。

> 適宜パッケージローカルにすることにより、他の Activity からの誤爆を防ぐことができます。

```java:MainAction.java
// ...

enum MainAction implements Action.Key {
    COUNT_UP,
    COUNT_DOWN,

    // ...    
}
```

#### MainActionCreator.java の作成

`ActionCreator` クラスを継承して作成します。ここでは Activity から呼ぶメソッドを定義します。基底クラスに用意した `dispatch` メソッドを呼ぶことで `Dispatcher` に `Action` を流すことができます。

```java:MainActionCreator.java
// ...

public class MainActionCreator extends ActionCreator {
    private SomeRepository someRepository;

    @Inject
    MainActionCreator(Dispatcher dispatcher, SomeRepository someRepository) {
        super(dispatcher);
        this.someRepository = someRepository;
    }

    void countUp(int num) {
        dispatch(MainAction.COUNT_UP, num);
    }

    void countDown(int num) {
        dispatch(MainAction.COUNT_DOWN, num);
    }

    // ...
}
```

> 今回は細かい説明は省きますが、WEB-API アクセス等の通信が必要な場合は、ここに書きます。今回のコードの例だと `SomeRepository` クラスが WEB-API 経由のデータ操作の為のラッパー的なものです。

#### MainStore.java の作成

`Store` クラスを継承して作成します。例えば `count(カウント)` という状態があるとすると、通常のデータクラスと同じように `count` をインスタンス変数に保持し Getter を用意します。

基底クラスに用した `on` メソッドにアクションの `key` を指定することで `Dispatcher` から当該アクションを購読できます。`on` メソッドの第二引数でアクションを受け取った際に実施する処理(`Store`が保持するデータの変更)を書きます。

```java:MainStore.java
// ...

@Singleton
public class MainStore extends Store {
    private Integer count = 0;

    // ...

    @Inject
    MainStore(Dispatcher dispatcher) {
        super(dispatcher);

        on(MainAction.COUNT_UP, action -> {
            count += (Integer) action.value;
        });

        on(MainAction.COUNT_DOWN, action -> {
            count -= (Integer) action.value;
        });

        // ...
    }

    // ...

    Integer getCount() {
        return count;
    }

    // ...
}
```

#### MainActivity.java

長くなるので全体は GitHub 上のソースをご覧ください。
⇒ [MainActivity.java](https://github.com/hkusu/android-rx-flux-example/blob/master/app/src/main/java/io/github/hkusu/rxflux/ui/main/MainActivity.java)

下記、抜粋して説明します。

`MainActionCreator` `MainStore` への参照はインスタンス変数として Activity に保持します。

```java:
private MainStore mainStore;
private MainActionCreator mainActionCreator;
```

> 今回のコード例だと Dagger の Component 経由でインスタンスを取得しています。

状態変更を起こすユーザアクション(カウントアップ/カウントダウンボタン押下など)では `MainActionCreator` に用意したメソッドを呼ぶようにします。

```java:
mainActionCreator.countUp(1);
```

```java:
mainActionCreator.countDown(1);
```

`onResume` ライフサイクルで `mainStore` の変更を購読＆UIを更新する処理を書きます。基本的に `MainStore` の Getter を通して値を取得しUI上のデータを更新(全書き換え)すればOKですが、ユーザアクションもここまで伝搬(下記のコードでいうと`action`変数)するようになっているので、必要に応じてユーザアクションの種類(`action.key`)によって場合分けしてUIを更新します。

```java:
@Override
protected void onResume() {
    super.onResume();
    mainStore.observeOnMainThread(action -> {
        // UI を更新する処理をここに
    });
}
```

以上が開発の基本的な流れとなります。

## 感想というか雑なメモ

- データの流れが一方方向なので分かりやすい。`Store` を見ればアプリの状態、画面の状態が分かる。UIを更新する箇所が一箇所なので分かりやすい、宣言的に書ける。
- ちょっとしたことをやりたい時にコード量が増えて煩雑と感じるかも。
	- ユーザアクションがあったら○○を更新、というのが同期的に書けず `Dispatcher` にアクションを投げるという感じになるので、アプリの設計の考え方を従来と変えないといけない。
- `Store` を `Application` と同じ生存期間のシングルトンとしていることで、仮に Activity が閉じていても裏側で `Store` を更新できるので可能性を感じる。例えると"いいね"された情報の画面間の同期とか。
	- Activity を再度起動した際に画面の状態を復元することもできる。
	- 従来 Activity へ引数を渡してた処理も `Store` 経由の値渡しで代替できる？
	- `Store` が生き続けるのでメモリとかリークとか大丈夫？
- 現状の実装では `Store` に変更があった場合に画面要素を全て再描画するようにしているが、パフォーマンス的に大丈夫か（例えば ListView や凝った画面など）。
	- 差分を見て更新があったもののみにした方がよいかもしれない。
	- JavaScript の [React](https://facebook.github.io/react/) + Flux の場合は React の仮想DOMの仕組みにより差分更新が実現できている。
- 現状の実装では `Action` の `value` に任意の型のデータ渡せるようにしてあるが、データを受け取る側で型チェックできてない且つコンパイルでもチェックできてないので、型を間違えると実行時エラーになる可能性を含んでいる。
- テストコードはまだ書いてないので何とも言えない。
- 今回は Activity に対して `Store`、`ActionCreator`、`Action(キー定義)` を作ったが、ベストとは思えないので何に対して、どういう単位でそれぞれ作るか試行錯誤が必要そう。
- Data Binding を組み合わせるといいかも。
- `Store` を永続化してもいいかもしれない。
- `Store` に親子関係(子が親のデータ変更を監視)を持たせてもよいかも。

いずれにせよ実運用し数をこなさないと何とも評価し難いかなと。幸い、いま仕事で新しいアプリ開発の立ち上げに携わっているので、この構成を部分的にでも試してみようと思っています。

## ほか

### 今回、使用したライブラリ

- [Dagger 2 (ver 2.7)](https://github.com/google/dagger)
- [RxJava (ver 1.2.1)](https://github.com/ReactiveX/RxJava)
- [RxAndroid (ver 1.2.1)](https://github.com/ReactiveX/RxAndroid)
- [Gradle Retrolambda Plugin (ver 3.3.0)](https://github.com/evant/gradle-retrolambda)
- [Butter Knife (ver 8.4.0)](https://github.com/JakeWharton/butterknife)

	> 今回ような実装方針で Flux やるのに必須なのは RxJava です。インスタンスの作成/シングルトン化は普通に自前でインスタンスを取り回しても良いですが、Dagger を利用した方が楽かと思います(学習コストは多少ありますが)。

	```diff:build.gradle
	buildscript {
	    repositories {
	        jcenter()
	    }
	    dependencies {
	        classpath 'com.android.tools.build:gradle:2.2.1'
	+        classpath 'me.tatarka:gradle-retrolambda:3.3.0'
	    }
	}
	
	allprojects {
	    repositories {
	        jcenter()
	    }
	}
	
	task clean(type: Delete) {
	    delete rootProject.buildDir
	}
	```

	```diff:app/build.gradle
	apply plugin: 'com.android.application'
	+ apply plugin: 'me.tatarka.retrolambda'
	
	android {
	    compileSdkVersion 24
	    buildToolsVersion "24.0.3"
	    defaultConfig {
	        applicationId "io.github.hkusu.rxflux"
	        minSdkVersion 21
	        targetSdkVersion 23
	        versionCode 1
	        versionName "0.0.0"
	        testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
	    }
	    buildTypes {
	        release {
	            minifyEnabled false
	            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
	        }
	    }
	+    compileOptions {
	+        sourceCompatibility JavaVersion.VERSION_1_8
	+        targetCompatibility JavaVersion.VERSION_1_8
	+    }
	}
	
	dependencies {
	    compile fileTree(dir: 'libs', include: ['*.jar'])
	    androidTestCompile('com.android.support.test.espresso:espresso-core:2.2.2', {
	        exclude group: 'com.android.support', module: 'support-annotations'
	    })
	    compile 'com.android.support:appcompat-v7:24.2.1'
	    compile "com.android.support:support-v4:24.2.1"
	    compile "com.android.support:support-annotations:24.2.1"
	+    compile 'com.google.dagger:dagger:2.7'
	+    annotationProcessor 'com.google.dagger:dagger-compiler:2.7'
	+    provided 'javax.annotation:jsr250-api:1.0'
	+    compile 'io.reactivex:rxjava:1.2.1'
	+    compile 'io.reactivex:rxandroid:1.2.1'
	+    compile 'com.jakewharton:butterknife:8.4.0'
	+    annotationProcessor 'com.jakewharton:butterknife-compiler:8.4.0'
	    testCompile 'junit:junit:4.12'
	}
	```

> 新しめの Gradle(ver `2.2.1`) なので別途 `apt` は必要なく標準で `annotationProcessor` が利用できました。また Retrolambda を導入するにあたり、開発環境(OS X)の Java は Java 8 を利用しています。

### Flux の実装で参考にしたソース

- https://github.com/lgvalle/android-flux-todo-app
- https://github.com/azu/mini-flux
	- こちらは Android でなく JavaScript でのサンプルですが簡潔な構成でとても分かりやすかったです。最初はこの実装を Java で書き換えるところから始めました。