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