---
# System prepended metadata

title: Android筆記–Jetpack Compose入門(五)狀態管理
tags: [mutableStateOf, 管理, rememberSaveable, android, VM, 狀態提升, Compose, 狀態, "Saveable\_", Jetpack, Remember, ViewModel]

---

# 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&lt;T&gt;</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 | 
>| -------- | -------- |
>| ![1742390008061](https://hackmd.io/_uploads/ByNDdFt3yg.gif =60%x) | ![1742390074324](https://hackmd.io/_uploads/SkEd_Kt2Jx.gif =60%x) |
>
>
></div>
></details>


</br>

<font size=5>&lt; <u><code>remember</code> 官方推薦寫法</u> &gt;</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&lt;T&gt;</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>&lt; <u><code>keyed remember</code> -- <code>remember</code> 的參數</u> &gt;</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>&lt; <u><code>remember</code> 和 <code>mutableStateOf()</code> 的工作原理</u> &gt;</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&lt;T, S&gt;</code>，<code>T</code> 代表你原本的資料型態，<code>S</code> 代表序列化後的型態（例如 <code>Map&lt;String, Any&gt;</code>）。
  - <code>restore</code>: 負責提供如何從序列化的資料中還原成原本的資料型態。

<font size=5>&lt; 範例 &gt;</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")
        }
    }
}
```
![1742875208820](https://hackmd.io/_uploads/SkcWss1aJg.gif =60%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>
![1742957932294](https://hackmd.io/_uploads/H1oap1ZTye.gif)

</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&lt;T&gt;</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)
- 