Androidにおける状態管理をスマートに実装するためにFluxを採用した話

こんにちは。Pairs Global事業部の@yuyakaidoです。


Eureka Native Advent Calendar 2017で2度目の登場です。
今回は前回で軽く紹介したPairsのグローバル版におけるFluxの詳細を解説したいと思います。

Flux採用の目的

前回の記事でFlux採用の目的を軽く紹介しましたが、改めて、Pairsは状態変更とそれによる挙動変化が多いサービスであり、これをスムーズに解決することを目的としてFluxを採用しました。

Fluxの紹介

FluxはFacebookがObserverパターンに改めて名前をつけて発表したもので、ライブラリやフレームワークではなく、ただの実装パターンです。オリジナルは以下のリポジトリで公開されています。

Fluxが重要視しているのは、データフローを単方向に制限することであり、これはGUIアプリケーションにおける状態変更とそれによる挙動変化を扱うのに適しています。まさにPairsのようなサービスに向いていると言えます。

Fluxは大きく分けて以下の4つのレイヤーから構成されています。

  • React Views
  • GUIアプリケーションにおける画面を構成するレイヤー
  • Androidでいう、Activity/Fragmentに相当する
  • Action Creators
  • APIコールなどの非同期処理を扱うためのレイヤー
  • Androidアプリでは必ずといっていいほど非同期処理が必要です
  • Dispatcher
  • 状態変更(Action)をStoreに伝えるためのハブとなるレイヤー
  • Storeの状態変更は必ずこのDispatcherを経由します
  • Store
  • 状態保持とその変更を伝播させるためのレイヤー
  • Storeがあるおかげで「変更通知とそれによる挙動変更」が実現出来ます

Fluxの実装

上記でFluxの概要をお伝えしましたが、Facebookが提唱しているのはあくまで考え方であり、どのように実装するのかは利用者に一任されています。一応サンプルコードがリポジトリに含まれており、弊社ではそのサンプルを参考にして、Android向けの実装に落とし込んでいます。


前回の記事で紹介したアーキテクチャの全体像をもとに実装をコードレベルで紹介していきます。

ActionCreator

ActionCreatorの役割はAPIの呼び出しなどの非同期処理を行い、その処理結果をActionとしてDispatcherに渡すことです。

class ApproachActionCreator @Inject constructor(
private val dispatcher: Dispatcher,
private val userRepository: UserRepository) : ActionCreator {
fun sendLike(to: User): Disposable {
return userRepository.like(to)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
}
.doOnSuccess { dispatcher.dispatch(ApproachAction.SendLike(it.user)) }
.execute()
}
}
擬似コードを含んでいますが、実際にPairsにていいね!を送信する部分のコードを抜粋しています。UserRepositoryクラスを使ってAPIの呼び出しを行い、いいね!送信が完了したことをActionとしてDispatcherに渡しています。
Dispatcher
Dispatcherの役割はActionを受け取り、StoreがActionを受け取るためのインターフェース(onメソッド)を提供することです。
class Dispatcher {
private val processor = PublishProcessor.create<Action>().toSerialized()
fun dispatch(action: Action) {
processor.onNext(action)
}
fun <T : Action> on(clazz: Class<T>): Observable<T> {
return processor.onBackpressureBuffer()
.ofType(clazz)
.observeOn(AndroidSchedulers.mainThread())
.toObservable()
}
}
内部にPublishProcessorを持っており、一度全てのActionをこのProcessorが受け取り、onメソッドを経由してStoreに伝わります。


ちなみに、onBackpressureBufferメソッドを呼び出しているのはActionの取りこぼしを防ぐためで、observeOnメソッドでメインスレッドを指定しているのは変更通知を購読しているのがViewであり、Viewの変更はAndroid OSの都合で必ずメインスレッドで行う必要があるためです。
Store
Storeの役割は状態保持とその変更通知を行うことです。Storeは外部にObservableとして変更通知のためのインターフェースを公開し、View側でこのObservableを購読しておくことで、変更検知とそれによる挙動変更が実現出来ます。
class FooStore @Inject constructor(private val dispatcher: Dispatcher) {
val refresh = dispatcher.on(FooAction.Refresh::class.java)
val progress = dispatcher.on(FooAction.RefreshProgress::class.java)
.scan(ProgressStatus.Invisible, { _, action -> action.status })
}
Storeが保持するものは大きく分けて以下の2つがあります。
  • 画面更新や画面遷移といった一時的な通知
  • 上記のrefreshに相当し、揮発性なため状態として保持する必要はない
  • ローディングの表示ステータスといった状態
  • 上記のprogressに相当し、基本的には不揮発性なため、常に最新の状態を保持して再度購読された場合には即座に最新の値を返す必要がある(scanでこれを実現しています)
ViewModel
ViewModelの役割はStoreが公開するプロパティをViewが使いやすい形に整形することです。
class FooViewModel @Inject constructor(private val fooStore: FooStore) {
val refresh = fooStore.refresh()
val progress = fooStore.progress().map { it.visibility }
}
上記サンプルではProgressStatusをViewのVisibilityに変換しています。あくまでStoreはアプリの状態を抽象的に表現し、ViewModelにて実際のViewに適した形に変換する、というスタンスで実装しています。
まとめ
今回はPairsのグローバル版で採用しているFlux実装について紹介しました。
  • Flux採用の目的
  • 状態変更とそれによるアプリ全体の挙動変更をスムーズに実装するため
  • Fluxの実装
  • ActionCreator:非同期処理を実行し、その結果をActionとして流す
  • Dispatcher:Actionを受け取り、それをStoreに流す
  • Store:Actionを受け取り、自身の状態を変更した上でそれを外部に通知する
  • ViewModel:Storeが公開しているプロパティをViewが使いやすい形に整形する
Fluxを採用したことは当初の目的を達成できていることから、それなりに成功だったのではないか、という感触です。しかし、開発スケジュールや当初の想定不足により、うまくいかなかったポイントもあります。
  • ActionCreatorが内部でActionをDispatcherに渡してしまっており、テスタビリティが低い
  • ActionCreatorがObservableを返すように変更することでテスト可能にすべきだった
  • Storeが自身の状態を変更する処理を内部に持ってしまっている
  • ReduxにおけるReducerのようなもの定義し、状態変更処理に対するテストコードを書けるようにすべきだった
さて、次回は少し趣向を変えて、Pairsのグローバル版にて実装されている様々なデバッグ機能と、その実現方法についてお伝えしたいと思います。
Like what you read? Give eureka_developers a round of applause.

From a quick cheer to a standing ovation, clap to show how much you enjoyed this story.