Flux とは
Facebook が提唱したアーキテクチャの名称で、最近のフロントエンド(JavaScript)界隈でアプリケーションの状態の複雑さに対応する為のアプローチの一つです。
旧来の MVC2 もしくは Observer パターンに Action
Dispatcher
Sotre
というコンポーネント名を定義し Flux という名称をつけたもの、と考えてもよいかと思います。
なぜフロントエンド(JavaScript)界隈で今このアーキテクチャが取り上げられているかと言うと React の仮想DOMの登場がきっかけになっています。アプリケーション開発でおおざっぱに状態(Flux でいうと
Store
)からView
全体を再描画するコードを書いても、仮想DOM側で差分でレンダリングされるため描画コストをあまり気にしなくてよくなった、という背景があります。
本投稿について
Flux は上述したとおり元々はフロントエンド(JavaScript)界隈でのものですが、この投稿は Flux を Android ネイティブアプリでやってみたよ、という内容です。他のアーキテクチャと比較したり優劣に言及するものではありません。
今回、作ったもの
ボタンを押すと数字がカウントアップ/カウントダウンする、簡単なカウンターです。
完成版のソースコードはこちら(GitHub)に置いてあります。
⇒ https://github.com/hkusu/android-rx-flux-example
各種イベントの伝搬には RxJava のストリームを利用しました。
ソースコードの説明
前提として今回のコード例では Retrolambda によるラムダ式を利用しています(RxJava 周りのコードを簡易化する為)。もし手元のプロジェクトで導入される場合は、この投稿の後半の『今回、使用したライブラリ』をご参考に導入ください。
基底ライブラリ類
lib
配下に各画面で Flux を実現する為のライブラリ的なクラスが置いてあります。ここに置いてあるソースコードは普段の画面開発で触ることはあません。
https://github.com/hkusu/android-rx-flux-example/ ... /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
)を保持するだけのクラスです。
- ソースコードの中身を見てもらうと分かりますが、単に RxJava のストリームのハブ(RxJava 的に言うと
-
Store.java
- 各画面用の
Store
を作る為の基底クラスです。このStore
には役目が2つあります。-
Dispatcher
を保持し、ストリームから流れてくるAction
を受け取る。受け取ったAction
の内容により自身のStore
の状態を更新する。 -
Store
自身がStore
特有の(Store
毎の) RxJava のストリームのハブを保持し、ストリームを購読している Activity/Fragment 等へ状態変更を通知する。
-
- 各画面用の
各画面の開発
例えば「メイン」画面の作成で MainActivity.java
を用意する場合、追加で Flux 的に必要なのは MainAction.java(キー定義)
MainActionCreator.java
MainStore.java
の3ファイルです。
どういう単位で
Action(キー定義)
ActionCreator
Store
を作ってもよいと思いますが今回は Activity 単位で作ることとします。特にAction(キー定義)
は単なる Enum なので、場合によっては他のクラスのインナークラスにしても構いません。
https://github.com/hkusu/android-rx-flux-example/ ... /ui/main
ちなみに今回のコードでは Dispatcher
○○ActionCreator
○○Store
のインスタンスは Google の Dagger(2) を利用して生成しています。Dispatcher
○○Store
は Application
と同じライフサイクルのシングルトン、Dispatcher
のインスタンスは ○○ActionCreator
と ○○Store
へコンストラクタで注入(DI)という感じです。Dagger を使わない場合は、同様となるように自前でインスタンスを取り回せばよいかと思います。
MainAction.java の作成
Action.Key
インタフェースを実装した Enum で作成します。画面の状態変更を起こす Action
の key
を、必要となるアクションの種類だけ列挙します。
適宜パッケージローカルにすることにより、他の Activity からの誤爆を防ぐことができます。
// ...
enum MainAction implements Action.Key {
COUNT_UP,
COUNT_DOWN,
// ...
}
MainActionCreator.java の作成
ActionCreator
クラスを継承して作成します。ここでは Activity から呼ぶメソッドを定義します。基底クラスに用意した dispatch
メソッドを呼ぶことで Dispatcher
に Action
を流すことができます。
// ...
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
が保持するデータの変更)を書きます。
// ...
@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
下記、抜粋して説明します。
MainActionCreator
MainStore
への参照はインスタンス変数として Activity に保持します。
private MainStore mainStore;
private MainActionCreator mainActionCreator;
今回のコード例だと Dagger の Component 経由でインスタンスを取得しています。
状態変更を起こすユーザアクション(カウントアップ/カウントダウンボタン押下など)では MainActionCreator
に用意したメソッドを呼ぶようにします。
mainActionCreator.countUp(1);
mainActionCreator.countDown(1);
onResume
ライフサイクルで mainStore
の変更を購読&UIを更新する処理を書きます。基本的に MainStore
の Getter を通して値を取得しUI上のデータを更新(全書き換え)すればOKですが、ユーザアクションもここまで伝搬(下記のコードでいうとaction
変数)するようになっているので、必要に応じてユーザアクションの種類(action.key
)によって場合分けしてUIを更新します。
@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 + Flux の場合は React の仮想DOMの仕組みにより差分更新が実現できている。
- 現状の実装では
Action
のvalue
に任意の型のデータ渡せるようにしてあるが、データを受け取る側で型チェックできてない且つコンパイルでもチェックできてないので、型を間違えると実行時エラーになる可能性を含んでいる。 - テストコードはまだ書いてないので何とも言えない。
- 今回は Activity に対して
Store
、ActionCreator
、Action(キー定義)
を作ったが、ベストとは思えないので何に対して、どういう単位でそれぞれ作るか試行錯誤が必要そう。 - Data Binding を組み合わせるといいかも。
-
Store
を永続化してもいいかもしれない。 -
Store
に親子関係(子が親のデータ変更を監視)を持たせてもよいかも。
いずれにせよ実運用し数をこなさないと何とも評価し難いかなと。幸い、いま仕事で新しいアプリ開発の立ち上げに携わっているので、この構成を部分的にでも試してみようと思っています。
ほか
今回、使用したライブラリ
-
今回ような実装方針で Flux やるのに必須なのは RxJava です。インスタンスの作成/シングルトン化は普通に自前でインスタンスを取り回しても良いですが、Dagger を利用した方が楽かと思います(学習コストは多少ありますが)。
build.gradlebuildscript { 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 }
app/build.gradleapply 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 で書き換えるところから始めました。