# Android筆記–Jetpack Compose入門(五)狀態管理
在 Jetpack Compose 中,介面的更新是透過不斷重新執行 <code>Composable 函式</code>來達成的,因此,如果在 Compose 函式中定義了一個變數,==每當資料或畫面更新,這個變數就會被重新賦值,資料就消失了==。
這時就==需要 <code>remember</code> 來幫忙「記住」變數的值==,使它在同一個 <code>Composition 生命週期</code>內不會因為重組而被重設。
但光是「記住」還不夠,Compose 還需要知道「這個值改變了,請重新繪製畫面」,因此通常==會搭配 <code>mutableStateOf()</code> 建立可觀察的狀態物件==。
不過,<code>remember</code> 只能撐過一般的重組,==一旦遇到螢幕旋轉或系統回收資源,記憶就會消失;這時就需要換用 <code>rememberSaveable</code>==,它會將狀態資料保存到可序列化的儲存機制中,讓 UI 在重建後也能恢復到先前的狀態。
---
<code>remember</code> 與 <code>mutableStateOf()</code>
---
在 Compose 中,只要資料有變化,畫面就會自動重新組合,因而「遺忘」原本的變數值,所以我們需要使用 <code>remember</code> 來進行記憶。
當我們使用 <code>remember</code> 時,通常會搭配 <code>mutableStateOf()</code> 使用,<code>mutableStateOf()</code> 是[觀察者模式](https://hackmd.io/@9YAtszqXS2OLNZOrLY_-Jg/DesignPattern_ObserverPattern)的實作,當 <code>muteableStateof()</code> 中的資料內容發生變動時,會主動發出訊號通知 Compose 重新繪製 UI
>[!Warning]不使用 <code>remember</code> 的錯誤範例
><details><summary>展開</summary>
><div>
>
>---
>```kotlin=
>//不使用Remember的錯誤範例
>@Composable
>fun Counter() {
> var count = 0
> Button(onClick = { count++ }) {
> Text("Clicked $count times")
> }
>}
>```
>會發現不管怎麼按,都是顯示「Clicked 0 times」,這是因為每次點擊按鈕時,畫面會重新組合,count 又重新變成 0。
>
></div>
></details>
>[!Tip]使用 <code>remember</code> 和 <code>mutableStateOf()</code> 的正確作法
><details><summary>展開</summary>
><div>
>
>---
>```kotlin=
>//使用Remember的正確範例
>@Composable
>fun Counter() {
> var count by remember { mutableStateOf(0) } // 加入了Remember
> Button(onClick = { count++ }) {
> Text("Clicked $count times")
> }
>}
>```
>- <code>mutableStateOf()</code>(可變狀態容器): 將任何型別的普通值,包成 Compose 可觀察、可驅動 UI 更新的 <code>MutableState<T></code>,也就是將一個變數變為"[Composable](https://hackmd.io/5timxP8GSIatbH1sknAXIg?view=#:~:text=%E8%88%87%E5%82%B3%E9%81%94%E8%A8%8A%E6%81%AF%E3%80%82-,%40Composable,-%E2%80%93%20%E5%AE%9A%E7%BE%A9%E5%8F%AF%E7%B5%84%E5%90%88)"。
>
>|不使用remember | 有使用remember |
>| -------- | -------- |
>|  |  |
>
>
></div>
></details>
</br>
<font size=5>< <u><code>remember</code> 官方推薦寫法</u> ></font>
```kotlin=
var value by remember { mutableStateOf(defaultValue) } // 最常用
val value = remember { mutableStateOf(defaultValue) } // 次常用
val (value, setValue) = remember { mutableStateOf(defaultValue) }
```
<details id="byRemember"><summary><code>var value by remember { mutableStateOf(defaultValue) }</code></summary>
<div>
---
Kotlin 的[委託語法(by)](https://hackmd.io/@9YAtszqXS2OLNZOrLY_-Jg/kotlin_by),會自動把 <code>getter</code>/<code>setter</code> 對應到 <code>mutableState.value</code>,讓你可以直接使用變量,而不需要每次都訪問 <code>.value</code>,==最常用的寫法==。
- 使用方式:
- 讀取值:直接使用 <code>value</code>
- 修改值:直接賦值 <code>value = newValue</code> 或 <code>value++</code>
- 特點:
- 語法最簡潔
- 使用起來就像一個普通變量
- 大多數情況下的首選方式
</div>
</details>
<details><summary><code>val value = remember { mutableStateOf(defaultValue) }</code></summary>
<div>
---
直接存儲 <code>MutableState<T></code> 對象,是最原始的狀態物件寫法。
- 使用方式:
- 讀取值:`mutableState.value`
- 修改值:`mutableState.value = newValue`
- 特點:
- 明確顯示這是一個狀態對象
- 需要每次都使用 `.value` 來訪問實際的值
</div>
</details>
<details><summary><code>val (value, setValue) = remember { mutableStateOf(defaultValue) }</code></summary>
<div>
---
使用了 <code>Kotlin 解構宣告</code>,將狀態對象分解為值和設置值的函數。
- 使用方式:
- 讀取值:直接使用 <code>value</code>
- 修改值:調用函數 <code>setValue(newValue)</code>
- 特點:
- 將讀取和修改操作分開
- 類似於 React 中的 <code>useState Hook</code>
- 提供了一個明確的設置函數
</div>
</details>
- 參考:[可組合項中的狀態](https://developer.android.com/develop/ui/compose/state?hl=zh-tw#state-in-composables)
</br>
<font size=5>< <u><code>keyed remember</code> -- <code>remember</code> 的參數</u> ></font>
- 一般的 <code>remember {...}</code> 只會在 Compose 執行時,之後即使發生重組,也會持續回傳同一個已記住的物件,不會重新建立(重複使用同一個 <code>lambda 區塊</code>)。
- 如果是 <code>remember(key) { ... }</code> 則會持續監視 <code>key</code> 的值,若 key 的值發生變化,則執行初始化邏輯並更新緩存</code>(建一個全新的 <code>lambda 區塊</code>)。此方法能有效優化效能,防止不必要的重複計算。
- <code>keyed remember</code> 和 <code>mutableStateOf()</code> 乍看之下很像,都是在變動時進行更新
- <code>keyed remember</code> 重新建立的是整個 <code>remember 的 lambda 區塊</code>(「記憶」本身被銷毀並重新初始化)。
- <code>mutableStateOf()</code> 則只是更新容器內部的數值(記憶本身沒變,只是內容物變了,並觸發「讀取過它的畫面」重新繪製)。
```kotlin=
remember(key) { ... }
```
```kotlin=
// 範例
@Composable
fun SkeletonExample(id: Int) { // 步驟 1:定義 Key (這裡的 id 就是 Key)
val cachedValue = remember(id) { // 步驟 2:將 key 值作為參數放入 remember(key){...} 中
"Current ID is $id" // 只有當 id(key 值) 變動時,這行才會重新執行
}
// 步驟 3:使用資料
Text(text = cachedValue)
}
```
</br>
<font size=5>< <u><code>remember</code> 和 <code>mutableStateOf()</code> 的工作原理</u> ></font>
<details style="border-left: 4px solid #1565C0;background-color: #0D1B2A;padding: 12px 16px;margin: 10px 0;border-radius: 4px;font-family: Arial, sans-serif;"><summary><code>remember</code> 的工作原理</summary>
<div>
---
- 按序記錄: 當 Compose 執行時,會像在「座次表」上填空位一樣,==依照 <code>remember</code> 在程式碼中出現的先後順序,把資料一個接一個存入格子==,這個格子稱為「<code>插槽表(slot table)</code>」。
- 依序存取: 每次<code>重組(Recomposition)</code>時,Compose 會拿著游標以==依序推進的方式開始往後走訪 <code>slot table</code>==,預期在相同的順序位置,讀到上一次存進去的資料。
- 位置依賴: 由於系統是「認位置不認人」,如果把 <code>remember</code> 放在不確定的結構中(例如 <code>if 條件式</code>)導致它消失,游標就會找錯格子、讀到錯誤的資料,因此必須維持穩定的執行順序。
</div>
</details>
<details style="border-left: 4px solid #1565C0;background-color: #0D1B2A;padding: 12px 16px;margin: 10px 0;border-radius: 4px;font-family: Arial, sans-serif;"><summary><code>mutableStateOf()</code>的工作原理</summary>
<div>
---
- 讀取時註冊:當 <code>Composable 函式</code>「讀取」這個狀態的值時,Compose 會默默記下:「喔!這個 UI 元件依賴於這筆資料。」
- 寫入時通知:當你「修改」這個狀態的值時,它會立刻發出訊號給 <code>Compose Runtime</code>。
- 觸發重組:Compose 收到訊號後,會精準地通知「讀取過這個值的 Composable 區塊」,叫它們重新執行一遍(Recomposition)。這讓 Compose 能夠進行精確的局部刷新,效能更好。
</div>
</details>
---
RememberSaveable(基本)
---
當螢幕旋轉或系統回收資源,<code>remember</code> 的記憶就會消失,此時就需要改用 <code>rememberSaveable</code>。 <code>rememberSaveable</code> 和 <code>remember</code> 一樣,是 <code>Jetpack Compose</code> 中用來保存 UI 狀態的一種工具
- 不同的是,<code>rememberSaveable</code> 會將資料存進 [Bundle](https://hackmd.io/@9YAtszqXS2OLNZOrLY_-Jg/android_bundleClass),以便在 <code>Activity</code>/<code>Fragment</code> 重構後能自動還原先前的狀態
- 預設情況下,<code>rememberSaveable</code> 只支援基本型別(因為是由 [Bundle](https://hackmd.io/@9YAtszqXS2OLNZOrLY_-Jg/android_bundleClass) 來實現)
- 如果需要保存複雜型態,就必須自訂一個 Saver,讓 Compose 知道如何序列化和反序列化該資料。
```kotlin=
// 範例
// rememberSaveable 的使用和 remember 一樣
@Composable
fun InputField() {
// 使用 rememberSaveable 保留文字輸入,即使螢幕旋轉也不會丟失
var text by rememberSaveable { mutableStateOf("") }
TextField(
value = text,
onValueChange = { text = it },
label = { Text("請輸入內容") }
)
}
```
---
<code>rememberSaveable</code>-- 進階(自訂型別)
---
預設的情況下,<code>rememberSaveable</code> 只支援基本型別(參見: [Bundle](https://hackmd.io/@9YAtszqXS2OLNZOrLY_-Jg/android_bundleClass)),但如果要保存自訂的或複雜的型態,就必須定義一個 「Saver」。
- <code>Saver</code> 負責告訴系統:「如何把這個物件轉成可存的格式,以及如何還原」(類似於 [Room 中的 @TypeConverter](https://hackmd.io/lza-34jBT-2zEnqDsiU1VA#%E2%96%BCTypeConverter%E3%80%81TypeConverters))
- <code>Saver</code> 介面的實作,須提供 <code>save</code> 與 <code>restore</code> 兩個 <code>lambda</code>
- <code>save</code>: 負責提供如何將資料轉換成可序列化的格式,格式為 <code>Saver<T, S></code>,<code>T</code> 代表你原本的資料型態,<code>S</code> 代表序列化後的型態(例如 <code>Map<String, Any></code>)。
- <code>restore</code>: 負責提供如何從序列化的資料中還原成原本的資料型態。
<font size=5>< 範例 ></font>
<details><summary>步驟1. 定義一個自訂型別 <code>DataClass</code></summary>
<div>
[甚麼是 DataClass ?](https://hackmd.io/@9YAtszqXS2OLNZOrLY_-Jg/kotlin_DataClass)
```kotlin=
// 自訂型別
data class Point(val x: Int, val y: Int)
```
</div>
</details>
<details><summary>步驟2. 定義一個 <code>Saver</code> 來描述資料的轉換格式</summary>
<div>
```kotlin=
val PointSaver = Saver<Point, List<Int>>(
save = { point -> listOf(point.x, point.y) }, // 將 Point Class 轉換為 List Class
restore = { list -> Point(list[0], list[1]) } // 將 List Class 轉換為 Point Class
)
```
</div>
</details>
<details><summary>步驟3. 使用</summary>
<div>
```kotlin=
@Composable
fun MyScreen() {
// 螢幕旋轉後,point 的值依然保留
var point by rememberSaveable(stateSaver = PointSaver) {
mutableStateOf(Point(0, 0))
}
Button(onClick = { point = Point(point.x + 1, point.y + 1) }) {
Text("目前座標:(${point.x}, ${point.y})")
}
}
```
</div>
</details>
---
<details><summary>範例 2</summary>
<div>
<font size=4><u>1. 定義一個 DataClass</u></font>
```kotlin=
data class PointDataClass(val x: Int, val y: Int)
```
假設我們有一個自訂的Dataclass`PointDataClass`,因為 `PointDataClass` 並非基本型別,我們需要自訂一個 Saver 來告訴 Compose 如何保存與還原它。
<font size=4><u>2. 定義 Saver</u></font>
定義一個 Saver,將 PointDataClass 轉換成一個包含 x 與 y 的 List,再從 List 還原回 Point。建議另外開一個File檔案以[object宣告](https://hackmd.io/@9YAtszqXS2OLNZOrLY_-Jg/kotlin_object)製作成工具類別。
```kotlin=
object PointDataClassUtils{
// 自訂 Saver:將 PointDataClass 轉換成 List<Int> 儲存,並從 List 還原
val pointSaver = Saver<PointDataClass, List<Int>>(
save = {point -> listOf(point.x, point.y) },
restore = { list -> PointDataClass(list[0], list[1])}
)
}
```
<font size=4><u>3. 使用 rememberSaveable 搭配自訂 Saver</u></font>
在 Composable 中使用 `rememberSaveable`,並指定剛剛定義的 `PointSaver`,這樣即使螢幕旋轉或配置變更,狀態也能被正確保存與還原。
```kotlin=
@Composable
fun PointScreen(){
//在 Composable 中使用 rememberSaveable,並指定剛剛定義在PointDataClassUtils中的 PointSaver,這樣即使螢幕旋轉或配置變更,狀態也能被正確保存與還原
//使用自定義的 PointDataClassUtils中的pointSaver 來儲存和恢復 Point 狀態
var point by rememberSaveable(stateSaver = PointDataClassUtils.pointSaver) { mutableStateOf(PointDataClass(10, 20)) }
Column(modifier = Modifier.padding(16.dp)) {
Text(text = "x: ${point.x}, y: ${point.y}")
Spacer(modifier = Modifier.height(8.dp))
//用 copy() 函數來創建一個新的 Point 物件,並遞增 x 座標
Button(onClick = { point = point.copy(x = point.x + 1) }) {
Text("Increase x")
}
}
}
```

</div>
</details>
<details><summary>範例 3</summary>
<div>
<font size=4><u>1. DataClass</u></font>
```kotlin=
data class UserDataClass(val name: String, val age: Int)
```
<font size=4><u>2. Saver</u></font>
```kotlin=
object UserDataClassUtil{
val UserSaver = Saver<UserDataClass, Map<String, Any>>(
save = { user ->
// 將 User 轉換成 Map<String, Any>
mapOf("name" to user.name, "age" to user.age)
},
restore = { map ->
// 從 Map 還原出 User
// 由於 map 的值型態是 Any,因此需要做類型轉換
val name = map["name"] as? String ?: ""
val age = map["age"] as? Int ?: 0
UserDataClass(name, age)
}
)
}
```
<font size=4><u>3. 使用</u></font>
```kotlin=
@Composable
fun UserProfile() {
// 使用 rememberSaveable 並指定自訂的 UserSaver
var user by rememberSaveable(stateSaver = UserDataClassUtil.UserSaver) {
mutableStateOf(UserDataClass("Alice", 25))
}
// 假設這裡有一個 UI 來展示 user 的內容
Column {
Text("Name: ${user.name}")
Text("Age: ${user.age}")
Button(onClick = {
// 例如,點擊後更新 user 的年齡
user = user.copy(age = user.age + 1)
}) {
Text("Increase Age")
}
}
}
```
<font size=5>成果:</font>

</div>
</details>
>[!Note]如果你的自訂型態能夠實作 [Parcelable](https://hackmd.io/@9YAtszqXS2OLNZOrLY_-Jg/android_ParcelableObject)(例如透過 @Parcelize)
>那麼 <code>rememberSaveable</code> 也能直接保存它,而不需要自訂Saver。
>參考:[Android筆記–Parcelable實作產生器(@Parcelize)](https://hackmd.io/@9YAtszqXS2OLNZOrLY_-Jg/android_Parcelize)
---
[狀態提升](https://developer.android.com/develop/ui/compose/state?hl=zh-tw#state-hoisting)
---
使用 <code>remember</code> 儲存物件的可組合項會建立內部狀態,使該可組合項「有狀態」。這種做法在呼叫端不需要控制狀態的情況下很有用。
不過,具有內部狀態的可組合項往往不易重複使用,也更難測試。
因此,在實務上我們會根據 <code>SOLID</code> 中的 [單一職責原則(SRP)](https://hackmd.io/QWzetRmyRoKh2JPe43CDWA?view#:~:text=Single%20Responsibility%20Principle),我們將其拆分為「持有狀態的責任」和「顯示 UI 的責任」分開,並將保有狀態的責任向上交付給上層元件。
>[!Warning]不狀態提升: 狀態在內部,有狀態
><details><summary>範例</summary>
><div>
>
>```kotlin=
>// HelloContent 自己管理狀態 → 外部無法控制
>@Composable
>fun HelloContent() {
> // 狀態鎖在這裡,外部看不到也摸不到
> var name by rememberSaveable { mutableStateOf("") }
>
> OutlinedTextField(
> value = name,
> onValueChange = { name = it } // 自己改自己
> )
>}
>```
>
></div>
></details>
>[!Tip]狀態提升: 狀態在上層元件,將「持有狀態的責任(包含實務邏輯)」和「顯示的責任分開」
><details><summary>負責持有狀態及業務邏輯</summary>
><div>
>
>```kotlin=
>// HelloScreen:狀態的擁有者(有狀態)
>@Composable
>fun HelloScreen() {
> var name by rememberSaveable { mutableStateOf("") } // 狀態在這裡
> val handleNameChange: (String) -> Unit = { newName -> ... } // 邏輯寫在這裡
>
> // 呼叫負責處理顯示的子元件,並依據需求將狀態及業務邏輯作為參數傳入
> HelloContent(
> name = name,
> onNameChange = handleNameChange
> )
>}
>```
></div>
></details>
>
>
><details><summary>負責顯示</summary>
><div>
>
>```kotlin=
>// HelloContent:純粹顯示,不擁有狀態(無狀態)
>@Composable
>fun HelloContent(
> name: String, // 只接收值
> onNameChange: (String) -> Unit // 只回報事件,不自己改
>) {
> OutlinedTextField(
> value = name,
> onValueChange = onNameChange // 使用者輸入 → 通知上層
> )
>}
>```
></div>
></details>
---
<code>mutableStateOf()</code> 與 ViewModel 協作
---
在 <code>Jetpack Compose</code> 的狀態管理架構中,除了利用 <code>remember</code> 與 <code>rememberSaveable</code> 處理 UI 區域狀態外,當我們結合 <code>ViewModel</code> 與 <code>mutableStateOf()</code> 時,通常會採用 『[後備屬性 (Backing Properties)](https://hackmd.io/@9YAtszqXS2OLNZOrLY_-Jg/kotlin_BackingProperty)』 的技巧,以確保狀態的封裝性與安全性。
- 須注意,在 ViewModel 中存取 <code>MutableStateOf<T></code> 的值時,需使用 <code>.value</code>(就算使用了 [by 語法](#byRemember) 也是一樣)
- 如果存取的不是基本類型而是自訂的 <code>DataClass</code>,通常不會直接修改內部欄位,而是透過 [DataClass 的 copy() 方法](https://hackmd.io/@9YAtszqXS2OLNZOrLY_-Jg/kotlin_DataClass#copy) 建立一份新的物件,只更新需要變更的欄位,再重新指定給 <code>.value</code>
```kotlin=
class CounterViewModel : ViewModel() {
// 1. 內部私有狀態:可讀寫 (Mutable),負責實際的數據儲存
// 目的:封裝數據,防止外部 UI 組件直接修改
private val _count = mutableStateOf(0)
// 2. 外部公開屬性:唯讀 (State),僅供 UI 觀察
// 目的:利用 Kotlin 的 get() 將類型轉為不可變的 State,確保單向資料流
val count: State<Int> = _count
// 3. 定義明確的行為 (Action)
// 目的:這是修改狀態的唯一入口,方便後續加入邏輯(如:最大值檢查)
fun increment() {
_count.value++
}
}
```
- <code>State<T></code> 是 <code>MutableStateOf</code> 的唯讀型態
---
<code>remember</code>、<code>rememberSaveable</code>、<code>ViewModel</code> 的選擇
---
<code>remember</code>、<code>rememberSaveable</code>、<code>ViewMode</code> 都可以「保留狀態」,但他們的責任範圍和保存時機不一樣。
```bash=
這個狀態需要在「螢幕旋轉或系統回收」後還原嗎?
↙ No ↘ Yes
這個狀態需要 這個狀態需要跨
跨 Composable 共享嗎? Composable 共享嗎?
↙ No ↘ Yes ↙ No ↘ Yes
remember ViewModel rememberSaveable ViewModel + SavedStateHandle
```
| | `remember` | `rememberSaveable` | `ViewModel` |
| --- | --- | --- | --- |
| 撐過重組 | ✅ | ✅ | ✅ |
| 撐過螢幕旋轉 | ❌ | ✅ | ✅ |
| 撐過系統回收 | ❌ | ✅ | ❌ |
| 跨 Composable 共享 | ❌ | ❌ | ✅ |
| 適合放商業邏輯 | ❌ | ❌ | ✅ |
---
Q&A
---
<details><summary>既然 <code>rememberSaveable</code> 已經把資料存進 <code>Bundle</code> 中了,那我們可以直接拿來用在不同 <code>Activity</code> 之間的傳遞嗎?</summary>
<div>
---
不行,雖然 <code>rememberSaveable</code> 確實使用了 <code>Bundle</code> 機制,但它的 <code>Bundle</code> 是私有的,它的用途只有一個:在同一個 <code>Composable</code> 的生命週期事件後還原資料。
</div>
</details>
---
上一篇:[Android筆記–Jetpack Compose入門(四)Modifier](https://hackmd.io/@9YAtszqXS2OLNZOrLY_-Jg/Android_JetpackCompose_4)
下一篇:[Android筆記–Jetpack Compose入門(六)Coil](https://hackmd.io/@9YAtszqXS2OLNZOrLY_-Jg/Android_JetpackCompose_6)
---
GitHub:
- Remember的示範: [MyPratice_JetpackCompose_Remember_Sample](https://github.com/PudCheetah/MyPratice_JetpackCompose_Remember_Sample)
- RememberSaveable示範1:[MyPratice_JetpackCompose_RememberSaveable_Sample_1](https://github.com/PudCheetah/MyPratice_JetpackCompose_RememberSaveable_Sample_1)
---
參考資料:
---
- [Jetpact Compose状态管理remember简单理解](https://juejin.cn/post/7000137483220418590)
- [狀態與 Jetpack Compose ](https://developer.android.com/develop/ui/compose/state?hl=zh-tw)
-