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

  • 42
    いいね
  • 2
    コメント

Flux とは

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

flux.png

https://facebook.github.io/flux/docs/overview.html

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

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

本投稿について

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

今回、作ったもの

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

スクリーンショット 2016-10-24 22.56.05.png

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

各種イベントの伝搬には RxJava のストリームを利用しました。

ソースコードの説明

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

基底ライブラリ類

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

スクリーンショット 2016-10-24 22.43.56.png

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 をインスタンス変数として保持し、DispatcherAction を流します。
  • Dispatcher.java
    • ソースコードの中身を見てもらうと分かりますが、単に RxJava のストリームのハブ(RxJava 的に言うと Subject)を保持するだけのクラスです。
  • 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 なので、場合によっては他のクラスのインナークラスにしても構いません。

スクリーンショット 2016-10-24 22.43.22.png

https://github.com/hkusu/android-rx-flux-example/ ... /ui/main

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

MainAction.java の作成

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

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

MainAction.java
// ...

enum MainAction implements Action.Key {
    COUNT_UP,
    COUNT_DOWN,

    // ...    
}

MainActionCreator.java の作成

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

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が保持するデータの変更)を書きます。

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

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

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 にアクションを投げるという感じになるので、アプリの設計の考え方を従来と変えないといけない。
  • StoreApplication と同じ生存期間のシングルトンとしていることで、仮に Activity が閉じていても裏側で Store を更新できるので可能性を感じる。例えると"いいね"された情報の画面間の同期とか。
    • Activity を再度起動した際に画面の状態を復元することもできる。
    • 従来 Activity へ引数を渡してた処理も Store 経由の値渡しで代替できる?
    • Store が生き続けるのでメモリとかリークとか大丈夫?
  • 現状の実装では Store に変更があった場合に画面要素を全て再描画するようにしているが、パフォーマンス的に大丈夫か(例えば ListView や凝った画面など)。
    • 差分を見て更新があったもののみにした方がよいかもしれない。
    • JavaScript の React + Flux の場合は React の仮想DOMの仕組みにより差分更新が実現できている。
  • 現状の実装では Actionvalue に任意の型のデータ渡せるようにしてあるが、データを受け取る側で型チェックできてない且つコンパイルでもチェックできてないので、型を間違えると実行時エラーになる可能性を含んでいる。
  • テストコードはまだ書いてないので何とも言えない。
  • 今回は Activity に対して StoreActionCreatorAction(キー定義) を作ったが、ベストとは思えないので何に対して、どういう単位でそれぞれ作るか試行錯誤が必要そう。
  • Data Binding を組み合わせるといいかも。
  • Store を永続化してもいいかもしれない。
  • Store に親子関係(子が親のデータ変更を監視)を持たせてもよいかも。

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

ほか

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

  • Dagger 2 (ver 2.7)
  • RxJava (ver 1.2.1)
  • RxAndroid (ver 1.2.1)
  • Gradle Retrolambda Plugin (ver 3.3.0)
  • Butter Knife (ver 8.4.0)

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

    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
    }
    
    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 の実装で参考にしたソース