配合分支:day08/start,Repo: 按我

昨天注意到,製作日記時機器轉一下什麼都不見了!(噢不!)這是怎麼一回事?精準的來說,這個是 Android 平台的特有問題,未來如果需要摸到 Client side 的 App(就是跑在使用者裝置上的),很可能也會遇到!
這類的 Client side app,通常建立在某種框架之上 (也就是,站在巨人的肩膀上。我們只需要用巨人提供的程式碼即可,不過,也需要遵守巨人訂出來的遊戲規則。框架英文是 Framework),Android 就是一種 Framework。包含:iOS、Web (React⋯)、遊戲(如:Unity),這類的 Framework 寫的程式,會有某種生命週期 (Lifecycle),但每一個巨人訂的遊戲規則不同。
畢竟如果使用者殺掉程式,那這些程式也沒必要留在記憶體裡頭。但是生命週期的管理,不是本系列文章 專注的部分,只是這個問題,先知道有這個東西就好。
簡而言之,機器轉一下在 Android App 的世界觀裡頭會走一次毀滅與重生,所以轉一下一切都毀了!但是不對!這樣其他的 App 怎麼做的呢?所幸,在 2026 年 Google 已經對於變數選轉死亡這點,已經有官方建議的實作方式,這次需要使用 Google 提供的 Library (也就是已經寫好的工具程式碼,中文是:函式庫,中國翻譯為「庫」),也就是 ViewModel。
今天就來 Focus 引入 Library 吧。
首先要先寫:需要哪一個 Library,這個寫在 gradle/libs.versions.toml 裡頭。
androidx-lifecycle-viewmodel-compose = { group = "androidx.lifecycle", name = "lifecycle-viewmodel-compose", version.ref = "lifecycleRuntimeKtx" }
寫在 [libraries] 區塊的下面,group 和 name 把它想成像是 library 的 id 那樣,可能會來自同一個 group ,每一個 library 在一個 group 下有一個 name。 後面的 version.ref 是版本,這個 library 和另外一個 lifecycle 來自同一個 group 所以直接用同樣的版本。
接著需要開啟另外一個檔案 app/build.gradle.kts。要加入一段 Code 讓編譯時,也把這個 Library 加入。因為編譯的時候,也需要多編譯這個才能繼續,這類的東西也叫做 Dependency (中文可能是依賴)。

所以聽到 Agent 說要加入依賴 (dependency)是,就需要知道它要引入新的 library 了。 接著需要重新進行 Gradle Sync,Gradle 是 Android 程式的 Dependency 管理工具,按下 Files -> Sync Project with Gradle Files,Gradle 就會將 Library 都拉下來了。

接下來,要利用 Google 提供的 ViewModel class,來保護狀態。說到狀態 (State),回到 PhotoDiaryApp() 會發現黏了很多狀態作為變數,例如:
// 主要 PhotoDiaryApp 狀態
var entries by remember { mutableStateOf(sampleEntries()) }
var isAddingDiary by remember { mutableStateOf(false) }
var validationError by remember { mutableStateOf<String?>(null) }
// 卡片裡頭的 DiaryForm
var title by remember { mutableStateOf("") }
var note by remember { mutableStateOf("") }
紀錄了一些狀態,例如日記列表,是否加入日記的開關,以及錯誤訊息。卡片裡頭則有標題和內文。這些都需要遷移到 ViewModel 裡頭。來看 ViewModel 怎麼寫:

