Slide 1

Slide 1 text

拡大期を迎えたプロダクトに 起きたこと - Android編 2022/04/20 @大人気教育アプリ 3社 のモバイルアプリ開発秘話 @yashims85

Slide 2

Slide 2 text

皆様はどのようなチームで開発を 行っていますか?

Slide 3

Slide 3 text

私がGlobeeに入社したのは abceedの売上が安定し、規模が 拡大していく最中のことです。

Slide 4

Slide 4 text

拡大期を迎えたことで、様々な事情 から、再設計を行う必要が出てきた タイミングでした。

Slide 5

Slide 5 text

Who is me? ● 本名: 屋代昌也 ● 年齢: 36歳くらい ● @ : @yashims85 (やしむす) ● ❤ : スノボ、アニメ、バイク、お酒 ● 2020年末頃にGlobeeに入社し、 Android開発をメインで担当

Slide 6

Slide 6 text

No content

Slide 7

Slide 7 text

何を作ってるの? どうやって発展してきたの?

Slide 8

Slide 8 text

作っているもの

Slide 9

Slide 9 text

abceedとは クイズ、シャドーイング、スラッシュリーディングなど 約20種類の豊富なアプリ学習に対応

Slide 10

Slide 10 text

累計ユーザー数 220万人突破 おかげさまで

Slide 11

Slide 11 text

なぜ設計を変える必要が出てきた か

Slide 12

Slide 12 text

なぜ設計を変える必要があったか ● つくった人がずっとコードを追っているので、未知のコー ドが存在しなかった ● 可読性、メンテのしやすさよりも、最短でリリースし収益 化することが優先された ● ミニマムから始め機能を追加していったために、スー パーなんでもクラスが生まれていた

Slide 13

Slide 13 text

あとから来た人が沼にハマりやすい 環境になっていた。

Slide 14

Slide 14 text

再設計をするときの注意点

Slide 15

Slide 15 text

設計を変えるという事が 今までの否定になってはいけない ● 今あるものは作ったときのリソースの制約や、環境のも とで全力で作った結果 ● その当時の設計で拡張できる限界。設計寿命に達した ● だから我々は再設計を行う

Slide 16

Slide 16 text

既存コードが悪いわけではなく、プロ ダクトに必要とされる価値や優先順 位が変化してきた

Slide 17

Slide 17 text

実際に変えた設計の一例をご紹介

Slide 18

Slide 18 text

Fluxアーキテクチャの採用

Slide 19

Slide 19 text

Flux ● ソフトウェアアーキテクチャの一つ ● MVCやMVVM、Reduxなどと並行概念 ● Redux、Flutter等はFluxの実装系 View Dispatcher Store Action send commit observe create

Slide 20

Slide 20 text

Flux ● コードの制約度が高い ● 大規模なイベント駆動型ソフトウェア向け ● ボイラープレートが多くなりがち View Dispatcher Store Action send commit observe create 単方向Pub/Sub 参照の向き 呼び出しの順番

Slide 21

Slide 21 text

なぜFlux? ● iOS, Androidのエンジニアは相互に補助したり、 p-rを出したり しながら開発を行っている ● そのため何かしらの共通化を行いたい要求がある ● 制約性の高いアーキテクチャで設計を共通化させる。 ○ 制約度が高いと自然と似たようなコードになる ○ それぞれのOS設計上で実装されるので、無理無くできる ● 多人数大規模開発に向いている ● 完全分業ならAndroid標準のMVVMにしたほうが無難な場合 も

Slide 22

Slide 22 text

AndroidでFlux Fluxは設計概念なので、実装は無数に存在する。 今回は独自で実装したので、それのご紹介です。

Slide 23

Slide 23 text

AndroidでFlux ● FluxのViewはFragmentに相当 ● Dispatcherは単体だと都合が悪いので、Dispatcherと ビジネスロジックのUseCaseを分離 Fragment (Presenter) Dispatcher UseCase Store Action send commit observe call create

Slide 24

Slide 24 text

