Trang này cung cấp ví dụ về cách bạn có thể di chuyển TextField
dựa trên giá trị sang TextField
dựa trên trạng thái. Hãy xem trang Định cấu hình trường văn bản để biết thông tin về sự khác biệt giữa các TextField
dựa trên giá trị và trạng thái.
Cách sử dụng cơ bản
Dựa trên giá trị
@Composable fun OldSimpleTextField() { var state by rememberSaveable { mutableStateOf("") } TextField( value = state, onValueChange = { state = it }, singleLine = true, ) }
Dựa trên trạng thái
@Composable fun NewSimpleTextField() { TextField( state = rememberTextFieldState(), lineLimits = TextFieldLineLimits.SingleLine ) }
- Thay thế
value, onValueChange
vàremember { mutableStateOf("")
} bằngrememberTextFieldState()
. - Thay thế
singleLine = true
vớilineLimits = TextFieldLineLimits.SingleLine
.
Lọc theo onValueChange
Dựa trên giá trị
@Composable fun OldNoLeadingZeroes() { var input by rememberSaveable { mutableStateOf("") } TextField( value = input, onValueChange = { newText -> input = newText.trimStart { it == '0' } } ) }
Dựa trên trạng thái
@Preview @Composable fun NewNoLeadingZeros() { TextField( state = rememberTextFieldState(), inputTransformation = InputTransformation { while (length > 0 && charAt(0) == '0') delete(0, 1) } ) }
- Thay thế vòng lặp gọi lại giá trị bằng
rememberTextFieldState()
. - Triển khai lại logic lọc trong
onValueChange
bằng cách sử dụngInputTransformation
. - Sử dụng
TextFieldBuffer
từ phạm vi của bộ nhậnInputTransformation
để cập nhậtstate
.InputTransformation
được gọi ngay sau khi phát hiện thấy thao tác nhập của người dùng.- Những thay đổi do
InputTransformation
đề xuất thông quaTextFieldBuffer
sẽ được áp dụng ngay lập tức, tránh được vấn đề đồng bộ hoá giữa bàn phím phần mềm vàTextField
.
Trình định dạng thẻ tín dụng TextField
Dựa trên giá trị
@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 } } ) } ) }
Dựa trên trạng thái
@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, "-") }, ) }
- Thay thế bộ lọc trong
onValueChange
bằngInputTransformation
để đặt độ dài tối đa của dữ liệu đầu vào.- Tham khảo phần Lọc thông qua
onValueChange
.
- Tham khảo phần Lọc thông qua
- Thay thế
VisualTransformation
bằngOutputTransformation
để thêm dấu gạch ngang.- Với
VisualTransformation
, bạn chịu trách nhiệm tạo văn bản mới có dấu gạch ngang, đồng thời tính toán cách ánh xạ các chỉ mục giữa văn bản trực quan và trạng thái hỗ trợ. OutputTransformation
sẽ tự động xử lý việc ánh xạ độ lệch. Bạn chỉ cần thêm dấu gạch ngang vào đúng vị trí bằng cách sử dụngTextFieldBuffer
trong phạm vi nhận củaOutputTransformation.transformOutput
.
- Với
Cập nhật trạng thái (đơn giản)
Dựa trên giá trị
@Composable fun OldTextFieldStateUpdate(userRepository: UserRepository) { var username by remember { mutableStateOf("") } LaunchedEffect(Unit) { username = userRepository.fetchUsername() } TextField( value = username, onValueChange = { username = it } ) }
Dựa trên trạng thái
@Composable fun NewTextFieldStateUpdate(userRepository: UserRepository) { val usernameState = rememberTextFieldState() LaunchedEffect(Unit) { usernameState.setTextAndPlaceCursorAtEnd(userRepository.fetchUsername()) } TextField(state = usernameState) }
- Thay thế vòng lặp gọi lại giá trị bằng
rememberTextFieldState()
. - Thay đổi việc chỉ định giá trị bằng
TextFieldState.setTextAndPlaceCursorAtEnd
.
Cập nhật trạng thái (phức tạp)
Dựa trên giá trị
@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 ) }
Dựa trên trạng thái
@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) ) }
Trong trường hợp sử dụng này, một nút sẽ thêm các thành phần trang trí Markdown để làm cho văn bản trở nên nổi bật xung quanh con trỏ hoặc vùng chọn hiện tại. Thao tác này cũng duy trì vị trí lựa chọn sau khi thay đổi.
- Thay thế vòng lặp gọi lại giá trị bằng
rememberTextFieldState()
. - Thay thế
maxLines = 10
vớilineLimits = TextFieldLineLimits.MultiLine(maxHeightInLines = 10)
. - Thay đổi logic tính toán
TextFieldValue
mới bằng lệnh gọiTextFieldState.edit
.- Một
TextFieldValue
mới được tạo bằng cách nối văn bản hiện có dựa trên lựa chọn hiện tại và chèn các thành phần trang trí Markdown vào giữa. - Ngoài ra, lựa chọn này cũng được điều chỉnh theo chỉ mục mới của văn bản.
TextFieldState.edit
có cách chỉnh sửa trạng thái hiện tại tự nhiên hơn bằng cách sử dụngTextFieldBuffer
.- Lựa chọn này xác định rõ vị trí cần chèn các thành phần trang trí.
- Sau đó, hãy điều chỉnh lựa chọn, tương tự như cách dùng
onValueChange
.
- Một
Cấu trúc StateFlow
ViewModel
Nhiều ứng dụng tuân theo Nguyên tắc phát triển ứng dụng hiện đại, trong đó khuyến khích sử dụng StateFlow
để xác định trạng thái giao diện người dùng của một màn hình hoặc thành phần thông qua một lớp bất biến duy nhất mang tất cả thông tin.
Trong các loại ứng dụng này, một biểu mẫu như màn hình Đăng nhập có dữ liệu đầu vào văn bản thường được thiết kế như sau:
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() ) } }
Thiết kế này hoàn toàn phù hợp với TextFields
sử dụng mô hình chuyển trạng thái lên trên value,
onValueChange
. Tuy nhiên, cách tiếp cận này có những nhược điểm khó lường khi nói đến việc nhập văn bản. Các vấn đề về việc đồng bộ hoá sâu với phương pháp này được giải thích chi tiết trong bài đăng trên blog Quản lý trạng thái hiệu quả cho TextField trong Compose.
Vấn đề là thiết kế TextFieldState
mới không tương thích trực tiếp với trạng thái giao diện người dùng ViewModel được hỗ trợ StateFlow
. Có vẻ như việc thay thế username: String
và password: String
bằng username: TextFieldState
và password: TextFieldState
là điều kỳ lạ, vì TextFieldState
vốn là một cấu trúc dữ liệu có thể thay đổi.
Một đề xuất thường gặp là tránh đặt các phần phụ thuộc giao diện người dùng vào một ViewModel
.
Mặc dù đây thường là một phương pháp hay, nhưng đôi khi phương pháp này có thể bị hiểu sai.
Điều này đặc biệt đúng đối với các phần phụ thuộc Compose chỉ là cấu trúc dữ liệu và không mang theo bất kỳ phần tử nào trên giao diện người dùng, chẳng hạn như TextFieldState
.
Các lớp như MutableState
hoặc TextFieldState
là các phần tử giữ trạng thái đơn giản được hỗ trợ bởi hệ thống trạng thái Snapshot của Compose. Chúng không khác gì các phần phụ thuộc như StateFlow
hoặc RxJava
. Do đó,bạn nên đánh giá lại cách áp dụng nguyên tắc "không có phần phụ thuộc giao diện người dùng trong ViewModel" trong mã của mình. Việc giữ một tham chiếu đến TextFieldState
trong ViewModel
không phải là một cách làm xấu vốn có.
Phương pháp đơn giản được đề xuất
Bạn nên trích xuất các giá trị như username
hoặc password
từ UiState
và giữ một tài liệu tham khảo riêng cho các giá trị đó trong 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) } }
- Thay thế
MutableStateFlow<UiState>
bằng một số giá trịTextFieldState
. - Truyền các đối tượng
TextFieldState
đó đếnTextFields
trong thành phần kết hợpLoginForm
.
Phương pháp tuân thủ
Những loại thay đổi về cấu trúc này không phải lúc nào cũng dễ thực hiện. Bạn có thể không có quyền tự do thực hiện những thay đổi này hoặc thời gian đầu tư có thể lớn hơn lợi ích của việc sử dụng TextField
mới. Trong trường hợp này, bạn vẫn có thể sử dụng các trường văn bản dựa trên trạng thái với một chút điều chỉnh.
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) } }
- Giữ nguyên các lớp
ViewModel
vàUiState
. - Thay vì trực tiếp chuyển trạng thái vào
ViewModel
và biến trạng thái đó thành nguồn đáng tin cậy choTextFields
, hãy biếnViewModel
thành một trình giữ dữ liệu đơn giản.- Để làm việc này, hãy quan sát những thay đổi đối với từng
TextFieldState.text
bằng cách thu thập mộtsnapshotFlow
trongLaunchedEffect
.
- Để làm việc này, hãy quan sát những thay đổi đối với từng
ViewModel
của bạn vẫn sẽ có các giá trị mới nhất từ giao diện người dùng, nhưnguiState: StateFlow<UiState>
của nó sẽ không điều khiển cácTextField
.- Mọi logic duy trì khác được triển khai trong
ViewModel
có thể giữ nguyên.