0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

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

Last updated at Posted at 2025-03-22

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

やりたいこと

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

image.png

画面のコードは以下の通りです。

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しか扱えないため。

TestModel
data class TestInputData(
    val id: String="",
    val name:String="",
    val age: String="",
)

id:Intとしておいて、テキストフィールドに送るときid.toString()と変換するという処理も考えたけど、フィールドをブランクにできずあきらめた:joy:

UIの状態を保持する場所を作る

今度は、実際にテキストフィールドに入力されたデータを保持する場所(testInputData)と
現在の入力値を判定して、3つのテキストフィールドにブランクがなければtrue、
1つでもブランクがあればfalseを設定するもの(isValid)を作ります。

TestUiState
data class TestUiState(
    val testInputData: TestInputData = TestInputData(),
    val isValid: Boolean = false
)

viewModelを作り入力を反映するロジックを追加

2つ変数を作ります。
_uiStateはTestUiStateの変更を監視し、変更があれば画面に反映するというもの。
外部から変更はできない。
uiStateは外部から_uiStateを参照するためにある。

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

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

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)側の設定

テキストフィールドは2つviewModelとやり取りを行わなくてはいけない。

  1. onValueChange
    入力したデータをviewModelのTestUiStateに渡し反映する処理

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

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

TestScreen
@Composable
fun TestScreen(
    modifier:Modifier = Modifier,
    viewModel: TestViewModel = viewModel(),
) {
    Column(
        //省略
    ) {
        val uiState by viewModel.uiState.collectAsState()
        //省略

viewModelのインスタンス作成はColumn{}の中に書いてはいけない。
Columnはコンポーズ関数なので、画面の更新等で再描画される。
その際、viewModelがまた新たに再作成されて、中身がリセットされるから。
▼失敗例💦

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!

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の値をそのまま渡してあげれば、3つのテキストフィールドの入力値を参照して、押せたり押せなくなったりという動作ができるようになります。

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

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?