このページでは、値ベースの TextField
を状態ベースの 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
を計算するロジックを変更します。- 現在の選択範囲に基づいて既存のテキストをスプライスし、その間に Markdown 装飾を挿入することで、新しい
TextFieldValue
が生成されます。 - また、テキストの新しいインデックスに合わせて選択範囲が調整されます。
TextFieldState.edit
では、TextFieldBuffer
を使用して現在の状態をより自然な方法で編集できます。- 選択範囲は、装飾を挿入する場所を明示的に定義します。
- 次に、
onValueChange
アプローチと同様に選択範囲を調整します。
- 現在の選択範囲に基づいて既存のテキストをスプライスし、その間に Markdown 装飾を挿入することで、新しい
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
への参照を保持することは、本質的に悪いプラクティスではありません。
推奨される簡単なアプローチ
UiState
から username
や password
などの値を抽出し、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
に実装されている他の永続化ロジックは同じままでかまいません。