本頁提供範例,說明如何將以值為基礎的 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
,設定輸入內容長度上限。 - 將
VisualTransformation
替換為OutputTransformation
,即可加入破折號。- 使用
VisualTransformation
時,您必須負責建立含有破折號的新文字,並計算視覺文字和支援狀態之間的索引對應方式。 OutputTransformation
會自動處理位移對應。您只需要在正確位置使用TextFieldBuffer
,從OutputTransformation.transformOutput
的接收器範圍新增破折號即可。
- 使用
更新狀態 (簡單)
以價值為準
@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) ) }
在這個用途中,按鈕會新增 Markdown 裝飾,讓游標或目前選取的文字變成粗體。此外,變更後也會維持選取位置。
- 將值回呼迴圈替換為
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 狀態不直接相容。以 username: TextFieldState
和 password: TextFieldState
取代 username: String
和 password: String
可能看起來很奇怪,因為 TextFieldState
本身就是可變動的資料結構。
一般建議是避免將 UI 依附元件放入 ViewModel
。雖然這通常是不錯的做法,但有時可能會遭到誤解。
如果 Compose 依附元件純粹是資料結構,且不包含任何 UI 元素 (例如 TextFieldState
),就更是如此。
MutableState
或 TextFieldState
等類別是簡單的狀態容器,由 Compose 的 Snapshot 狀態系統支援。這與 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
變成簡單的資料持有者,而不是直接將狀態提升至ViewModel
,並將其做為TextFields
的真實資訊來源。- 如要這麼做,請在
LaunchedEffect
中收集snapshotFlow
,觀察每個TextFieldState.text
的變化。
- 如要這麼做,請在
- 您的
ViewModel
仍會從 UI 取得最新值,但其uiState: StateFlow<UiState>
不會驅動TextField
。 ViewModel
中實作的任何其他持續性邏輯都將維持不變。