Halaman ini memberikan contoh cara memigrasikan TextField berbasis nilai ke TextField berbasis status. Lihat halaman Mengonfigurasi kolom teks untuk mengetahui informasi tentang perbedaan antara TextField berbasis nilai dan status.
Penggunaan dasar
Berbasis nilai
@Composable fun OldSimpleTextField() { var state by rememberSaveable { mutableStateOf("") } TextField( value = state, onValueChange = { state = it }, singleLine = true, ) }
Berdasarkan status
@Composable fun NewSimpleTextField() { TextField( state = rememberTextFieldState(), lineLimits = TextFieldLineLimits.SingleLine ) }
- Ganti
value, onValueChange, danremember { mutableStateOf("")} denganrememberTextFieldState(). - Mengganti
singleLine = truedenganlineLimits = TextFieldLineLimits.SingleLine.
Memfilter melalui onValueChange
Berbasis nilai
@Composable fun OldNoLeadingZeroes() { var input by rememberSaveable { mutableStateOf("") } TextField( value = input, onValueChange = { newText -> input = newText.trimStart { it == '0' } } ) }
Berdasarkan status
@Preview @Composable fun NewNoLeadingZeros() { TextField( state = rememberTextFieldState(), inputTransformation = InputTransformation { while (length > 0 && charAt(0) == '0') delete(0, 1) } ) }
- Ganti loop callback nilai dengan
rememberTextFieldState(). - Terapkan kembali logika pemfilteran di
onValueChangemenggunakanInputTransformation. - Gunakan
TextFieldBufferdari cakupan penerimaInputTransformationuntuk memperbaruistate.InputTransformationdipanggil tepat setelah input pengguna terdeteksi.- Perubahan yang diusulkan oleh
InputTransformationmelaluiTextFieldBufferakan segera diterapkan, sehingga menghindari masalah sinkronisasi antara keyboard software danTextField.
Pemformat kartu kredit TextField
Berbasis nilai
@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 } } ) } ) }
Berdasarkan status
@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, "-") }, ) }
- Ganti pemfilteran di
onValueChangedenganInputTransformationuntuk menetapkan panjang maksimum input.- Lihat bagian Memfilter melalui
onValueChange.
- Lihat bagian Memfilter melalui
- Ganti
VisualTransformationdenganOutputTransformationuntuk menambahkan tanda hubung.- Dengan
VisualTransformation, Anda bertanggung jawab untuk membuat teks baru dengan tanda hubung dan menghitung cara indeks dipetakan antara teks visual dan status pendukung. OutputTransformationakan menangani pemetaan offset secara otomatis. Anda hanya perlu menambahkan tanda hubung di tempat yang benar menggunakanTextFieldBufferdari cakupan penerimaOutputTransformation.transformOutput.
- Dengan
Memperbarui status (sederhana)
Berbasis nilai
@Composable fun OldTextFieldStateUpdate(userRepository: UserRepository) { var username by remember { mutableStateOf("") } LaunchedEffect(Unit) { username = userRepository.fetchUsername() } TextField( value = username, onValueChange = { username = it } ) }
Berdasarkan status
@Composable fun NewTextFieldStateUpdate(userRepository: UserRepository) { val usernameState = rememberTextFieldState() LaunchedEffect(Unit) { usernameState.setTextAndPlaceCursorAtEnd(userRepository.fetchUsername()) } TextField(state = usernameState) }
- Ganti loop callback nilai dengan
rememberTextFieldState(). - Ubah penetapan nilai dengan
TextFieldState.setTextAndPlaceCursorAtEnd.
Memperbarui status (kompleks)
Berbasis nilai
@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 ) }
Berdasarkan status
@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) ) }
Dalam kasus penggunaan ini, tombol menambahkan dekorasi Markdown untuk membuat teks tebal di sekitar kursor atau pilihan saat ini. Hal ini juga mempertahankan posisi pilihan setelah perubahan.
- Ganti loop callback nilai dengan
rememberTextFieldState(). - Mengganti
maxLines = 10denganlineLimits = TextFieldLineLimits.MultiLine(maxHeightInLines = 10). - Ubah logika penghitungan
TextFieldValuebaru dengan panggilanTextFieldState.edit.TextFieldValuebaru dibuat dengan menggabungkan teks yang ada berdasarkan pilihan saat ini, dan menyisipkan dekorasi Markdown di antaranya.- Selain itu, pilihan disesuaikan menurut indeks baru teks.
TextFieldState.editmemiliki cara yang lebih alami untuk mengedit status saat ini dengan penggunaanTextFieldBuffer.- Pilihan ini secara eksplisit menentukan tempat untuk menyisipkan dekorasi.
- Kemudian, sesuaikan pilihan, mirip dengan pendekatan
onValueChange.
Arsitektur ViewModel StateFlow
Banyak aplikasi mengikuti Panduan pengembangan aplikasi modern, yang
mendorong penggunaan StateFlow untuk menentukan status UI layar atau komponen
melalui satu class imutable yang membawa semua informasi.
Dalam jenis aplikasi ini, formulir seperti layar Login dengan input teks biasanya didesain sebagai berikut:
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() ) } }
Desain ini sangat cocok dengan TextFields yang menggunakan paradigma pengangkatan status value,
onValueChange. Namun, ada kekurangan yang tidak dapat diprediksi dari pendekatan ini dalam hal input teks. Masalah sinkronisasi mendalam dengan pendekatan ini dijelaskan secara mendetail dalam postingan blog Pengelolaan status yang efektif untuk TextField di Compose.
Masalahnya adalah desain TextFieldState baru tidak kompatibel secara langsung
dengan status UI ViewModel yang didukung StateFlow. Mungkin terlihat aneh untuk mengganti
username: String dan password: String dengan username: TextFieldState dan
password: TextFieldState, karena TextFieldState adalah struktur
data yang pada dasarnya dapat berubah.
Rekomendasi umum adalah menghindari penempatan dependensi UI ke dalam ViewModel.
Meskipun umumnya merupakan praktik yang baik, terkadang hal ini dapat disalahartikan.
Hal ini terutama berlaku untuk dependensi Compose yang murni merupakan struktur
data dan tidak membawa elemen UI apa pun, seperti TextFieldState.
Class seperti MutableState atau TextFieldState adalah holder status sederhana yang
didukung oleh sistem status Snapshot Compose. Tidak ada bedanya dengan
dependensi seperti StateFlow atau RxJava. Oleh karena itu,sebaiknya Anda mengevaluasi kembali cara menerapkan prinsip "tidak ada dependensi UI di ViewModel" dalam kode Anda. Mempertahankan referensi ke TextFieldState dalam ViewModel Anda
bukanlah praktik yang buruk.
Pendekatan sederhana yang direkomendasikan
Sebaiknya ekstrak nilai seperti username atau password dari UiState,
dan simpan referensi terpisah untuk nilai tersebut di 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) } }
- Ganti
MutableStateFlow<UiState>dengan beberapa nilaiTextFieldState. - Teruskan objek
TextFieldStatetersebut keTextFieldsdi composableLoginForm.
Pendekatan yang sesuai
Jenis perubahan arsitektur ini tidak selalu mudah. Anda mungkin tidak memiliki kebebasan untuk melakukan perubahan ini, atau investasi waktu yang diperlukan mungkin lebih besar daripada manfaat penggunaan TextField baru. Dalam hal ini, Anda tetap dapat menggunakan kolom teks berbasis status dengan sedikit penyesuaian.
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) } }
- Pastikan kelas
ViewModeldanUiStateAnda sama. - Daripada mengangkat status langsung ke
ViewModeldan menjadikannya sumber kebenaran untukTextFields, ubahViewModelmenjadi penampung data sederhana.- Untuk melakukannya, amati perubahan pada setiap
TextFieldState.textdengan mengumpulkansnapshotFlowdalamLaunchedEffect.
- Untuk melakukannya, amati perubahan pada setiap
ViewModelAnda akan tetap memiliki nilai terbaru dari UI, tetapiuiState: StateFlow<UiState>-nya tidak akan mendorongTextField.- Logika persistensi lain yang diterapkan di
ViewModelAnda dapat tetap sama.