Share
Embed
[JA] (Unofficial) Guide to App Architecture Guide (Vol 1) - DroidKaigi 2022
Sa-ryong Kang
October 05, 2022
Transcript
46 slides
-
(Unofficial) Guide to App Architecture Guide カン・サリョン (Saryong Kang, justfaceit_kr@)
Developer Relations Engineer @ Google 1 English Version: https://speakerdeck.com/saryong/en-unofficial-g uide-to-app-architecture-guide-droidkaigi-2022 -
2 • Google • Android Policy / Compatibility Program Software
Engineer Jing Ji Saryong Kang Developer Relations Engineer Andoird Engineering所属 Device毎の機能の一貫性が主な仕事 (特にbg task) -
Clean Architecture is more like a way of thinking than
a design / architectural pattern. 9 -
Clean Architecture is not a pattern • クリーンアーキテクチャから得られる重要なインサイトは、 レイヤ毎に分けられた構造がモバイルアプリ設計にも役に立つ事 •
ただ、 ◦ レイヤの具体的なコンポーネントは定義されていない ◦ 前のページの図はその模範事例の一つにすぎない 10 -
Yes, Clean Architecture affected The Guide a lot • Layered
architecture: UI層 - Domain層 - Data層 • SOLID 原則, 特に単一責務の原則(Single Responsibility Principle) • だが、クリーンアーキテクチャの実現が目標ではない 12 -
なぜこういう概念はカバーしていない? • MVI (Flux, Redux), MVP, … ◦ Because they
are not important? No • 全てのパターン・アーキテクチャーを網羅するのが目的ではない ◦ 一般的なbest practice + 推奨事項 13 -
何故ドメイン層が重要なのか • UI層とデータ層の間の仲介がない時に生じる設計上の問題を解決できる ◦ ドメイン層を実装しない場合でも、考察が必要 • UI層に集中しすぎると、 ◦ 画面中心思考 →
画面の区別を超える対応が難しくなる ◦ Agile逆効果 → 画面の要素がそのままbacklog/featureになり、 classになってしまう • データ層に集中しすぎると、 ◦ データスキーマがドメイン/UI層まで影響 → 典型的な設計ミス 15 -
Challenge: as an Android Engineer, we may not have enough
design experiences. 16 -
Coffee Maker Mk IV Case Study • 一連のコーヒーメーカー (Mark Vもいつか発売さ
れるはず) • 保温プレートはポットを長時間暖める • 手順 a. コーヒー粉をフィルターに入れ、バスケットを 閉じる b. 水を水タンクに入れ、「抽出」ボタン押下 c. 加熱された水が蒸気化 d. 蒸気口が開けられ、コーヒー粉に噴射される e. 抽出されたコーヒーはフィルタを通過しポット に入れる 18 Source: https://cleancoders.com/episode/clean-code-episode-15 Image source: https://www.cafeappliances.com/ -
Coffee Maker Mk IV Case Study • 加熱器 (boiler; can
be turned on / off) • 保温プレート (Warmer plate; on / off) • 保温プレートセンサー (状態: warmerEmpty, potEmpty, potNotEmpty. • 加熱器センサー (状態: boilerEmpty, boilerNotEmpty) • 抽出ボタン + ランプ • 蒸気口 19 Source: https://cleancoders.com/episode/clean-code-episode-15 Image source: https://www.cafeappliances.com/ -
Think again: is that a screaming architecture? 21 • 手順
a. コーヒー粉をフィルターに入れ、バスケットを閉じる b. 水を水タンクに入れ、「抽出」ボタン押下 c. 加熱された水が蒸気化 d. 蒸気口が開けられ、コーヒー粉に噴射される e. 抽出されたコーヒーはフィルタを通過しポットに入れる -
Insight • データー及びエンティティ中心の思考ではいい設計が出来ない • 逆に、核心行為とactorを抽象化する形がもっと重要 • 上記の思想を仲介者の設計に適用すると、色んな形で設計ができる ◦ 1. DDDに近い設計
◦ 1-1. Use Caseを利用 ◦ 2. non-domain mediator layer 例: Gateway, Mapper, Data Controller, Translator, … ◦ 3. データ層で吸収: Repository 24 -
Repository pattern • よくある誤解: Repository in Android is anti-pattern!? ◦
It violates Single Responsibility Principle?! • Repositoryは、ドメイン層(又はUI層)から呼び出されるための抽象を提供 ◦ 他のレイヤに見せるためのinterface提供 ◦ データ格納に関する具体的な事項を隠してくれる 26 -
Data Source • データIOの実装を隠してくれる • Liskov置換原則 (LSP) ◦ 継承側で実装を切り替えても動作は変わらない ▪
REST ↔ gRPC ▪ Room ↔ other ORM ▪ Real 実装 ↔ Fake 27 -
データ層の考慮事項 • CRUD類の簡単な操作だけなら、 Repository / Data Source 両方が必要ないかも • Repositoryのライフサイクル
• Who should be the Single Source of Truth in your app? 28 -
What AAC ViewModel does for you • View Model in
Ancdroid Architecutre Components: 一般的な実装に役に立つ最小限の機能を提供 ◦ 安全な状態格納のための仕組み ▪ Safe from configuration change (by default) ▪ Safe from process kill (thru SavedStateHandler) ◦ Coroutine Scope (with some caveats) ◦ 依存性注入 via Dagger Hilt ◦ Helper for Jetpack Navigation 30 注意: Googleの公式な意見と関係ありません -
What AAC ViewModel doesn’t do for you • However, ◦
it’s completely your job to make VM VM-like ⇒ Adopting AAC VM doesn’t automatically mean you built a good MVVM architecture. ◦ it may not appropriate for some use cases ◦ sometimes implementation of VM seems too verbose 31 -
If you don’t like it, • You can build your
own! ◦ And AAC ViewModel source code will inspire you about how to implement Saved State Handler, its own Life Cycle, Coroutines Scope, Navigation, etc. 32 -
Another Downside of ViewModel: Verbosity • Circular event flow ◦
(1) Viewからイベント発生 ◦ (2) ViewModelが処理し、 Stateを変更 ◦ (3) View側で受け取り、 画面に描画 33 override fun onViewCreated(...) { // (1) binding.plusButton.setOnClickListener { _ -> viewModel.incrementCounter() } // (3) viewModel.counter.observe(viewLifeCycleOwner) { binding.plusButton.text = "Count: $it" } } class CounterViewModel { private val _counter = MutableStateFlow(0) val counter: StateFlow get() = _counter.asStateFlow() fun incrementCounter() { // (2) _counter.value += 1 // something aync... } } -
Solution • boiler plateを許容 • Data Binding! - can solve
this in elegant way 34 -
Consideration on Jetpack Compose • MVP doesn't make sense in
many cases ◦ View ↔ Presenterの相互作用がメソッド呼び出しで行われる ◦ 宣言型(declarative) Viewの場合は微妙 • ViewModel seems to make more sense, but.. does it? 35 -
Consideration on Jetpack Compose • Is ViewModel really necessary to
me? ◦ rememberSavable: AAC ViewModelが提供していたstateをView 側で定義可能 ◦ データレイヤのrepositoryが別途のlife cycleを持っている場合、 ▪ stateの格納をViewModelに委任する必要があるの? ▪ VMがなくてもよく動作する場面が多い (if ドメイン or データレイヤが十分なビジネスルールを実装してい る場合 + AAC VMの便利機能が要らない場合) 36 -
Consideration on Jetpack Compose • Be cautious about construction ◦
What is wrong with the code in the next slide? 37 -
38 @Composable fun MyComposable( viewModel: MyViewModel = hiltViewModel() ) {
Text(text = viewModel.myState) } class MyViewModel : ViewModel() { private val _myState = mutableStateOf("A") val myState: State = _myState init { viewModelScope.launch(Dispatchers.IO) { myState = "B" } } } -
Consideration on Jetpack Compose • Be cautious about construction ◦
1. バックグランドスケジューラで Composableのsnapshot stateを変更 → Crash! ◦ 2. 非同期処理 in constructor: coroutineScope.launch 中の非同期処理が終わる前にcontructorが 終了される可能性が高い → エラー時のデバッグが非常に難しい、テストコードが書きにくい ◦ 3. 単一責務の原則(SRP)違反: そもそもconstructorで簡単なinstantiation以外の処理をするのは望ま しくない 39 -
Consideration on Jetpack Compose • Be cautious about construction ◦
提案 ▪ Implement separated init() method ▪ Or, run initial job when view starts collection / subscription 40 -
41 private val _myState = MutableStateFlow("A") val myState: StateFlow =
_myState.asStateFlow() .onSubscription { // initial loading from local db... } .map { state -> // when _myState updated.. } .stateIn(viewModelScope, SharingStarted.WhileSubscribed(5_000), "") -
Consideration on Jetpack Compose • Be cautious about the scope
◦ Don’t add anything that may affect the snapshot ◦ Don’t store UI state (eg. animation) to VM 42 -
Consideration on Jetpack Compose • Be cautious about the scope
◦ ViewModelScope uses Dispatchers.Main.immediate ▪ instead, you can now use custom CoroutineScope https://developer.android.com/jetpack/androidx/release s/lifecycle#2.5.0 (addClosable(), 新しいconstructor of VM) 43 -
44 class CloseableCoroutineScope( context: CoroutineContext = SupervisorJob() + Dispatchers.Main )
: Closeable, CoroutineScope { override val coroutineContext: CoroutineContext = context override fun close() { coroutineContext.cancel() } } class MyViewModel( val customScope: CloseableCoroutineScope = CloseableCoroutineScope() ) : ViewModel(customScope) { // You can now use customScope in the same way as viewModelScope } -
Guide to App Architecture Guide Vol. 2 • 依存性注入 ◦
Dagger Hiltの長所、短所 ◦ 他の選択肢はないか • Multimodule ◦ Large scale modular architecture ◦ マルチモジュール化はいつした方がいい? ◦ 何故私のマルチモジュールはビルドが遅い? 45