12
7

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

ZOZOAdvent Calendar 2021

Day 23

JetpackComposeのViewModelにおける状態管理を試してみた

Last updated at Posted at 2021-12-22

現在JetpackComposeについて勉強していてViewModelで状態管理する場合はどうするのがいいのか気になったので調べてみました。
なお本記事の内容以外にも方法はたくさんあるので一例として参考程度に見ていただければと思います。

方針

実装の方針として以下2点に重点をおいて調査しました。

  1. 再コンポーズの際に変更のある関数だけが再実行される 1
  2. 状態を1つのクラスで管理する

さきに結論

mutableStateOf を使って状態を管理するクラスをラップすると比較的簡単かと思います。
他に LiveData Flow RxJava2 も利用できるとのことで2、その中でLiveDataだけよく使っていたので試したのですが2つの方針を同時に満たすことが難しそうでした。(最後におまけで記載します)

作成物

今回検証のために作成したアプリはこのようなものです。赤、緑、青をそれぞれ入力して四角の色をそれらの値から生成したRGBで色付けします。

レイアウト

こちらの画面は主に3つのComposable関数から構成されています。(なお再コンポーズが行われるか確認するために全ての関数の最初にログを仕込んでいます)

@Composable
fun ImageShape(modifier: Modifier = Modifier, color: Color) {
    Log.d("compose", "ImageShape")
    Box(
        modifier = modifier
            .size(150.dp)
            .clip(shape = RectangleShape)
            .background(color)
    )
}

四角を作るComposable関数。適応する色は引数でもらいます。


@Composable
fun InputColorField(
    modifier: Modifier = Modifier,
    value: Int,
    label: String,
    changeValue: (Int) -> Unit
) {
    Log.d("compose", "InputColorField $label")
    TextField(
        modifier = modifier
            .width(100.dp),
        value = value.toString(),
        onValueChange = {
            if (it.isNotBlank()) {
                changeValue(it.toInt())
            }
        },
        label = { Text(label) },
        keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number)
    )
}

色ごとの値の入力を受け付けるComposable関数。表示する値は引数でもらい、値の変更があった場合は引数の関数(changeValue)に渡します。

@Composable
fun ImageSampleScreen(uiState: ImageSampleUiState, inputTextField: (Int, Color) -> Unit) {
    Log.d("compose", "ImageSampleScreen")
    Column(
        modifier = Modifier.fillMaxSize(),
        verticalArrangement = Arrangement.Top,
        horizontalAlignment = Alignment.CenterHorizontally,
    ) {
        val color = Color(red = uiState.red, blue = uiState.blue, green = uiState.green)
        ImageShape(Modifier.padding(20.dp), color = color)

        Row() {
            InputColorField(value = uiState.red, label = "Red") {
                inputTextField(it, Color.Red)
            }
            InputColorField(value = uiState.green, label = "Green") {
                inputTextField(it, Color.Green)
            }
            InputColorField(value = uiState.blue, label = "Blue") {
                inputTextField(it, Color.Blue)
            }
        }
    }
}

data class ImageSampleUiState(
    var red: Int,
    var blue: Int,
    var green: Int,
)

画面のレイアウトです。先ほど紹介したComposable関数を配置したものです。状態に関わる情報は全て引数の uiState:ImageSampleUiState からもらいます。また各色の入力欄に値の変更があれば、どの色を、どんな値にしたかを呼び出し元に伝えます。

レイアウトをスクリーンショットと照らし合わせるとこんな感じです
スクリーンショット 2021-12-21 17.14.53.png

ViewModelの実装とComposable関数との連携

class ImageSampleViewModel : ViewModel() {

    var uiState by mutableStateOf(ImageSampleUiState(red = 0, blue = 0, green = 0))
        private set

    fun changeRed(value: Int) {
        uiState = uiState.copy(red = value)
    }

    fun changeGreen(value: Int) {
        uiState = uiState.copy(green = value)
    }