Presenter AndroidでFlux ● FragmentがもりもりになるのでPresenterに分離 Dispatcher UseCase Store Action send commit observe call create Fragment call push ViewModel observe

Slide 25

Slide 25 text

Presenter AndroidでFlux ● プレゼンテーション層とビジネスロジック層の分離ができた ● ビジネスロジックはプレゼンテーションへの参照を持たない Dispatcher UseCase Store Action send commit observe call create Fragment call push ViewModel observe プレゼンテーション ビジネスロジック

Slide 26

Slide 26 text

実装 ● Flux Store: reactivex.Observable ● Presenter: FragmentとBLのつなぎ処理 ● Flux Action: メッセージング用data class ● Flux Dispatcher: kotlinx.coroutine ● UseCase: Dispatcherから叩かれ、Storeを更新する処 理

Slide 27

Slide 27 text

Presenter AndroidでFluxのStore Dispatcher UseCase Store Action send commit observe call create Fragment call push ViewModel observe

Slide 28

Slide 28 text

Flux Store class UserDetailStore { val error = RxProperty() val userInfo: RxProperty = RxProperty() fun commitError(exception: Exception) { this.error.value = exception } fun commitUserInfo (info: UserInfo) { this.userInfo.value = tabList } } AndroidのLiveDataと同じようなものと思っ て問題ありません。 実装の実態はRx.BehaviourSubjectです。

Slide 29

Slide 29 text

Presenter AndroidでFluのView Dispatcher UseCase Store Action send commit observe call create Fragment call push ViewModel observe

Slide 30

Slide 30 text

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) } }

Slide 31

Slide 31 text

Presenter AndroidでFluxのAction Dispatcher UseCase Store Action send commit observe call create Fragment call push ViewModel observe

Slide 32

Slide 32 text

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にいい感じに送れるようにしています。

Slide 33

Slide 33 text

Presenter AndroidでFluxのDispatcher Dispatcher UseCase Store Action send commit observe call create Fragment call push ViewModel observe

Slide 34

Slide 34 text

Flux Dispatcher class FluxDispatcher(   private val coroutineCxt : CoroutineContext = CoroutineName( "FluxDispatcher" ) + Dispatchers. Default ) {   private val observerList : MutableList> = mutableListOf()  @Suppress ("UNCHECKED_CAST" )   fun register (   klass: KClass< T>,   cb: suspend (action: T) -> Unit   ): ActionObserver< T> = ActionObserver(   klass = klass,   cb = cb   ).also {   runBlocking(coroutineCxt ) {   observerList .add(it as ActionObserver)   }  }   @Suppress ("UNCHECKED_CAST" )   fun 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)   }   }   fun unRegister (observer: ActionObserver< out FluxAction>) {   runBlocking(coroutineCxt ) {   observerList .remove(observer)   }  }   fun 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 ->   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)   } } 資料は公開する予定です ので、後ほどご確認くださ い!

Slide 35

Slide 35 text

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

Slide 36

Slide 36 text

Fluxイベントサイクル プレゼンテーションイベントサイクル Presenter AndroidでFlux プレゼンテーションイベントサイクルと Fluxのイベントサイクルが分離さ れ、Presenterで接続されている Dispatcher UseCase Store Action send commit observe call create Fragment call push ViewModel observe 参照の向き 呼び出しの順番 赤字 非同期

Slide 37

Slide 37 text

まとめ ● プロジェクトや組織のフェーズによって、コードに求めら れる優先価値は変わってくる ● プロダクトは組織や顧客の写し鏡 ○ ある設計やその設計を元にしたコードには寿命が 存在する ○ 課題が変化したタイミングが再設計の良いタイミン グ ● Fluxの単方向Pub/Subは多人数開発ではとても具合が 良いので、同じような境遇の方はネタの一つに加えてみ てはいかがでしょうか?

Slide 38

Slide 38 text

Eigo wakaran!! Globeeでは一緒に働いていただける エンジニア募集中です! (英語ができなくても大丈夫です!)

Slide 39

Slide 39 text

おわり