Lock in $30 Savings on PRO—Offer Ends Soon! ⏳

拡大期を迎えたプロダクトに起きたこと - Android編

拡大期を迎えたプロダクトに起きたこと - Android編

私の携わるプロダクト「abceed」が拡大期を迎えたとき、人の増加に伴う設計変更の必要性に遭遇したときの話です。
なぜ再設計が必要だったのか、そしてなぜFluxを選択し、同実装したのかを解説します。

Masaya Yashiro

April 20, 2022
Tweet

More Decks by Masaya Yashiro

Other Decks in Technology

Transcript

  1. Who is me? • 本名: 屋代昌也 • 年齢: 36歳くらい •

    @ : @yashims85 (やしむす) • ❤ : スノボ、アニメ、バイク、お酒 • 2020年末頃にGlobeeに入社し、 Android開発をメインで担当
  2. なぜFlux? • iOS, Androidのエンジニアは相互に補助したり、 p-rを出したり しながら開発を行っている • そのため何かしらの共通化を行いたい要求がある • 制約性の高いアーキテクチャで設計を共通化させる。

    ◦ 制約度が高いと自然と似たようなコードになる ◦ それぞれのOS設計上で実装されるので、無理無くできる • 多人数大規模開発に向いている • 完全分業ならAndroid標準のMVVMにしたほうが無難な場合 も
  3. 実装 • Flux Store: reactivex.Observable • Presenter: FragmentとBLのつなぎ処理 • Flux

    Action: メッセージング用data class • Flux Dispatcher: kotlinx.coroutine • UseCase: Dispatcherから叩かれ、Storeを更新する処 理
  4. 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です。
  5. 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) } }
  6. 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にいい感じに送れるようにしています。
  7. 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)   } } 資料は公開する予定です ので、後ほどご確認くださ い!
  8. 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
  9. まとめ • プロジェクトや組織のフェーズによって、コードに求めら れる優先価値は変わってくる • プロダクトは組織や顧客の写し鏡 ◦ ある設計やその設計を元にしたコードには寿命が 存在する ◦

    課題が変化したタイミングが再設計の良いタイミン グ • Fluxの単方向Pub/Subは多人数開発ではとても具合が 良いので、同じような境遇の方はネタの一つに加えてみ てはいかがでしょうか?