🕶️

StateFlowを使ってValidationをかける

2022/10/28に公開

スペースマーケットでAndroidエンジニアやっていますseoです。

最近、複数のフォームを入力する画面をJetpack Composeで作成していたので、その際に学んだことを書き記したいと思います。

アイキャッチ絵文字については、今回Flow/StateFlowについて書くので、ラッパーぽいサングラスにしました。(マイクの方がよかったかな)

はじめに

Jetpack Composeを使っていると、普通のStateとStateFlowって何が異なるの?? 状態変化の観測はStateクラスで良くない??って思っていましたが、今回StateFlowのパンチラインを喰らったので、その記録を残しておきたいと思いました。

State

ご存じのとおり、Jetpack Composeの最も初めに習うStateクラスですね。
状態変化があるComposeに使うクラスです。

@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") }
       )
   }
}

https://developer.android.com/jetpack/compose/state

StateFlow

StateFlowはその名のとおり、StateがFlowとしてやってくるAPIです。(語彙力が小学生並みなのは許してください)

class LatestNewsViewModel(
    private val newsRepository: NewsRepository
) : ViewModel() {
    private val _uiState = MutableStateFlow(LatestNewsUiState.Success(emptyList()))
    val uiState: StateFlow<LatestNewsUiState> = _uiState

    init {
        viewModelScope.launch {
            newsRepository.favoriteLatestNews
                .collect { favoriteNews ->
                    _uiState.value = LatestNewsUiState.Success(favoriteNews)
                }
        }
    }
}

sealed class LatestNewsUiState {
    data class Success(val news: List<ArticleHeadline>): LatestNewsUiState()
    data class Error(val exception: Throwable): LatestNewsUiState()
}

https://developer.android.com/kotlin/flow/stateflow-and-sharedflow

Flowが返ってきて何がうれしいの??という疑問にアンサーしていきます

ユースケース

今回、このようなフォームに文字とレーティングを指定後、送信する画面を作成し、フォームのValidationがtrueならButtonを活性化、falseなら非活性化する実装をしていきたいと思います。

Stateを使う場合

まずは通常のStateを使ってValidateする場合です。

  • ReviewViewModel.kt
private val _rate: MutableState<Float> = mutableStateOf(0f)
val rate: State<Float> = _rate

fun setRate(newValue: Float) {
    _rate.value = newValue
}

private val _text: MutableState<String> = mutableStateOf("")
val text: State<String> = _text

fun setText(newValue: String) {
    _text.value = newValue
}

private val _isEnable: MutableState<Boolean> = mutableStateOf(false)
val isEnable: State<Boolean> = _isEnable

fun validateForm() {
    // rateは星1~5までが有効
    val isValidRate = _rate.value in 1f..5f
    // textの文字数は10~400文字までが有効
    val isValidText = _text.value.length in 10..400
    _isEnable.value = isValidRate && isValidText
}
  • ReviewView.kt
val viewModel = ReviewViewModel()
val rate by viewModel.rate
val text by viewModel.text
val isEnable by viewModel.isEnable

RatingBar(
    value = rate,
    onValueChange = { 
	viewModel.setRate(it)
	viewModel.validateForm()
    }
)

TextField(
    value = text,
    onValueChange = { 
        viewModel.setText(it)
	viewModel.validateForm()
    }
)

Button(
    onClick = {  },
    colors = if (isEnable) ButtonDefaults.textButtonColors(backgroundColor = colorResource(R.color.blue))
    else ButtonDefaults.textButtonColors(backgroundColor = colorResource(R.color.gray)),
    enabled = isEnable
) {
    Text("レビューを投稿")
}

*UIは、RatingBar(星のバー)とTextFieldとButtonのComposeだけを記載します。
また、本筋から外れるため、modifierを省略しているので、コピペしただけでは上記のUIにはならないことご了承ください。

ViewModelにそれぞれのFormの値の状態を持つStateを定義して、onValueChangeのラムダ式で、Buttonが有効か無効かを判定するという方法です。
onValueChangeの度に判定することになるため、onValueChangeが発火する前の値がVlidationを突破しているかどうかがわからないですね。(例えば、前回のレビューがformに自動入力されている場合など)

StateFlowを使った場合

次にStateFlowを使って状態管理・Validation判定する場合です。

  • ReviewViewModel.kt
private val _rate: MutableStateFlow<Float> = MutableStateFlow(0f)
val rate = _rate.asStateFlow()

fun setRate(newValue: Float) {
    _rate.value = newValue
}

private val _text: MutableStateFlow<String> = MutableStateFlow("")
val text = _text.asStateFlow()

fun setText(newValue: String) {
    _text.value = newValue
}

// rateとtextの状態を結合して`isEnable: Flow<Boolean>`を返す
val isEnable = combine(_rate, _text) { rate, text ->
    val isValidRate = rate in 1f..5f
    val isValidText = text.length in 10..400        
    isValidRate && isValidText
}

Flowが返ってくることで、上記のようなcombinemap, filterなどの強力なflow operatorを使って、状態を観測することができます!

  • ReviewView.kt
val viewModel = ReviewViewModel()
val rate by viewModel.rate.collectAsState()
val text by viewModel.text.collectAsState()
// flowをstateへ変換するためには、初期値を設定する必要あり
val isEnable by viewModel.isEnable.collectAsState(initial = false)

RatingBar(
    value = viewModel.rate.value,
    onValueChange = { 
	viewModel.setRate(it)
    }
)

TextField(
    value = viewModel.text.value,
    onValueChange = { 
        viewModel.setText(it)
    }
)

Button(
    onClick = {  },
    colors = if (isEnable) ButtonDefaults.textButtonColors(backgroundColor = colorResource(R.color.blue))
    else ButtonDefaults.textButtonColors(backgroundColor = colorResource(R.color.gray)),
    enabled = isEnable
) {
    Text("レビューを投稿")
}

これで、ratetextの中身に応じて、isEnableが反応してくれるようになりました!

最後に

スペースマーケットでは、パーティー用途から1人用の作業ワークスペースなど、さまざまな用途のスペースが15分単位で借りられます。
また弊社のオフィスも掲載しており、テレワークスペース・会議室などございます。
今日は働く場所を変えてみたいな、と思ったそこのエンジニアのあなた、ぜひアプリから予約して遊びに来てください!

https://www.spacemarket.com/owners/7cx8molou4puj7xp

採用情報はこちら↓

スペースマーケット Engineer Blog

Discussion