Lock in $30 Savings on PRO—Offer Ends Soon! ⏳

[JA] (Unofficial) Guide to App Architecture Gui...

Sa-ryong Kang
October 05, 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. Clean Architecture is more like a way of thinking than

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

    ただ、 ◦ レイヤの具体的なコンポーネントは定義されていない ◦ 前のページの図はその模範事例の一つにすぎない 10
  5. Yes, Clean Architecture affected The Guide a lot • Layered

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

    are not important? No • 全てのパターン・アーキテクチャーを網羅するのが目的ではない ◦ 一般的なbest practice + 推奨事項 13
  7. 何故ドメイン層が重要なのか • UI層とデータ層の間の仲介がない時に生じる設計上の問題を解決できる ◦ ドメイン層を実装しない場合でも、考察が必要 • UI層に集中しすぎると、 ◦ 画面中心思考 →

    画面の区別を超える対応が難しくなる ◦ Agile逆効果 → 画面の要素がそのままbacklog/featureになり、 classになってしまう • データ層に集中しすぎると、 ◦ データスキーマがドメイン/UI層まで影響 → 典型的な設計ミス 15
  8. 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/
  9. 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/
  10. Think again: is that a screaming architecture? 21 • 手順

    a. コーヒー粉をフィルターに入れ、バスケットを閉じる b. 水を水タンクに入れ、「抽出」ボタン押下 c. 加熱された水が蒸気化 d. 蒸気口が開けられ、コーヒー粉に噴射される e. 抽出されたコーヒーはフィルタを通過しポットに入れる
  11. Repository pattern • よくある誤解: Repository in Android is anti-pattern!? ◦

    It violates Single Responsibility Principle?! • Repositoryは、ドメイン層(又はUI層)から呼び出されるための抽象を提供 ◦ 他のレイヤに見せるためのinterface提供 ◦ データ格納に関する具体的な事項を隠してくれる 26
  12. 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の公式な意見と関係ありません
  13. 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
  14. 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
  15. 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... } }
  16. 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
  17. 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
  18. Consideration on Jetpack Compose • Be cautious about construction ◦

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

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

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

    Dagger Hiltの長所、短所 ◦ 他の選択肢はないか • Multimodule ◦ Large scale modular architecture ◦ マルチモジュール化はいつした方がいい? ◦ 何故私のマルチモジュールはビルドが遅い? 45