Share
Embed
拡大期を迎えたプロダクトに起きたこと - Android編
私の携わるプロダクト「abceed」が拡大期を迎えたとき、人の増加に伴う設計変更の必要性に遭遇したときの話です。
なぜ再設計が必要だったのか、そしてなぜFluxを選択し、同実装したのかを解説します。
Masaya Yashiro
April 20, 2022
Transcript
39 slides
-
Who is me? • 本名: 屋代昌也 • 年齢: 36歳くらい •
@ : @yashims85 (やしむす) • ❤ : スノボ、アニメ、バイク、お酒 • 2020年末頃にGlobeeに入社し、 Android開発をメインで担当 -
Flux • ソフトウェアアーキテクチャの一つ • MVCやMVVM、Reduxなどと並行概念 • Redux、Flutter等はFluxの実装系 View Dispatcher Store
Action send commit observe create -
Flux • コードの制約度が高い • 大規模なイベント駆動型ソフトウェア向け • ボイラープレートが多くなりがち View Dispatcher Store
Action send commit observe create 単方向Pub/Sub 参照の向き 呼び出しの順番 -
なぜFlux? • iOS, Androidのエンジニアは相互に補助したり、 p-rを出したり しながら開発を行っている • そのため何かしらの共通化を行いたい要求がある • 制約性の高いアーキテクチャで設計を共通化させる。
◦ 制約度が高いと自然と似たようなコードになる ◦ それぞれのOS設計上で実装されるので、無理無くできる • 多人数大規模開発に向いている • 完全分業ならAndroid標準のMVVMにしたほうが無難な場合 も -
AndroidでFlux • FluxのViewはFragmentに相当 • Dispatcherは単体だと都合が悪いので、Dispatcherと ビジネスロジックのUseCaseを分離 Fragment (Presenter) Dispatcher UseCase
Store Action send commit observe call create -
Presenter AndroidでFlux • FragmentがもりもりになるのでPresenterに分離 Dispatcher UseCase Store Action send commit
observe call create Fragment call push ViewModel observe -
Presenter AndroidでFlux • プレゼンテーション層とビジネスロジック層の分離ができた • ビジネスロジックはプレゼンテーションへの参照を持たない Dispatcher UseCase Store Action
send commit observe call create Fragment call push ViewModel observe プレゼンテーション ビジネスロジック -
実装 • Flux Store: reactivex.Observable • Presenter: FragmentとBLのつなぎ処理 • Flux
Action: メッセージング用data class • Flux Dispatcher: kotlinx.coroutine • UseCase: Dispatcherから叩かれ、Storeを更新する処 理 -
Presenter AndroidでFluxのStore Dispatcher UseCase Store Action send commit observe call
create Fragment call push ViewModel observe -
Flux Store class UserDetailStore { val error = RxProperty<Exception>() val
userInfo: RxProperty<UserInfo> = RxProperty() fun commitError(exception: Exception) { this.error.value = exception } fun commitUserInfo (info: UserInfo) { this.userInfo.value = tabList } } AndroidのLiveDataと同じようなものと思っ て問題ありません。 実装の実態はRx.BehaviourSubjectです。 -
Presenter AndroidでFluのView Dispatcher UseCase Store Action send commit observe call
create Fragment call push ViewModel observe -
Flux View (Presenter) class UserDetailPresenter( private val store: UserDetailStore ,
private val dispatcher: FluxDispatcher , ) { private val disposables = CompositeDisposable() private var vm: UserDetailViewModel? = null // onViewCreated 時などに呼ばれる fun initialize(vm: UserDetailVideoViewModel) { this.vm = vm store.error.asObservable() .subscribe { e: Exception -> vm.postError(e) } .addTo(disposables) store.userInfo.asObservable() .subscribe { info: UserInfo -> vm.postUserName(info. name) } .addTo(disposables) } // onStart 時などに呼ばれる fun showUserDetail (userId: String) { PrepareUserDetailAction( userId = userId).sendTo( dispatcher) } } -
Presenter AndroidでFluxのAction Dispatcher UseCase Store Action send commit observe call
create Fragment call push ViewModel observe -
Flux Action interface FluxAction { fun uid(): String = hashCode().toString()
fun sendTo(dispatcher: FluxDispatcher) = dispatcher.dispatch( this) } data class PrepareUserDetailAction( val userId: String ) : FluxAction FluxAction インターフェースを継承することで、後述の FluxDispatcherにいい感じに送れるようにしています。 -
Presenter AndroidでFluxのDispatcher Dispatcher UseCase Store Action send commit observe call
create Fragment call push ViewModel observe -
Flux Dispatcher class FluxDispatcher( private val coroutineCxt : CoroutineContext
= CoroutineName( "FluxDispatcher" ) + Dispatchers. Default ) { private val observerList : MutableList<ActionObserver<FluxAction>> = mutableListOf() @Suppress ("UNCHECKED_CAST" ) fun <T : FluxAction> register ( klass: KClass< T>, cb: suspend (action: T) -> Unit ): ActionObserver< T> = ActionObserver( klass = klass, cb = cb ).also { runBlocking(coroutineCxt ) { observerList .add(it as ActionObserver<FluxAction>) } } @Suppress ("UNCHECKED_CAST" ) fun <T : FluxAction> registerOnce ( klass: KClass< T>, cb: suspend (action: T) -> Unit ): ActionObserver< T> = ActionObserver( klass = klass, cb = cb, dispatchCount = 1 ).also { runBlocking(coroutineCxt ) { observerList .add(it as ActionObserver<FluxAction>) } } fun unRegister (observer: ActionObserver< out FluxAction>) { runBlocking(coroutineCxt ) { observerList .remove(observer) } } fun <T : FluxAction> dispatch (action: T) { runBlocking(coroutineCxt ) { val execObserverList = observerList .filter { it.isMyAction(action) } Log.d( "Flux" , "Dispatch action: ${action:: class.simpleName }#${action.uid() } for ${execObserverList. size} observers." ) execObserverList. forEach { observer: ActionObserver<FluxAction> -> CoroutineScope(Dispatchers. Default ).launch { observer.observe(action) withContext( coroutineContext ) { if (observer. dispatchCount > 0) { observer. dispatchCount -- if (observer. dispatchCount == 0) { unRegister(observer) } } } } } } } companion object { private const val DISPATCH_UNLIMITED : Int = - 1 } data class ActionObserver< T : FluxAction>( val klass: KClass< T>, val cb: suspend (action: T) -> Unit , var dispatchCount : Int = DISPATCH_UNLIMITED ) { fun isMyAction (action: T): Boolean = klass.isSuperclassOf(action:: class) suspend fun observe (action: T) = cb.invoke(action) } } 資料は公開する予定です ので、後ほどご確認くださ い! -
UseCase // data class PrepareUserDetailAction ( // val userId: String
// ) : FluxAction class UserDetailUseCase( private val dispatcher: FluxDispatcher , private val userRep: UserRepository , private val store: UserDetailStore , ) { private val disposables = CompositeDisposable() init { dispatcher.register(PrepareUserDetailAction:: class) { prepareUserDetail( it.userId) } } private fun prepareUserDetail (userId: String) { userRep.readUserInfo(config) .subscribe({ userInfo: UserInfo -> store.commitUserInfo(userInfo) }, {e: Throwable -> store.commitError(e as Exception) }) .addTo(disposables) } } Dispatcherに登録。 callbackは、バックエン ドスレッド内で実行され てる NWエラー等が発生し てもエラーの正常系と して扱える! バックエンドからデータ を取得しStoreに commit -
Fluxイベントサイクル プレゼンテーションイベントサイクル Presenter AndroidでFlux プレゼンテーションイベントサイクルと Fluxのイベントサイクルが分離さ れ、Presenterで接続されている Dispatcher UseCase Store
Action send commit observe call create Fragment call push ViewModel observe 参照の向き 呼び出しの順番 赤字 非同期 -
まとめ • プロジェクトや組織のフェーズによって、コードに求めら れる優先価値は変わってくる • プロダクトは組織や顧客の写し鏡 ◦ ある設計やその設計を元にしたコードには寿命が 存在する ◦
課題が変化したタイミングが再設計の良いタイミン グ • Fluxの単方向Pub/Subは多人数開発ではとても具合が 良いので、同じような境遇の方はネタの一つに加えてみ てはいかがでしょうか?