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 = true
denganlineLimits = 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
onValueChange
menggunakanInputTransformation
. - Gunakan
TextFieldBuffer
dari cakupan penerimaInputTransformation
untuk memperbaruistate
.InputTransformation
dipanggil tepat setelah input pengguna terdeteksi.- Perubahan yang diusulkan oleh
InputTransformation
melaluiTextFieldBuffer
akan 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
onValueChange
denganInputTransformation
untuk menetapkan panjang maksimum input.- Lihat bagian Memfilter melalui
onValueChange
.
- Lihat bagian Memfilter melalui
- Ganti
VisualTransformation
denganOutputTransformation
untuk 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. OutputTransformation
menangani pemetaan offset secara otomatis. Anda hanya perlu menambahkan tanda hubung di tempat yang benar menggunakanTextFieldBuffer
dari 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 = 10
denganlineLimits = TextFieldLineLimits.MultiLine(maxHeightInLines = 10)
. - Ubah logika penghitungan
TextFieldValue
baru dengan panggilanTextFieldState.edit
.TextFieldValue
baru 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.edit
memiliki 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 Pedoman 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
TextFieldState
tersebut keTextFields
di 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
s 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) } }
- Pertahankan kelas
ViewModel
danUiState
Anda tetap sama. - Daripada mengangkat status langsung ke
ViewModel
dan menjadikannya sumber kebenaran untukTextFields
, ubahViewModel
menjadi penampung data sederhana.- Untuk melakukannya, amati perubahan pada setiap
TextFieldState.text
dengan mengumpulkansnapshotFlow
dalamLaunchedEffect
.
- Untuk melakukannya, amati perubahan pada setiap
ViewModel
Anda akan tetap memiliki nilai terbaru dari UI, tetapiuiState: StateFlow<UiState>
-nya tidak akan mendorongTextField
.- Logika persistensi lain yang diterapkan di
ViewModel
Anda dapat tetap sama.