Jetpack Composeを使ってみたい③ State

Jetpack Composeを使ってみたい!と思ってCodelabを元に勉強してみた記録。
Jetpack Compose  |  Android デベロッパー  |  Android Developers

Jetpack Composeの基本コードラボのコードを使いながらComposeでの状態の管理についてまとめる。
Jetpack Compose の基本  |  Android Developers

※状態とは時間とともに変化する可能性のある値のこと。

状態の保持

サンプルコードは上記コードラボより。
ボタンをクリックすることでアイテムを展開したり戻したりできるようにし、
展開しているときはボタンのテキストを"Show less"、閉じているときは"Show more"と表示したい。
これを実装するために各アイテムが展開しているかどうかという状態を保持するboolean値を定義する。

//良くない例
@Composable
private fun Greeting(name: String) {
    var expanded = false

    val extraPadding = if (expanded.value) 48.dp else 0.dp

    Surface(
        color = MaterialTheme.colors.primary,
        modifier = Modifier.padding(vertical = 4.dp, horizontal = 8.dp)
    ) {
        Row(modifier = Modifier.padding(24.dp)) {
              Column(modifier = Modifier
                .weight(1f)
                .padding(bottom = extraPadding)
            ) {
                Text(text = "Hello, ")
                Text(text = name)
            }
            OutlinedButton(
                onClick = { expanded = !expanded }
            ) {
                Text(if (expanded) "Show less" else "Show more")
            }
        }
    }
}

onClickにexpandedの値を変更する処理を記述しており、
expandedが変更されたら再コンポーズされて表示が変わることを狙っている。
しかし、この定義の仕方では動作しない。

問題1:Composeが状態の変更に応じて自動で再コンポーズするためには状態をState型で保持する必要があり、このままでは状態の変更が認識されない。
mutableStateOf関数を使用して状態を保持する

問題2:再コンポーズによって変数がfalseにリセットされてしまう。
rememberを使用して状態を記憶する

修正したコードがこちら。

@Composable
private fun Greeting(name: String) {

    val expanded = remember { mutableStateOf(false) }

    val extraPadding = if (expanded.value) 48.dp else 0.dp
    //State.valueで状態を取得する

    Surface(
        color = MaterialTheme.colors.primary,
        modifier = Modifier.padding(vertical = 4.dp, horizontal = 8.dp)
    ) {
        //省略
    }
}

MutableStateオブジェクトの宣言にはbyデリゲート構文を使用することもできる。
この場合、値を取得する時の.valueは不要になる。

import androidx.compose.runtime.getValue
import androidx.compose.runtime.setValue

var value by remember { mutableStateOf(default) }

rememberでの状態の記憶はアクティビティ全体の再起動(画面の回転など)には対応しておらず、
これに対応するためには代わりにrememberSavableを使用する。

 val expanded = rememberSavable { mutableStateOf(false) }

状態ホイスティング

状態をコンポーザブルの呼び出し元に移動して、
コンポーザブルをステートレスにすることを状態ホイスティングという。

サンプルコードは公式ドキュメントより。
状態と Jetpack Compose  |  Android Developers

//状態ホイスティングなし
@Composable
fun HelloContent() {
   Column(modifier = Modifier.padding(16.dp)) {
       //コンポーザブル自身で状態を保持
       var name by remember { mutableStateOf("") }

       if (name.isNotEmpty()) {
           Text(
               text = "Hello, $name!",
               modifier = Modifier.padding(bottom = 8.dp),
               style = MaterialTheme.typography.h5
           )
       }
       OutlinedTextField(
           value = name,
           onValueChange = { name = it },
           //ここで状態の更新
           label = { Text("Name") }
       )
   }
}

上記のコードよりnameというStateをHelloContent関数の呼び出し元に移動する。
呼び出し元にnameをStateとして定義し直し、
値と値を変更するイベントの2つのパラメータに分けてコンポーザブルに渡す。

//状態ホイスティング
@Composable
fun HelloScreen() {
    var name by rememberSaveable { mutableStateOf("") }

    HelloContent(name = name, onNameChange = { name = it })
}

@Composable
fun HelloContent(name: String, onNameChange: (String) -> Unit) {
    Column(modifier = Modifier.padding(16.dp)) {
        Text(
            text = "Hello, $name",
            modifier = Modifier.padding(bottom = 8.dp),
            style = MaterialTheme.typography.h5
        )
        OutlinedTextField(
            value = name,
            onValueChange = onNameChange,
            label = { Text("Name") }
        )
    }
}

これによって、状態が複数の関数で読み取ったり変更される場合、
状態を重複して持つことなく共有可能になり、
状態を持たないことでコンポーザブルの再利用がしやすくテストも容易になる。


前の記事
mtnmr.hatenablog.com
次の記事
mtnmr.hatenablog.com