    fun changeBlue(value: Int) {
        uiState = uiState.copy(blue = value)
    }
}

ViewModelには赤と緑と青のそれぞれを変更するメソッドを用意し、copy関数を使って特定の値のみ変更してuiStateを更新します。
ViewModelを扱うActivityは以下のようなコードになります。

class MainActivity : ComponentActivity() {

    val viewModel by viewModels<ImageSampleViewModel>()

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            Surface() {
                MainActivityScreen(viewModel)
            }
        }
    }
}

@Composable
private fun MainActivityScreen(viewModel: ImageSampleViewModel) {
    ImageSampleScreen(uiState = viewModel.uiState, inputTextField = { value, color ->
        when (color) {
            Color.Red -> viewModel.changeRed(value)
            Color.Green -> viewModel.changeGreen(value)
            Color.Blue -> viewModel.changeBlue(value)
            else -> {
            }
        }
    })
}

ImageSampleScreenviewModeluiStateを渡し、入力欄に変更があればviewModelの関数を実行します。

動作結果

赤、緑、青の入力変更にあわせて四角の色が変わります。

ログはこんな感じ
スクリーンショット 2021-12-21 18.33.41.png

ImageSampleScreenとImageShapeは毎回再実行されていますが赤・緑・青の入力欄は変更があった時のみ再実行されるようになっています。

※今回の調査の中でたまに全てのComposableが値の変更にかかわらず必ず再コンポーズされる事象が発生しました。たまたまかもしれませんが Clean Project をしたら解消されたので、もし同様のことが発生した場合は Clean Project をお試しください

まとめと感想

mutableStateOfを活用すれば1つのデータクラスで複数パラメータの状態管理をしても、再コンポーズ時には変更のある値に関連するComposable関数のみ実行されることがわかりました。
思ったより簡単にやりたいことが実現できたのでこれからは非同期処理との連携方法など調べてうまく活用できるようになりたいと思います。

おまけ(LiveDataで実装)

状態を1つのクラスで管理しようとすると、LiveDataの場合全てのComposableが再実行されてしまいました。
どういう実装にしたかというとViewModelは以下のような感じ

class ImageSampleViewModel : ViewModel() {

    private val _uiState = MutableLiveData(ImageSampleUiState(red = 0, blue = 0, green = 0))
    var uiState: LiveData<ImageSampleUiState> = _uiState

    fun changeRed(value: Int) {
        _uiState.value = uiState.value?.copy(red = value)
    }

    fun changeGreen(value: Int) {
        _uiState.value = uiState.value?.copy(green = value)
    }

    fun changeBlue(value: Int) {
        _uiState.value = uiState.value?.copy(blue = value)
    }
}

ActivityではobserveAsStateを利用してStateに変換
この関数はruntime-livedataに入っている拡張関数なのでgradleのdependenciesにも追加します

dependencies {
    implementation "androidx.compose.runtime:runtime-livedata:$compose_version"
}
MainActivity.kt
@Composable
private fun MainActivityScreen(viewModel: ImageSampleViewModel) {
    viewModel.uiState.observeAsState().value?.let {
        ImageSampleScreen(uiState = it) { value, color ->
            when (color) {
                Color.Red -> viewModel.changeRed(value)
                Color.Green -> viewModel.changeGreen(value)
                Color.Blue -> viewModel.changeBlue(value)
                else -> {
                }
            }
        }
    }
}

実行結果は残念なことに特定の色のみを変更しても全てのInputColorFieldが再コンポーズされています
スクリーンショット 2021-12-22 1.34.12.png

おそらくred,green,blueを個別のLiveDataで扱えば余計な再Composeは防げるとは思いますが、Composable関数の方での値の渡し方がそれだと少し面倒になりそうなのとmutableStateOfで目標を達成できたので調査はここまでにしました。

  1. Compose の思想 <再コンポーズ>に関連する記載があります。

  2. 状態と Jetpack Compose <その他のサポートされている状態の種類>に記載されています。

12
7
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
12
7

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?