このページでは、値ベースの TextField
を状態ベースの TextField
に移行する方法の例を示します。値ベースと状態ベースの TextField
の違いについては、テキスト フィールドを構成するをご覧ください。
基本的な使用方法
価値に基づく
@Composable fun OldSimpleTextField() { var state by rememberSaveable { mutableStateOf("") } TextField( value = state, onValueChange = { state = it }, singleLine = true, ) }
状態ベース
@Composable fun NewSimpleTextField() { TextField( state = rememberTextFieldState(), lineLimits = TextFieldLineLimits.SingleLine ) }
value, onValueChange
とremember { mutableStateOf("")
をrememberTextFieldState()
に置き換えます。singleLine = true
をlineLimits = TextFieldLineLimits.SingleLine
に置き換えます。
onValueChange
によるフィルタリング
価値に基づく
@Composable fun OldNoLeadingZeroes() { var input by rememberSaveable { mutableStateOf("") } TextField( value = input, onValueChange = { newText -> input = newText.trimStart { it == '0' } } ) }
状態ベース
@Preview @Composable fun NewNoLeadingZeros() { TextField( state = rememberTextFieldState(), inputTransformation = InputTransformation { while (length > 0 && charAt(0) == '0') delete(0, 1) } ) }
- 値コールバック ループを
rememberTextFieldState()
に置き換えます。 InputTransformation
を使用して、onValueChange
のフィルタリング ロジックを再実装します。InputTransformation
のレシーバー スコープのTextFieldBuffer
を使用して、state
を更新します。InputTransformation
は、ユーザー入力が検出された直後に呼び出されます。InputTransformation
~TextFieldBuffer
で提案された変更はすぐに適用されるため、ソフトウェア キーボードとTextField
の間で同期の問題が発生しません。
クレジット カードのフォーマッタ TextField
価値に基づく
@Composable fun OldTextFieldCreditCardFormatter() { var state by remember { mutableStateOf("") } TextField( value = state, onValueChange = { if (it.length <= 16) state = it }, visualTransformation = VisualTransformation { text -> // Making XXXX-XXXX-XXXX-XXXX string. var out = "" for (i in text.indices) { out += text[i] if (i % 4 == 3 && i != 15) out += "-" } TransformedText( text = AnnotatedString(out), offsetMapping = object : OffsetMapping { override fun originalToTransformed(offset: Int): Int { if (offset <= 3) return offset if (offset <= 7) return offset + 1 if (offset <= 11) return offset + 2 if (offset <= 16) return offset + 3 return 19 } override fun transformedToOriginal(offset: Int): Int { if (offset <= 4) return offset if (offset <= 9) return offset - 1 if (offset <= 14) return offset - 2 if (offset <= 19) return offset - 3 return 16 } } ) } ) }
状態ベース
@Composable fun NewTextFieldCreditCardFormatter() { val state = rememberTextFieldState() TextField( state = state, inputTransformation = InputTransformation.maxLength(16), outputTransformation = OutputTransformation { if (length > 4) insert(4, "-") if (length > 9) insert(9, "-") if (length > 14) insert(14, "-") }, ) }
onValueChange
のフィルタリングをInputTransformation
に置き換えて、入力の最大長を設定します。onValueChange
によるフィルタリングのセクションを参照してください。
VisualTransformation
をOutputTransformation
に置き換えてダッシュを追加します。VisualTransformation
を使用する場合は、ダッシュを含む新しいテキストを作成するだけでなく、ビジュアル テキストとバッキング状態の間でインデックスがどのようにマッピングされるかを計算する必要があります。OutputTransformation
はオフセット マッピングを自動的に処理します。OutputTransformation.transformOutput
のレシーバ スコープのTextFieldBuffer
を使用して、ダッシュを適切な場所に追加するだけです。
状態の更新(簡単)
価値に基づく
@Composable fun OldTextFieldStateUpdate(userRepository: UserRepository) { var username by remember { mutableStateOf("") } LaunchedEffect(Unit) { username = userRepository.fetchUsername() } TextField( value = username, onValueChange = { username = it } ) }
状態ベース
@Composable fun NewTextFieldStateUpdate(userRepository: UserRepository) { val usernameState = rememberTextFieldState() LaunchedEffect(Unit) { usernameState.setTextAndPlaceCursorAtEnd(userRepository.fetchUsername()) } TextField(state = usernameState) }
- 値コールバック ループを
rememberTextFieldState()
に置き換えます。 - 値の割り当てを
TextFieldState.setTextAndPlaceCursorAtEnd
に変更します。
状態の更新(複雑)
価値に基づく
@Composable fun OldTextFieldAddMarkdownEmphasis() { var markdownState by remember { mutableStateOf(TextFieldValue()) } Button(onClick = { // add ** decorations around the current selection, also preserve the selection markdownState = with(markdownState) { copy( text = buildString { append(text.take(selection.min)) append("**") append(text.substring(selection)) append("**") append(text.drop(selection.max)) }, selection = TextRange(selection.min + 2, selection.max + 2) ) } }) { Text("Bold") } TextField( value = markdownState, onValueChange = { markdownState = it }, maxLines = 10 ) }
状態ベース
@Composable fun NewTextFieldAddMarkdownEmphasis() { val markdownState = rememberTextFieldState() LaunchedEffect(Unit) { // add ** decorations around the current selection markdownState.edit { insert(originalSelection.max, "**") insert(originalSelection.min, "**") selection = TextRange(originalSelection.min + 2, originalSelection.max + 2) } } TextField( state = markdownState, lineLimits = TextFieldLineLimits.MultiLine(1, 10) ) }
このユースケースでは、ボタンによってマークダウンの装飾が追加され、カーソルまたは現在の選択範囲のテキストが太字になります。また、変更後も選択位置が保持されます。
- 値コールバック ループを
rememberTextFieldState()
に置き換えます。 maxLines = 10
をlineLimits = TextFieldLineLimits.MultiLine(maxHeightInLines = 10)
に置き換えます。TextFieldState.edit
呼び出しで新しいTextFieldValue
を計算するロジックを変更します。- 新しい
TextFieldValue
は、現在の選択に基づいて既存のテキストをつなぎ合わせ、その間にマークダウンの装飾を挿入することで生成されます。 - また、選択範囲はテキストの新しいインデックスに応じて調整されます。
TextFieldState.edit
では、TextFieldBuffer
を使用して現在の状態をより自然に編集できます。- 選択によって、装飾を挿入する場所が明示的に定義されます。
- 次に、
onValueChange
のアプローチと同様に選択を調整します。
- 新しい
ViewModel の StateFlow
アーキテクチャ
多くのアプリは、最新のアプリ開発ガイドラインに準拠しています。このガイドラインでは、StateFlow
を使用して、すべての情報を保持する単一の不変クラスで画面またはコンポーネントの UI 状態を定義することを推奨しています。
このようなタイプのアプリでは、テキスト入力のあるログイン画面などのフォームは通常、次のように設計されます。
class LoginViewModel : ViewModel() { private val _uiState = MutableStateFlow(UiState()) val uiState: StateFlow<UiState> get() = _uiState.asStateFlow() fun updateUsername(username: String) = _uiState.update { it.copy(username = username) } fun updatePassword(password: String) = _uiState.update { it.copy(password = password) } } data class UiState( val username: String = "", val password: String = "" ) @Composable fun LoginForm( loginViewModel: LoginViewModel, modifier: Modifier = Modifier ) { val uiState by loginViewModel.uiState.collectAsStateWithLifecycle() Column(modifier) { TextField( value = uiState.username, onValueChange = { loginViewModel.updateUsername(it) } ) TextField( value = uiState.password, onValueChange = { loginViewModel.updatePassword(it) }, visualTransformation = PasswordVisualTransformation() ) } }
この設計は、value,
onValueChange
状態のホイスティング パラダイムを使用する TextFields
に完全に適しています。ただし、テキスト入力に関しては、このアプローチには予測できないデメリットがあります。このアプローチの詳細な同期の問題については、Compose での TextField の効果的な状態管理の投稿をご覧ください。
問題は、新しい TextFieldState
設計が、StateFlow
でサポートされている ViewModel UI の状態と直接互換性がないことです。TextFieldState
は本質的に変更可能なデータ構造であるため、username: String
と password: String
を username: TextFieldState
と password: TextFieldState
に置き換えることは奇妙に思えるかもしれません。
一般的な推奨事項は、UI の依存関係を ViewModel
に配置しないことである。これは通常は良い方法ですが、誤解されることもあります。これは、TextFieldState
のように、純粋にデータ構造であり、UI 要素を伴わない Compose 依存関係に特に当てはまります。
MutableState
や TextFieldState
などのクラスは、Compose のスナップショット状態システムを基盤とする単純な状態ホルダーです。StateFlow
や RxJava
などの依存関係と同じです。そのため、コードで「ViewModel に UI 依存関係がない」という原則を適用する方法を再評価することをおすすめします。ViewModel
内に TextFieldState
への参照を保持することは、本質的に悪い方法ではありません。
推奨されるシンプルなアプローチ
username
や password
などの値を UiState
から抽出し、ViewModel
に別個の参照を保持することをおすすめします。
class LoginViewModel : ViewModel() { val usernameState = TextFieldState() val passwordState = TextFieldState() } @Composable fun LoginForm( loginViewModel: LoginViewModel, modifier: Modifier = Modifier ) { Column(modifier) { TextField(state = loginViewModel.usernameState,) SecureTextField(state = loginViewModel.passwordState) } }
MutableStateFlow<UiState>
は、いくつかのTextFieldState
値に置き換えます。- これらの
TextFieldState
オブジェクトをLoginForm
コンポーザブルのTextFields
に渡します。
コンフォームするアプローチ
このようなアーキテクチャの変更は、必ずしも簡単ではありません。このような変更を自由に行うことができない場合や、新しい TextField
を使用するメリットよりも時間の投資の方が大きい場合があります。この場合でも、少し調整すれば状態ベースのテキスト フィールドを使用できます。
class LoginViewModel : ViewModel() { private val _uiState = MutableStateFlow(UiState()) val uiState: StateFlow<UiState> get() = _uiState.asStateFlow() fun updateUsername(username: String) = _uiState.update { it.copy(username = username) } fun updatePassword(password: String) = _uiState.update { it.copy(password = password) } } data class UiState( val username: String = "", val password: String = "" ) @Composable fun LoginForm( loginViewModel: LoginViewModel, modifier: Modifier = Modifier ) { val initialUiState = remember(loginViewModel) { loginViewModel.uiState.value } Column(modifier) { val usernameState = rememberTextFieldState(initialUiState.username) LaunchedEffect(usernameState) { snapshotFlow { usernameState.text.toString() }.collectLatest { loginViewModel.updateUsername(it) } } TextField(usernameState) val passwordState = rememberTextFieldState(initialUiState.password) LaunchedEffect(usernameState) { snapshotFlow { usernameState.text.toString() }.collectLatest { loginViewModel.updatePassword(it) } } SecureTextField(passwordState) } }
ViewModel
クラスとUiState
クラスは同じにします。- 状態を
ViewModel
に直接ホイスティングしてTextFields
の信頼できる情報源にするのではなく、ViewModel
を単純なデータ保持者にします。- これを行うには、
LaunchedEffect
でsnapshotFlow
を収集して、各TextFieldState.text
の変更を観察します。
- これを行うには、
ViewModel
には引き続き UI からの最新の値が含まれますが、そのuiState: StateFlow<UiState>
はTextField
を駆動しません。ViewModel
に実装されている他の永続化ロジックはそのままにできます。