Droidux: ReduxをAndroidに持ち込んで状態管理から解放されよう!

  • 99
    いいね
  • 1
    コメント
この記事は最終更新日から1年以上が経過しています。

本稿はpotatotips #23でのトーク内容『"Predictable state container" and DataBinding』をそれっぽく文字に起こしたものになります(実際のLTよりは丁寧に,よりポエムっぽく書いてます).

何か間違い等があればコメント等で優しく教えていただけると幸いです.また,議論も大歓迎です.

TL;DR

  • JSとAndroid・iOSは全く違う環境に見えて目的は近いのでは?
  • JS界の爆発的成長をAndroidに取り入れてみよう!
  • 2015年フロントエンドWebのトレンドなライブラリであるReduxをAndroid向けに実装してみたよ => Droidux

Introduction

Androiderのみなさんから見てJavaScript界隈はどんな風に見えますか?
「jQueryはオワコン!」「Angularは死んだ!」「これからはReactとFluxの時代!」のように1年ごとに目まぐるしく切り替わっていくトレンドを「たいへんそうだなあ」と遠くから眺めている みたいな方が多いと思います.

しかし,個人的にはJava(Android Java)とJavaScript(Web frontend JavaScript)を書く目的はある程度共通していると考えています.

  • ユーザインタフェースを構築する
  • ユーザのインタラクションを適切に捌く
  • アプリケーションの状態をいい感じに管理する

などですね.ということは,この爆速で進化していくJavaScriptの文化をAndroid(あるいはiOSのモバイルアプリ等)に取り入れることができれば,AndroidでもJS界の発展の恩恵をうけることが出来るのではないでしょうか.

本稿では先ほどの「共通の目的」から以下の2点にフォーカスし,JSにおける文化をAndroid Javaに取り入れるとどうなるか という話をしていきます.

  • ユーザのインタラクションを適切に捌く
  • アプリケーションの状態をいい感じに管理する

Application architecture on Android/JavaScript

Android

Androidにおけるアプリケーションアーキテクチャでは,MVC(Model - View - Controller)を聞くことが多いような気がします.しかし,以下の2記事のようにAndroidにおけるMVCは意外とツラいこともたくさんあったりするようです.

MVCがツラい!ということに至ると,MVP(Model - View - Presenter)の方に移行する方が多いようです.また,最近はThe clean architectureのようなものの採用もあるようです(本稿ではこれらの詳細には触れません).

JavaScript

MVW

JavaScriptでもやはりMVなんちゃら(ここではMVW: Model - View - Whateverと呼びましょう)が主流である時期がありました,有名どころではAngularJSの1.x系やBackbone.jsがわかりやすくそんな感じですね.

Flux

よりリッチな環境を求める人たちの影響もあり,Webは狂気のスピードで発展を続けていきます.Single Page Application(SPA)などの発展・需要増加に伴いMVCでは複雑すぎる!スケールしない!とか言い出す人達が出てきました.そこでFacebookが出してきた新たなライブラリ,『なぜ仮想DOMという概念が俺達の魂を震えさせるのか - Qiita』などの影響もあり,日本においても流行の兆しを見せたのがReact.js,そしてFluxです(ReactはUIコンポーネント周りのライブラリなので今回は触れません).Fluxは"Flux eschews MVC in favor of a unidirectional data flow"ともある通り,MVCの複雑さを避け,単方向のデータフロー(unidirectional data flow)を意識するというアーキテクチャです(Fluxについて概要をサッと理解するには@azuさんの10分で実装するFluxという資料がおすすめです).

以下は公式サイトに掲載されているデータフロー図です.

flux-simple-f8-diagram-explained-1300w.png (142.0 kB)

Flux | Application Architecture for Building User Interfaces

図を見れば明らかにデータが一方向に制限されていますね.いろいろ書いてありますが,個人的には「Observerパターンに名前をつけてgoto化を回避したもの」と理解するのがシンプルではないかと思っています.

Redux

