StateFlowを使ってValidationをかける
スペースマーケットで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") }
)
}
}
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()
}
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が返ってくることで、上記のようなcombine
やmap
, 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("レビューを投稿")
}
これで、rate
とtext
の中身に応じて、isEnable
が反応してくれるようになりました!
最後に
スペースマーケットでは、パーティー用途から1人用の作業ワークスペースなど、さまざまな用途のスペースが15分単位で借りられます。
また弊社のオフィスも掲載しており、テレワークスペース・会議室などございます。
今日は働く場所を変えてみたいな、と思ったそこのエンジニアのあなた、ぜひアプリから予約して遊びに来てください!
採用情報はこちら↓
スペースを簡単に貸し借りできるサービス「スペースマーケット」のエンジニアによる公式ブログです。 弊社採用技術スタックはこちら -> whatweuse.dev/company/spacemarket
Discussion