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, ) }
Berbasis 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' } } ) }
Berbasis 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 } } ) } ) }
Berbasis 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 juga 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 } ) }
Berbasis 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 ) }
Berbasis 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. Fungsi 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 pemilihan saat ini, dan menyisipkan dekorasi Markdown di antaranya.- Selain itu, pilihan disesuaikan sesuai dengan indeks teks baru.
TextFieldState.edit
memiliki cara yang lebih alami untuk mengedit status saat ini dengan menggunakanTextFieldBuffer
.- Pilihan menentukan secara eksplisit tempat untuk menyisipkan dekorasi.
- Kemudian, sesuaikan pilihan, mirip dengan pendekatan
onValueChange
.
Arsitektur StateFlow
ViewModel
Banyak aplikasi mengikuti Panduan pengembangan aplikasi modern, yang
mendorong penggunaan StateFlow
untuk menentukan status UI layar atau komponen
melalui satu class yang tidak dapat diubah yang membawa semua informasi.
Dalam jenis aplikasi ini, formulir seperti layar Login dengan input teks biasanya dirancang 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 kelemahan
yang tidak dapat diprediksi dari pendekatan ini terkait 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 dapat diubah secara inheren.
Rekomendasi umum adalah menghindari menempatkan dependensi UI ke dalam ViewModel
.
Meskipun umumnya merupakan praktik yang baik, hal ini terkadang 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. Keduanya tidak berbeda dengan dependensi seperti StateFlow
atau RxJava
. Oleh karena itu,sebaiknya Anda
mengevaluasi ulang cara menerapkan prinsip "tanpa dependensi UI di ViewModel" dalam
kode Anda. Menyimpan referensi ke TextFieldState
dalam ViewModel
Anda
bukanlah praktik yang buruk secara inheren.
Rekomendasi pendekatan sederhana
Sebaiknya ekstrak nilai seperti username
atau password
dari UiState
,
dan simpan referensi terpisah untuknya 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
dalam composableLoginForm
.
Pendekatan yang sesuai
Jenis perubahan arsitektur ini tidak selalu mudah. Anda mungkin tidak memiliki kebebasan untuk melakukan perubahan ini, atau investasi waktu dapat melebihi 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 class
ViewModel
danUiState
Anda tetap sama. - Daripada mengangkat status langsung ke
ViewModel
dan menjadikannya sumber tepercaya untukTextFields
, ubahViewModel
menjadi holder data sederhana.- Untuk melakukannya, amati perubahan pada setiap
TextFieldState.text
dengan mengumpulkansnapshotFlow
diLaunchedEffect
.
- Untuk melakukannya, amati perubahan pada setiap
ViewModel
Anda akan tetap memiliki nilai terbaru dari UI, tetapiuiState: StateFlow<UiState>
-nya tidak akan mendorongTextField
.- Logika persistensi lainnya yang diterapkan di
ViewModel
Anda dapat tetap sama.