前節の図を見ての通りですが,Fluxは比較的登場人物も多く記述が冗長になりがちです.そこで次に出てきたのがReduxです.僕がグダグダ書くよりはIntroduction to Redux // Speaker Deckを見ていただくのが一番早いと思います.

Introduction to Redux // Speaker Deck

Reduxには以下のような3つのprincipleが存在します:

- Three Principles

Reduxはそのシンプルさ・明快さと実用性から瞬く間に流行の最先端を突っ走ることになったライブラリです(完全に主観・完全に別種だけど,立ち位置的にはFluxの置き換えみたいなかんじ?). なんか誰かJSのすごい人が「2013年はAngularJS,2014年はReact.jsなら2015年はReduxだ」みたいなこと言ってたような気がしなくもないです(あいまい).

さて,次節からはこのReduxをAndroidでも使えるように実装してみたという話が続きます.

Droidux

Droiduxは前節で紹介したReduxをAndroidで使いやすいように再設計しJavaで実装したライブラリになります.

Motivation

FacebookがFluxで言及したように,MVWは状態管理まわりが特に複雑化しやすく,アプリの規模が大きくなればなるほど辛くなってくると思います.
一方,Reduxは基本的にはSingle state/Single storeな感じで状態を管理していくので,とりあえずルートになってるStoreにaction投げとけ!みたいな雑な扱いができるので,頭悪くてもとりあえずいい感じに使えそうです(※ メモリとか考えたら1つのStoreに全部結合すると辛くなりそうなので適当に…).

Usage

とりあえず管理対象の状態のクラス作ります.ここではTodoListを例にします.

public class TodoList extends ArrayList<TodoList.Todo> {
    public TodoList() {
    }

    public TodoList(List<Todo> list) {
        super(list);
    }

    public static class Todo {
        // pojoっぽいの
        // immutableにしとくといいことあるよ
    }
}

Reducerを定義します.@Reducerアノテーションに管理対象のstateのクラスを渡してあげます.また,@Dispatchableアノテーションに受けるActionのクラスを渡してあげることでいい感じにActionをハンドリングしてくれます.

なお,Reducerのクラス名と各Dispatchableメソッドの引数は決められていますので.違反するとprocessorがException吐いてきます.また,reducerの各メソッドの実装はオリジナルのReduxにもあるようにimmutableな感じでオブジェクト生成して返してあげると後々幸せになります(副作用減らそう).

@Reducer(TodoList.class)
public class TodoListReducer {
    @Dispatchable(AddTodoAction.class)
    public TodoList addTodo(TodoList state, AddTodoAction action) {
        TodoList newState = new TodoList(state);
        newState.add(new TodoList.Todo(action.getText()));
        return newState;
    }

    // ...
}

続いて,Storeクラスのinterfaceを定義します.BaseStoreを継承し,@StoreアノテーションにはReducerクラスを渡してあげます(複数可).また,interfaceにはstateのGetterメソッド,stateのObservableを返すメソッドが定義できます(旧ver.だと全メソッド自動生成だったのですが,Dagger 2等のコード生成系ライブラリとの相性からこのような形に変更されました).

@Store(TodoListReducer.class)
interface RootStore extends BaseStore {
    TodoList getTodoList();
    Observable<TodoList> observeTodoList();
}

Storeの定義がちゃんとできていれば,Javaのannotation processorがいい感じにStoreクラス(Droidux + interface名)とbuilder()メソッドを生成してくれます.

TodoListStore store = DroiduxTodoListStore.builder()
        .setReducer(new TodoListReducer(), new TaskList())
        .build();

このへんはオリジナルとは違うところで,オリジナルはReducerのfunctionたちをいい感じにcreateStore()に喰わせてあげればStoreが生成されますが,Droiduxでは(型の恩恵を最大限に受けるためにも)Storeクラスを自動で生成します.また,この自動生成の恩恵として,本来はReducer function内に記述するAction.typeによるハンドリング処理をも自動的に実装してくれるというものがあります.

