$30 off During Our Annual Pro Sale. View Details »

[JA] (Unofficial) Guide to App Architecture Guide (Vol 1) - DroidKaigi 2022

[JA] (Unofficial) Guide to App Architecture Guide (Vol 1) - DroidKaigi 2022

Sa-ryong Kang

October 05, 2022
Tweet

More Decks by Sa-ryong Kang

Other Decks in Technology

Transcript

  1. (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. 2 • Google • Android Policy / Compatibility Program Software

    Engineer Jing Ji Saryong Kang Developer Relations Engineer Andoird Engineering所属 Device毎の機能の一貫性が主な仕事 (特にbg task)
  3. 免責事項 • この講義の内容はあくまで個人の意見です。 • Googleの公式見解とは異なる可能性があります。 3

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

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

    04 5
  6. Clean Architecture • モバイルアプリ設計にクリーンアーキテクチャを適用するための昔からの 議論 ◦ SOLID原則 ◦ レイヤ別のコンポーネント •

    例: 2015年のブログ記事 6
  7. 7 Source: https://qiita.com/koutalou/items/07a4f9cf51a2d13e4cdc 注意: Googleの公式な意見と関係ありません

  8. So.. Is Clean Architecture a design pattern? 8

  9. Clean Architecture is more like a way of thinking than

    a design / architectural pattern. 9
  10. Clean Architecture is not a pattern • クリーンアーキテクチャから得られる重要なインサイトは、 レイヤ毎に分けられた構造がモバイルアプリ設計にも役に立つ事 •

    ただ、 ◦ レイヤの具体的なコンポーネントは定義されていない ◦ 前のページの図はその模範事例の一つにすぎない 10
  11. 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

  12. Yes, Clean Architecture affected The Guide a lot • Layered

    architecture: UI層 - Domain層 - Data層 • SOLID 原則, 特に単一責務の原則(Single Responsibility Principle) • だが、クリーンアーキテクチャの実現が目標ではない 12
  13. なぜこういう概念はカバーしていない? • MVI (Flux, Redux), MVP, … ◦ Because they

    are not important? No • 全てのパターン・アーキテクチャーを網羅するのが目的ではない ◦ 一般的なbest practice + 推奨事項 13
  14. Agenda App Architecutre Guideが話さない事 ドメイン層 データ層 UI層 01 02 03

    04 14
  15. 何故ドメイン層が重要なのか • UI層とデータ層の間の仲介がない時に生じる設計上の問題を解決できる ◦ ドメイン層を実装しない場合でも、考察が必要 • UI層に集中しすぎると、 ◦ 画面中心思考 →

    画面の区別を超える対応が難しくなる ◦ Agile逆効果 → 画面の要素がそのままbacklog/featureになり、 classになってしまう • データ層に集中しすぎると、 ◦ データスキーマがドメイン/UI層まで影響 → 典型的な設計ミス 15
  16. Challenge: as an Android Engineer, we may not have enough

    design experiences. 16
  17. Let me prove it. 17

  18. 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/
  19. 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/
  20. Looks nice? 20 Source: https://cleancoders.com/episode/clean-code-episode-15

  21. Think again: is that a screaming architecture? 21 • 手順

    a. コーヒー粉をフィルターに入れ、バスケットを閉じる b. 水を水タンクに入れ、「抽出」ボタン押下 c. 加熱された水が蒸気化 d. 蒸気口が開けられ、コーヒー粉に噴射される e. 抽出されたコーヒーはフィルタを通過しポットに入れる
  22. Design example: Coffee Maker Mark IV 22 Source: https://cleancoders.com/episode/clean-code-episode-15

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

  24. Insight • データー及びエンティティ中心の思考ではいい設計が出来ない • 逆に、核心行為とactorを抽象化する形がもっと重要 • 上記の思想を仲介者の設計に適用すると、色んな形で設計ができる ◦ 1. DDDに近い設計

    ◦ 1-1. Use Caseを利用 ◦ 2. non-domain mediator layer 例: Gateway, Mapper, Data Controller, Translator, … ◦ 3. データ層で吸収: Repository 24
  25. Agenda App Architecutre Guideが話さない事 ドメイン層 データ層 UI層 01 02 03

    04 25
  26. Repository pattern • よくある誤解: Repository in Android is anti-pattern!? ◦

    It violates Single Responsibility Principle?! • Repositoryは、ドメイン層(又はUI層)から呼び出されるための抽象を提供 ◦ 他のレイヤに見せるためのinterface提供 ◦ データ格納に関する具体的な事項を隠してくれる 26
  27. Data Source • データIOの実装を隠してくれる • Liskov置換原則 (LSP) ◦ 継承側で実装を切り替えても動作は変わらない ▪

    REST ↔ gRPC ▪ Room ↔ other ORM ▪ Real 実装 ↔ Fake 27
  28. データ層の考慮事項 • CRUD類の簡単な操作だけなら、 Repository / Data Source 両方が必要ないかも • Repositoryのライフサイクル

    • Who should be the Single Source of Truth in your app? 28
  29. Agenda App Architecutre Guideが話さない事 ドメイン層 データ層 UI層 01 02 03

    04 29
  30. 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の公式な意見と関係ありません
  31. 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
  32. 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
  33. 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... } }
  34. Solution • boiler plateを許容 • Data Binding! - can solve

    this in elegant way 34
  35. 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
  36. 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
  37. Consideration on Jetpack Compose • Be cautious about construction ◦

    What is wrong with the code in the next slide? 37
  38. 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" } } }
  39. Consideration on Jetpack Compose • Be cautious about construction ◦

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

    提案 ▪ Implement separated init() method ▪ Or, run initial job when view starts collection / subscription 40
  41. 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), "")
  42. 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
  43. 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. 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 }
  45. Guide to App Architecture Guide Vol. 2 • 依存性注入 ◦

    Dagger Hiltの長所、短所 ◦ 他の選択肢はないか • Multimodule ◦ Large scale modular architecture ◦ マルチモジュール化はいつした方がいい? ◦ 何故私のマルチモジュールはビルドが遅い? 45
  46. ご清聴ありがとうございました! 46