本頁面提供範例,說明如何將以值為準的 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()
。 - 以
lineLimits = TextFieldLineLimits.SingleLine
取代singleLine = true
。
透過 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) ) }
在這個用途中,按鈕會加入 Markdown 修飾符號,讓游標或目前選取範圍內的文字以粗體顯示。並在變更後維持選取位置。
- 將值回呼迴圈替換為
rememberTextFieldState()
。 - 以
lineLimits = TextFieldLineLimits.MultiLine(maxHeightInLines = 10)
取代maxLines = 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
,並將其設為TextFields
的真實資訊來源,而是將ViewModel
轉換為簡單的資料容器。- 如要這麼做,請在
LaunchedEffect
中收集snapshotFlow
,觀察每個TextFieldState.text
的變更。
- 如要這麼做,請在
ViewModel
仍會取得 UI 的最新值,但其uiState: StateFlow<UiState>
不會驅動TextField
。ViewModel
中實作的任何其他持久性邏輯都可以保持不變。