現在JetpackComposeについて勉強していてViewModelで状態管理する場合はどうするのがいいのか気になったので調べてみました。
なお本記事の内容以外にも方法はたくさんあるので一例として参考程度に見ていただければと思います。
方針
実装の方針として以下2点に重点をおいて調査しました。
- 再コンポーズの際に変更のある関数だけが再実行される 1
- 状態を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
からもらいます。また各色の入力欄に値の変更があれば、どの色を、どんな値にしたかを呼び出し元に伝えます。
レイアウトをスクリーンショットと照らし合わせるとこんな感じです
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 -> {
}
}
})
}
ImageSampleScreen
にviewModel
のuiState
を渡し、入力欄に変更があればviewModel
の関数を実行します。
動作結果
赤、緑、青の入力変更にあわせて四角の色が変わります。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"
}
@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が再コンポーズされています
おそらくred,green,blueを個別のLiveDataで扱えば余計な再Composeは防げるとは思いますが、Composable関数の方での値の渡し方がそれだと少し面倒になりそうなのとmutableStateOf
で目標を達成できたので調査はここまでにしました。
-
Compose の思想 <再コンポーズ>に関連する記載があります。 ↩
-
状態と Jetpack Compose <その他のサポートされている状態の種類>に記載されています。 ↩