Bermigrasi ke kolom teks berbasis status

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, dan remember { mutableStateOf("") } dengan rememberTextFieldState().
  • Mengganti singleLine = true dengan lineLimits = 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 menggunakan InputTransformation.
  • Gunakan TextFieldBuffer dari cakupan penerima InputTransformation untuk memperbarui state.
    • InputTransformation dipanggil tepat setelah input pengguna terdeteksi.
    • Perubahan yang diusulkan oleh InputTransformation melalui TextFieldBuffer akan segera diterapkan, sehingga menghindari masalah sinkronisasi antara keyboard software dan TextField.

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 dengan InputTransformation untuk menetapkan panjang maksimum input.
  • Ganti VisualTransformation dengan OutputTransformation 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 menggunakan TextFieldBuffer dari cakupan penerima OutputTransformation.transformOutput.

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 dengan lineLimits = TextFieldLineLimits.MultiLine(maxHeightInLines = 10).
  • Ubah logika penghitungan TextFieldValue baru dengan panggilan TextFieldState.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 menggunakan TextFieldBuffer.
    • 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.

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 nilai TextFieldState.
  • Teruskan objek TextFieldState tersebut ke TextFields dalam composable LoginForm.

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 dan UiState Anda tetap sama.
  • Daripada mengangkat status langsung ke ViewModel dan menjadikannya sumber tepercaya untuk TextFields, ubah ViewModel menjadi holder data sederhana.
    • Untuk melakukannya, amati perubahan pada setiap TextFieldState.text dengan mengumpulkan snapshotFlow di LaunchedEffect.
  • ViewModel Anda akan tetap memiliki nilai terbaru dari UI, tetapi uiState: StateFlow<UiState>-nya tidak akan mendorong TextField.
  • Logika persistensi lainnya yang diterapkan di ViewModel Anda dapat tetap sama.