Upgrade to Pro
— share decks privately, control downloads, hide ads and more …
Speaker Deck
Features
Speaker Deck
PRO
Sign in
Sign up for free
Search
Search
拡大期を迎えたプロダクトに起きたこと - Android編
Search
Masaya Yashiro
April 20, 2022
Technology
0
460
拡大期を迎えたプロダクトに起きたこと - Android編
私の携わるプロダクト「abceed」が拡大期を迎えたとき、人の増加に伴う設計変更の必要性に遭遇したときの話です。
なぜ再設計が必要だったのか、そしてなぜFluxを選択し、同実装したのかを解説します。
Masaya Yashiro
April 20, 2022
Tweet
Share
More Decks by Masaya Yashiro
See All by Masaya Yashiro
How useful Kotlin/Native in Kotlin 1.3
yashims
0
370
UX design trend 2019
yashims
5
1.5k
Kotlin/MPP getting started and troubles
yashims
0
3.7k
C# code refactoring with Scope Functions
yashims
0
2.7k
ココがダメだよWebCamTexture
yashims
0
75
Introduction of MaterialDesign for engineer
yashims
0
91
Other Decks in Technology
See All in Technology
開発生産性をむしろ向上させる セキュリティパートナーの作り方 / Dev Productivity Con 2024
flatt_security
0
360
ゆめみのアクセシビリティの現在地と今後
ryokatsuse
3
290
目標設定は好きですか? アジャイルとともに目標と向き合い続ける方法 / Do you like target Management?
kakehashi
10
3k
AIアシスタントの活用で品質の向上と開発ワークフローのスピードアップ
nagix
1
200
簡単に始めるSnowflakeの機械学習
nayuts
1
190
Matterport を使ってクラスメソッド各拠点のバーチャルオフィスツアーを作成してみた
wakatsuki
0
160
楽しくGoを学び合う、LayerXの勉強会文化 / LayerX's study culture of having fun and learning Go together
ar_tama
2
350
E2Eテスト自動化プラットフォームにおけるAIの活用
shift_evolve
0
180
サーバーレスAPI(API Gateway+Lambda)とNext.jsで 個人ブログを作ろう!
shuntaka
PRO
0
560
Flutter研修【MIXI 24新卒技術研修】
mixi_engineers
PRO
0
160
公共領域から学ぶ クラウド移行についてエンジニアが意識していること
kawakawa2222
0
140
LLMアプリケーションの評価の実践と課題 ~PharmaXにおける今後の展望~
pharma_x_tech
2
160
Featured
See All Featured
Typedesign – Prime Four
hannesfritz
37
2.2k
Refactoring Trust on Your Teams (GOTO; Chicago 2020)
rmw
29
2.5k
A Tale of Four Properties
chriscoyier
155
22k
KATA
mclloyd
20
13k
How STYLIGHT went responsive
nonsquared
93
5k
Responsive Adventures: Dirty Tricks From The Dark Corners of Front-End
smashingmag
248
20k
Dealing with People You Can't Stand - Big Design 2015
cassininazir
360
22k
What's in a price? How to price your products and services
michaelherold
239
11k
Happy Clients
brianwarren
94
6.5k
How to train your dragon (web standard)
notwaldorf
79
5.5k
How to Think Like a Performance Engineer
csswizardry
4
590
Fantastic passwords and where to find them - at NoRuKo
philnash
42
2.7k
Transcript
拡大期を迎えたプロダクトに 起きたこと - Android編 2022/04/20 @大人気教育アプリ 3社 のモバイルアプリ開発秘話 @yashims85
皆様はどのようなチームで開発を 行っていますか?
私がGlobeeに入社したのは abceedの売上が安定し、規模が 拡大していく最中のことです。
拡大期を迎えたことで、様々な事情 から、再設計を行う必要が出てきた タイミングでした。
Who is me? • 本名: 屋代昌也 • 年齢: 36歳くらい •
@ : @yashims85 (やしむす) • ❤ : スノボ、アニメ、バイク、お酒 • 2020年末頃にGlobeeに入社し、 Android開発をメインで担当
None
何を作ってるの? どうやって発展してきたの?
作っているもの
abceedとは クイズ、シャドーイング、スラッシュリーディングなど 約20種類の豊富なアプリ学習に対応
累計ユーザー数 220万人突破 おかげさまで
なぜ設計を変える必要が出てきた か
なぜ設計を変える必要があったか • つくった人がずっとコードを追っているので、未知のコー ドが存在しなかった • 可読性、メンテのしやすさよりも、最短でリリースし収益 化することが優先された • ミニマムから始め機能を追加していったために、スー パーなんでもクラスが生まれていた
あとから来た人が沼にハマりやすい 環境になっていた。
再設計をするときの注意点
設計を変えるという事が 今までの否定になってはいけない • 今あるものは作ったときのリソースの制約や、環境のも とで全力で作った結果 • その当時の設計で拡張できる限界。設計寿命に達した • だから我々は再設計を行う
既存コードが悪いわけではなく、プロ ダクトに必要とされる価値や優先順 位が変化してきた
実際に変えた設計の一例をご紹介
Fluxアーキテクチャの採用
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は設計概念なので、実装は無数に存在する。 今回は独自で実装したので、それのご紹介です。
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は多人数開発ではとても具合が 良いので、同じような境遇の方はネタの一つに加えてみ てはいかがでしょうか?
Eigo wakaran!! Globeeでは一緒に働いていただける エンジニア募集中です! (英語ができなくても大丈夫です!)
おわり