あとはビルドされたstoreインスタンスに適当なActionをdispatch()してあげることで状態の更新を行うことができます.なお,Store#dispatch()rx.Observable<Action>を返すためsubscribe()が必要になることが多いでしょう.

store.dispatch(new AddTodoAction("new todo1")).subscribe();

基本はこんな感じです.複数Reducerを結合して1つのデカいStoreクラスを生成したいときは@Storeに対象のReducerクラスたちを渡してあげる,undo/redo機能が欲しければReducerに@Undoableアノテーションを付与するなど様々な拡張が可能となっています.

また,DataBindingさせたい場合はgetterに@Bindableアノテーションを付与してあげて,setReducer()に自動生成されたfield idを渡してあげることでいい感じにやってくれるようになります.

@Store(CounterReducer.class)
public interface CounterStore extends BaseStore {
    @Bindable Counter getCounter();
}

CounterStore store = DroiduxCounterStore.builder()
        .setReducer(new CounterReducer(), new Counter(0), BR.counter)
        .build();
<layout>
    <data>
        <variable android:name="store" android:type="CounterStore" />
    </data>
    <RelativeLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent" >

        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_centerInParent="true"
            android:text="@{store.counter}" />

    </RelativeLayout>
</layout>

また,非同期Actionに関してはdroidux-thunkという拡張を入れたうえでThunkMiddlewareを挿入してあげればいい感じにやってくれます.これも本家と似たような方式です.

こまかいところはREADMEや付属するサンプルアプリ等を見ていただけるといいでしょう.

ぼくがかんがえるりだっくすのいいところ

Droiduxの章でも書きましたが,個人的にはReduxの好きなところはこういうところです.

一方,Reduxは基本的にはSingle state/Single storeな感じで状態を管理していくので,とりあえずルートになってるStoreにaction投げとけ!みたいな雑な扱いができるので頭悪くてもとりあえずいい感じに使えそうです

だれが状態もってるのか? みたいなのを考えず,とりあえずrootのstoreにactionをdispatchしとけばいい!ってのはシンプルでいいですよね.MVPの状態管理部分だけReduxにしてあげるのとかも良いかもしれません.

ただ,JS世界には""ブラウザ”””という神が存在します.一方でAndroidの神といえば?ApplicationActivityなどのContextになるのですかね(techbooster.fm #10@laco0416がそんなこと言ってた).Reduxのstoreは1つの神1つ与えるぐらいがバランスがいいのかもしれません(メモリ的にも).この辺は実際に大きいアプリを実装してみないと何ともいえませんが….

Discussion

Droiduxpotatotipsで紹介したとき,「(undo/redo機能があるから)○○といった用途に良さそう!」みたいなリアクションをする方が何人かいらっしゃいました.本家Reduxでもある程度ウリにはしている(ってrebuild.fm #114で言ってた)のですが,Reduxの本質はそこではないような気がしています.ReduxをAndroidに持ち込んだものであるDroiduxでも同様です.

ReduxのThree Principlesを再度確認してみましょう.

- Three Principles

Droiduxでも多少なりとも形は変えつつですが.ある程度は踏襲しています.
では,これらの原則をAndroidに持ち込んだらどうなるでしょうか.
また,Redux以外のJS由来のアーキテクチャ・ライブラリ・思想をAndroidに持ち込んだとき,そこには広がるのはどんな世界になるでしょうか.


このへんの話はDroidKaigi 2016にproposalを出しています.もしrejectされても,別の何処かでお話出来るかもしれません.みんなでわいわい議論しましょう.

Conclusion

  • JSとAndroid・iOSは全く違う環境に見えて目的は近い
  • JS界の爆発的成長をAndroidに取り入れてみよう
  • 試しに2015年フロントエンドWebのトレンドなライブラリであるReduxをAndroid向けに実装してみたよ: Droidux
  • JSの知見を取り入れて,Androidアプリの設計に新たな風を
  • 続きはDroiduKaigi 2016で!

References

Documents

Libraries

Others