ANDROID CORE · 110
Lifecycle / ViewModel / UI State:誰活多久?誰擁有 State?
Android app 不是 main() 從第一行跑到最後一行。Framework 依系統與使用者事件建立、暫停、重建 UI component。真正要學的是 lifecycle ownership,而不是死背一條固定 callback 順序。
Why now
107–109 已經建立 Trip Meter domain state 與 suspend repository。現在要把它接進 Android screen,而不讓 Activity/Composable 同時變成 business logic、network client、GPS manager、state store。
Learning outcomes
- 能說明 lifecycle callback 是 framework callback,而非固定線性 main flow。
- 能決定 screen state 的 owner。
- 能用 ViewModel 把 UI 與 domain/data 工作分離。
- 能理解 configuration change 與 process death 是不同問題。
1. Lifecycle 是 state machine,不是背一本道路
Created
↓
Started
↓
Resumed
可能因情境:
Resumed → Paused
Started → Stopped
Component 被重建
Process 甚至被系統終止
實際 callback 路徑依 navigation、configuration、multi-window、system reclaim 等情況而變。不要假設每次都完整走同一條。
2. UI component 不應成為唯一 state source
class TripActivity : Activity() {
var distanceKm = 0.0
}
若重要 screen state 只存在 UI component field,重建 component 時就容易遺失或重做昂貴工作。
3. ViewModel:screen-level state owner
data class TripScreenState(
val loading: Boolean = false,
val trip: TripState? = null,
val error: String? = null
)
class TripViewModel(
private val repository: TripRepository
) : ViewModel() {
private val _state =
MutableStateFlow(
TripScreenState()
)
val state:
StateFlow<TripScreenState>
= _state
}
ViewModel 不是 Database,也不是永遠活著;它主要讓 screen-level state / coordination 跨某些 UI recreation 存活並與 lifecycle 整合。
4. StateFlow:UI 訂閱 state,不直接控制 repository internals
fun refresh() {
viewModelScope.launch {
_state.value =
TripScreenState(
loading = true
)
val result =
runCatching {
repository
.loadSnapshot()
}
_state.value =
result.fold(
onSuccess = {
TripScreenState(
trip = it
)
},
onFailure = {
TripScreenState(
error =
it.message
)
}
)
}
}
5. Derived UI data 不必全部塞進 state
val speedText =
state.trip
?.let(::displaySpeed)
?: "--"
如果值可以由 canonical state 直接算出,就不一定要額外保存一份,避免同步問題。
6. Configuration change ≠ Process death
ViewModel 常可保留 screen state。
整個 process memory 可能消失;需要 SavedState / persistent storage 才能真正恢復。
Project checkpoint:Trip Meter Android Shell v1
Activity / Compose UI
↓ observes
TripScreenState
↑
TripViewModel
↓ suspend call
TripRepository
↓
Trip domain core下一課加入 location permission 與 location provider,會出現另一個 state machine。
Debug evidence:旋轉後重複 request
- request 是在 UI creation 每次直接啟動,還是 ViewModel 管理?
- state owner 是誰?
- 是否把 side effect 綁到會重複 composition/recreation 的位置?
- 舊 job 有沒有取消?
7. Current state-saving model:ViewModel、rememberSaveable、SavedStateHandle、Persistence
不要把所有 state 都丟進 ViewModel。現在比較可靠的判斷方式是先問:這個 state 是 UI element state、screen business state,還是 durable domain data?
| State 類型 | 典型 owner / mechanism |
|---|---|
| 輕量 UI element state | rememberSaveable / saved instance state |
| Screen-level business state | ViewModel |
| Screen state 需要跨 system-initiated process death 恢復 | SavedStateHandle 作為 backup |
| 真正 durable domain data | Database / file / repository persistence |
ViewModel 會跨 configuration change 保留,但不會在 system-initiated process death 後保留同一個 in-memory instance。SavedStateHandle 則適合保存輕量、可重建 screen state;它仍不是永久資料庫。
Rotation
Activity recreated
ViewModel retained
Process death
memory gone
SavedStateHandle / rememberSaveable may restore lightweight state
Reboot / task removal / durable history
persistent data layer8. Lifecycle evidence 不只 callback name
Debug lifecycle 問題時,最重要的是把「誰被重建、誰仍存在、哪個 side effect 在哪個 scope 啟動」記錄下來。只看一條 callback log 很容易把 configuration change 誤認為 process death。
Evidence:
Activity instance id
ViewModel instance id
process id
saved-state keys
repository request id
job start / cancel time
如果 Activity id 變、ViewModel id 不變,較像 configuration recreation;如果 process id 與 ViewModel 都換了,就要往 process recreation / saved-state recovery 查。
Knowledge check
- 為什麼 Activity lifecycle 不是固定一本道路?
- ViewModel 能保證 process death 後資料一定還在嗎?
- 哪些是 canonical state,哪些可 derive?
- 畫出 UI → ViewModel → Repository → state → UI 的資料流。