入力フィールドの値をviewModelを使って保持する

:::note info
Androidアプリを勉強中です。
UIはJetpackComposeを使ってます。
今日はできそうでできない、テキストフィールドの値更新です。
:::

# やりたいこと
こんな感じで、TextScreenという名前のテキストフィールド３つ、ボタン１つある画面を作りました。
・入力したデータを画面に表示させたい（今のままでは何も表示されないので）
・３つのフィールド全部入力されたら「登録」ボタンが押せるようにしたい
・状態を保持するためviewModelを使いたい

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3556392/9ce9b45e-db7e-43e2-8a65-c3a85fe35908.png)

画面のコードは以下の通りです。
```kotlin:TestScreen
@Composable
fun TestScreen(modifier:Modifier = Modifier) {
    Column(
        modifier=modifier.padding(16.dp).fillMaxSize(),
        horizontalAlignment = Alignment.CenterHorizontally,
    ) {
        TextField(
            label= { Text("id") },
            value = "",
            onValueChange = {  },
            modifier = Modifier.padding(16.dp)
        )
        //・・・省略・・・
        Button(
            onClick = {},
            modifier = Modifier.padding(16.dp)
        ){
            Text(text = "登録")
        }
    }
}
```
うん。見た目だけはOK

# 入力データの型(Model)を作成
idとnameとageを管理したいので、TestModelというデータクラスを作りました。
テキストフィールドに入力された値は、この形式で管理します。
idやageのように数値入力が目的なのに、IntとせずStringにしているのは、
テキストフィールドがStringしか扱えないため。
```kotlin:TestModel
data class TestInputData(
    val id: String="",
    val name:String="",
    val age: String="",
)
```
id:Intとしておいて、テキストフィールドに送るときid.toString()と変換するという処理も考えたけど、フィールドをブランクにできずあきらめた:joy:

# UIの状態を保持する場所を作る
今度は、実際にテキストフィールドに入力されたデータを保持する場所（testInputData）と
現在の入力値を判定して、３つのテキストフィールドにブランクがなければtrue、
１つでもブランクがあればfalseを設定するもの（isValid）を作ります。
```kotlin:TestUiState
data class TestUiState(
    val testInputData: TestInputData = TestInputData(),
    val isValid: Boolean = false
)
```

# viewModelを作り入力を反映するロジックを追加
２つ変数を作ります。
_uiStateはTestUiStateの変更を監視し、変更があれば画面に反映するというもの。
外部から変更はできない。
uiStateは外部から_uiStateを参照するためにある。

```kotlin:TestViewModel
class TestViewModel: ViewModel() {
    private val _uiState = MutableStateFlow(TestUiState())
    val uiState = _uiState.asStateFlow()
}
```

TestViewModelの中にupdateFieldメソッドを追加。
テキストフィールドに何かが入力されたら、その値でTestUiStateが更新される。
テキストフィールドは３つあるので、どのフィールド（field）に何（value）が入力されたのかを
取得し、それに対応するプロパティを更新。
テキストフィールドのvalueはString型なので、それぞれ適したものに型変換。

```kotlin:TestViewModel
    fun updateField(field: String, value: String) {
        val oldData = _uiState.value.testInputData  // TestUiStateインスタンスを取得
        val newData = when (field) {    // 更新したいフィールドに応じてnewDataに値をセット
            "id" -> oldData.copy(id = value)   //idフィールドであればTestUiStateのidを更新
            "name" -> oldData.copy(name = value)
            "age" -> oldData.copy(age = value)
            else -> oldData
        }
        //入力値更新
        _uiState.update {               //TestUiStateを新しいデータで上書き
            it.copy(
                testInputData = newData,
                isValid = newData.id.isNotEmpty() && newData.name.isNotEmpty() && newData.age.isNotEmpty()
            )
        }
    }
```

# 画面（UI）側の設定
テキストフィールドは２つviewModelとやり取りを行わなくてはいけない。
1. **onValueChange**
入力したデータをviewModelのTestUiStateに渡し反映する処理

2. **value**
viewModelのTestUiStateからデータを受け取りテキストフィールドに反映する処理

TestScreenのコンストラクタにviewModelのインスタンスを作ります。
Columnの{}の中にuiStateへの参照を作ります。
.collectAsState()とすることでviewModelのuiStateを監視し、変更があれば画面を更新する処理をしてくれます。

```kotlin:TestScreen
@Composable
fun TestScreen(
    modifier:Modifier = Modifier,
    viewModel: TestViewModel = viewModel(),
) {
    Column(
        //省略
    ) {
        val uiState by viewModel.uiState.collectAsState()
        //省略
```
:::note warn
viewModelのインスタンス作成はColumn{}の中に書いてはいけない。
Columnはコンポーズ関数なので、画面の更新等で再描画される。
その際、viewModelがまた新たに再作成されて、中身がリセットされるから。
▼失敗例💦
```kotlin:TestScreen
@Composable
fun TestScreen(
    modifier:Modifier = Modifier,
) {
    Column(
        //省略
    ) {
        val viewModel:TestViewModel = viewModel()  //←ここに書いちゃだめ
        val uiState by viewModel.uiState.collectAsState()
```
:::

次は、TextFieldのonValueChangeに、viewModelで設定したupdateFieldを設定します。
引数は、fieldへ"id"、"name"、"age"のいずれか、
valueはit（テキストフィールドのvalue）を設定。
これで、テキストフィールドに入力された値で、TestUiStateが更新されます。

更新された値をテキストフィールドに反映されるため、valueにuiState.testInputData.〇〇を設定すればOK！

```kotlin:TestScreen
@Composable
fun TestScreen(
    modifier:Modifier = Modifier,
    viewModel: TestViewModel = viewModel(),
) {
    Column(
        modifier=modifier.padding(16.dp).fillMaxSize(),
        horizontalAlignment = Alignment.CenterHorizontally,
    ) {
        val uiState by viewModel.uiState.collectAsState()

        TextField(
            label= { Text("id") },
            value = uiState.testInputData.id,  //ここを変更
            onValueChange = { viewModel.updateField("id", it) },  //ここを変更
            modifier = Modifier.padding(16.dp)
        )
        //省略
```

# 登録ボタンの設定変更
登録ボタンのenabledプロパティは、trueのときクリックできて、falseのときクリックできないという仕様になっています。
なので、uiStateのisValidの値をそのまま渡してあげれば、３つのテキストフィールドの入力値を参照して、押せたり押せなくなったりという動作ができるようになります。
```Kotlin:TestScreen
@Composable
fun TestScreen(
    modifier:Modifier = Modifier,
    viewModel: TestViewModel = viewModel(),
) {
    Column(
        //省略
    ) {
        val uiState by viewModel.uiState.collectAsState()

        //省略

        Button(
            enabled = uiState.isValid,  //ここ
            onClick = {},
            modifier = Modifier.padding(16.dp)
        ){
            Text(text = "登録")
        }
    }
}
```

# 最後に実行して確認
テキストフィールドに何かを入力して、入力値が反映されたこととボタンの動作を確認します！
viewModelの中で入力値を保持しているので、回転により画面更新されても入力値がリセットされるようなことはないです。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3556392/54908f6f-0499-4e5f-adb4-2107dca84aaa.png)