先把這些狀態黏在 ViewModel 裡頭,這裡
class PhotoDiaryViewModel : ViewModel() {
// 藍圖
}
: ViewModel() 代表 PhotoDiaryViewModel 繼承(字面上的意思) 自 Google 寫好的 ViewModel,也就擁有了 ViewModel 的能力。跟 MainActivity : ComponentActivity() 一樣,MainActivity 是繼承自 Android Framework 的 ComponentActivity,才有 Android Activity 超能力。
先看下面這一塊:
private fun sampleEntries(): List<DiaryEntry> = listOf(
DiaryEntry(
photoResId = R.drawable.diary_example001,
title = "城市縮影裡的警醒",
note = "看見熟悉的城市被做成防災模型,才發現準備不能只停在想像。",
mood = "震撼",
createdAt = LocalDateTime.parse("2026-03-10T17:52:37")
),
DiaryEntry(
photoResId = R.drawable.diary_example002,
title = "夜裡的共享空間",
note = "雨夜裡留下來整理想法,空間安靜得剛剛好。",
mood = "專注",
createdAt = LocalDateTime.parse("2026-04-04T21:04:51")
),
DiaryEntry(
photoResId = R.drawable.diary_example003,
title = "山海之間的晴天",
note = "站在高處看著海岸線,雲和海把心情慢慢拉開。",
mood = "平靜",
createdAt = LocalDateTime.parse("2026-01-04T11:23:44")
)
)
這是一個範例 DiaryEntry 列表,然後是私有函數 (private function),代表只有 PhotoDiaryViewModel 可以存取,別人看不到。然後把變數黏上去:
var entries by mutableStateOf(sampleEntries())
var isAddingDiary by mutableStateOf(false)
private set
var title by mutableStateOf("")
private set
var note by mutableStateOf("")
private set
var validationError by mutableStateOf<String?>(null)
private set
和原來那邊的實作,有幾個不同:
remember。 remember 是給畫面程式碼用的 ViewModel 不是,所以不需要。(先這樣理解就好)viewModel.title // 👈🏼 可以拿到
viewModel.title = "Hello" // 🫠 失敗,因為 set 是 private
接著對外暴露一些方法,給其他人使用

有幾個方法給外面的人呼叫:暫存新日記的標題、筆記,儲存新日記到日記列表,還有切換新日記表格需不需要顯示的 Method。
fun startAddingDiary(): 顯示新增日記的介面用fun updateTitle(title: String):儲存新增日記表單的「標題」內容fun updateNote(note: String):儲存新增日記表單的「筆記」內容fun saveDiary(title: String, note: String):新增筆記到列表,在 callback function 時搓這個fun cancelAddingDiary():放棄新增日記,就搓這個private fun clearForm():私有方法,快速清除新增日記表單的筆記和標題,設計成私有方法重複使用。再來看看 Screen 怎麼調整,要先把 ViewModel init 出來,不過 ViewModel 因為是來自於 Google 的 Library 不能隨意從建構子(Constructor)創建。幸好,他們有提供很懶惰的方式取得 ViewModel。
不過,先來看一下怎麼調新增照片日記的表單:

這裡很單純,表單不綁變數後,直接進行 callback,讓外層的人去煩惱。這招有個專有名詞,叫 State Hoisting。

用 viewModel() 就可以取得 PhotoDiaryViewModel。有兩個前提:
import androidx.lifecycle.viewmodel.compose.viewModel 把 Library 寫好的程式碼 import 進來@Composable 這種函數才能使用,這個階段只需要知道有寫 @Composable 的函數,才可以畫 UI (像是 Text,Column 之類的東西⋯)取得 ViewModel 後就很單純,該取資料的去取資料。該設定的去設定資料。這裡補充這種超帥的寫法:
onTitleChange = viewModel::updateTitle
onTitleChange 是 Function Type: (String) -> Unit 代表會回傳一個 String。拆開來看就是:
onTitleChange = { viewModel.updateTitle(it) } // 那個 it 就是回來的 String 啦
Kotlin 語法糖很變態,他會直接去搓 Method 照著順序塞進去。因為 updateTitle 只接受一個 String,他就會自動把 it 塞進去。這是一個語法糖。
又像是:
onSave = viewModel::saveDiary,
onSave 會回傳:(String, String) -> Unit 兩個 String,然後 saveDiary 接受兩個參數:
fun saveDiary(title: String, note: String)
所以這裡的語法糖就會依序塞進去:
onSave = { aaa, bbb ->
viewModel.saveDiary(aaa, bbb)
}
是不是很舒爽呢?

太好了!終於修正完成!一切都正常的感覺真好! 🤗
ViewModel 來保護狀態。var 只要 mutableStateOf,不要再包 remember。app/build.gradle.kts 加入一段 code,編譯時才會把這個 Library 一起編進去;聽到「加入依賴」就是在引入新 library。PhotoDiaryApp() 的變數搬進來,轉螢幕就不會全部不見。: ViewModel()):PhotoDiaryViewModel 繼承自 Google 的 ViewModel 才有它的能力,跟 MainActivity : ComponentActivity() 同一招。viewModel.title,但不能直接指定數值;要改值只能呼叫它對外公開的 method。viewModel::updateTitle(語法糖):onTitleChange 是 (String) -> Unit,寫成 method 參照會依序把參數塞進對應 method,等於自己寫 lambda 去搓它。沒想到 2026 年還會講怎麼引入 Library,文藝復興!
想不到吧,我怕開發者被 Agent 加的不明不白!