Slide 1

Slide 1 text

(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

Slide 2

Slide 2 text

2 ● Google ● Android Policy / Compatibility Program Software Engineer Jing Ji Saryong Kang Developer Relations Engineer Andoird Engineering所属 Device毎の機能の一貫性が主な仕事 (特にbg task)

Slide 3

Slide 3 text

免責事項 ● この講義の内容はあくまで個人の意見です。 ● Googleの公式見解とは異なる可能性があります。 3

Slide 4

Slide 4 text

Agenda App Architecutre Guideが話さない事 ドメイン層 データ層 UI層 01 02 03 04 4

Slide 5

Slide 5 text

Agenda App Architecutre Guideが話さない事 ドメイン層 データ層 UI層 01 02 03 04 5

Slide 6

Slide 6 text

Clean Architecture ● モバイルアプリ設計にクリーンアーキテクチャを適用するための昔からの 議論 ○ SOLID原則 ○ レイヤ別のコンポーネント ● 例: 2015年のブログ記事 6

Slide 7

Slide 7 text

7 Source: https://qiita.com/koutalou/items/07a4f9cf51a2d13e4cdc 注意: Googleの公式な意見と関係ありません

Slide 8

Slide 8 text

So.. Is Clean Architecture a design pattern? 8

Slide 9

Slide 9 text

Clean Architecture is more like a way of thinking than a design / architectural pattern. 9

Slide 10

Slide 10 text

Clean Architecture is not a pattern ● クリーンアーキテクチャから得られる重要なインサイトは、 レイヤ毎に分けられた構造がモバイルアプリ設計にも役に立つ事 ● ただ、 ○ レイヤの具体的なコンポーネントは定義されていない ○ 前のページの図はその模範事例の一つにすぎない 10

Slide 11

Slide 11 text

11 Source (left): https://blog.cleancoder.com/uncle-bob/2012/08/13/the-clean-architecture.html Source (right): https://blog.cleancoder.com/uncle-bob/2012/08/13/the-clean-architecture.html

Slide 12

Slide 12 text

Yes, Clean Architecture affected The Guide a lot ● Layered architecture: UI層 - Domain層 - Data層 ● SOLID 原則, 特に単一責務の原則(Single Responsibility Principle) ● だが、クリーンアーキテクチャの実現が目標ではない 12

Slide 13

Slide 13 text

なぜこういう概念はカバーしていない? ● MVI (Flux, Redux), MVP, … ○ Because they are not important? No ● 全てのパターン・アーキテクチャーを網羅するのが目的ではない ○ 一般的なbest practice + 推奨事項 13

Slide 14

Slide 14 text

Agenda App Architecutre Guideが話さない事 ドメイン層 データ層 UI層 01 02 03 04 14

Slide 15

Slide 15 text

何故ドメイン層が重要なのか ● UI層とデータ層の間の仲介がない時に生じる設計上の問題を解決できる ○ ドメイン層を実装しない場合でも、考察が必要 ● UI層に集中しすぎると、 ○ 画面中心思考 → 画面の区別を超える対応が難しくなる ○ Agile逆効果 → 画面の要素がそのままbacklog/featureになり、 classになってしまう ● データ層に集中しすぎると、 ○ データスキーマがドメイン/UI層まで影響 → 典型的な設計ミス 15

Slide 16

Slide 16 text

Challenge: as an Android Engineer, we may not have enough design experiences. 16

Slide 17

Slide 17 text

Let me prove it. 17

Slide 18

Slide 18 text

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/

Slide 19

Slide 19 text

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/

Slide 20

Slide 20 text

Looks nice? 20 Source: https://cleancoders.com/episode/clean-code-episode-15

Slide 21

Slide 21 text

Think again: is that a screaming architecture? 21 ● 手順 a. コーヒー粉をフィルターに入れ、バスケットを閉じる b. 水を水タンクに入れ、「抽出」ボタン押下 c. 加熱された水が蒸気化 d. 蒸気口が開けられ、コーヒー粉に噴射される e. 抽出されたコーヒーはフィルタを通過しポットに入れる

Slide 22

Slide 22 text

Design example: Coffee Maker Mark IV 22 Source: https://cleancoders.com/episode/clean-code-episode-15

Slide 23

Slide 23 text

Design example: Coffee Maker Mark IV 23 Source: https://cleancoders.com/episode/clean-code-episode-15

Slide 24

Slide 24 text

Insight ● データー及びエンティティ中心の思考ではいい設計が出来ない ● 逆に、核心行為とactorを抽象化する形がもっと重要 ● 上記の思想を仲介者の設計に適用すると、色んな形で設計ができる ○ 1. DDDに近い設計 ○ 1-1. Use Caseを利用 ○ 2. non-domain mediator layer 例: Gateway, Mapper, Data Controller, Translator, … ○ 3. データ層で吸収: Repository 24

Slide 25

Slide 25 text

Agenda App Architecutre Guideが話さない事 ドメイン層 データ層 UI層 01 02 03 04 25

Slide 26

Slide 26 text

Repository pattern ● よくある誤解: Repository in Android is anti-pattern!? ○ It violates Single Responsibility Principle?! ● Repositoryは、ドメイン層(又はUI層)から呼び出されるための抽象を提供 ○ 他のレイヤに見せるためのinterface提供 ○ データ格納に関する具体的な事項を隠してくれる 26

Slide 27

Slide 27 text

Data Source ● データIOの実装を隠してくれる ● Liskov置換原則 (LSP) ○ 継承側で実装を切り替えても動作は変わらない ■ REST ↔ gRPC ■ Room ↔ other ORM ■ Real 実装 ↔ Fake 27

Slide 28

Slide 28 text

データ層の考慮事項 ● CRUD類の簡単な操作だけなら、 Repository / Data Source 両方が必要ないかも ● Repositoryのライフサイクル ● Who should be the Single Source of Truth in your app? 28

Slide 29

Slide 29 text

Agenda App Architecutre Guideが話さない事 ドメイン層 データ層 UI層 01 02 03 04 29

Slide 30

Slide 30 text

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の公式な意見と関係ありません

Slide 31

Slide 31 text

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

Slide 32

Slide 32 text

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

Slide 33

Slide 33 text

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

Slide 34

Slide 34 text

Solution ● boiler plateを許容 ● Data Binding! - can solve this in elegant way 34

Slide 35

Slide 35 text

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

Slide 36

Slide 36 text

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

Slide 37

Slide 37 text

Consideration on Jetpack Compose ● Be cautious about construction ○ What is wrong with the code in the next slide? 37

Slide 38

Slide 38 text

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

Slide 39

Slide 39 text

Consideration on Jetpack Compose ● Be cautious about construction ○ 1. バックグランドスケジューラで Composableのsnapshot stateを変更 → Crash! ○ 2. 非同期処理 in constructor: coroutineScope.launch 中の非同期処理が終わる前にcontructorが 終了される可能性が高い → エラー時のデバッグが非常に難しい、テストコードが書きにくい ○ 3. 単一責務の原則(SRP)違反: そもそもconstructorで簡単なinstantiation以外の処理をするのは望ま しくない 39

Slide 40

Slide 40 text

Consideration on Jetpack Compose ● Be cautious about construction ○ 提案 ■ Implement separated init() method ■ Or, run initial job when view starts collection / subscription 40

Slide 41

Slide 41 text

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), "")

Slide 42

Slide 42 text

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

Slide 43

Slide 43 text

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

Slide 44

Slide 44 text

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 }

Slide 45

Slide 45 text

Guide to App Architecture Guide Vol. 2 ● 依存性注入 ○ Dagger Hiltの長所、短所 ○ 他の選択肢はないか ● Multimodule ○ Large scale modular architecture ○ マルチモジュール化はいつした方がいい? ○ 何故私のマルチモジュールはビルドが遅い? 45

Slide 46

Slide 46 text

ご清聴ありがとうございました! 46