Di chuyển sang trường văn bản dựa trên trạng thái

Trang này cung cấp các 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 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, onValueChangeremember { mutableStateOf("") } bằng rememberTextFieldState().
  • Thay thế singleLine = true bằng lineLimits = TextFieldLineLimits.SingleLine.

Lọc thông qua 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 InputTransformation.
  • Sử dụng TextFieldBuffer từ phạm vi trình nhận của InputTransformation để cập nhật state.
    • InputTransformation được gọi ngay sau khi phát hiện thấy hoạt động đầu vào của người dùng.
    • Các thay đổi do InputTransformation đề xuất thông qua TextFieldBuffer sẽ được áp dụng ngay lập tức, tránh 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ế tính năng lọc trong onValueChange bằng InputTransformation để đặt độ dài tối đa của dữ liệu đầu vào.
  • Thay thế VisualTransformation bằng OutputTransformation để 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 các chỉ mục được ánh xạ giữa văn bản trực quan và trạng thái sao lưu.
    • OutputTransformation 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ụng TextFieldBuffer từ phạm vi trình nhận của OutputTransformation.transformOutput.

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 gán 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 trang trí Markdown để làm cho văn bản xung quanh con trỏ hoặc vùng chọn hiện tại trở nên đậm. Nút này cũng duy trì vị trí vùng 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 bằng lineLimits = TextFieldLineLimits.MultiLine(maxHeightInLines = 10).
  • Thay đổi logic tính toán TextFieldValue mới bằng lệnh gọi TextFieldState.edit.
    • TextFieldValue mới được tạo bằng cách ghép văn bản hiện có dựa trên vùng chọn hiện tại và chèn các trang trí Markdown vào giữa.
    • Ngoài ra, vùng chọn được điều chỉnh theo các 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 khi sử dụng TextFieldBuffer.
    • Vùng chọn xác định rõ vị trí chèn các trang trí.
    • Sau đó, điều chỉnh vùng chọn, tương tự như phương pháp onValueChange.

Kiến trúc StateFlow của ViewModel

Nhiều ứng dụng tuân theo Nguyên tắc phát triển ứng dụng hiện đại, 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 một thành phần thông qua một lớp bất biến duy nhất mang theo 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 (state hoisting) value, onValueChange. Tuy nhiên, phương pháp này có những nhược điểm không thể đoán trước khi nhập văn bản. Các vấn đề đồ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 sao lưu StateFlow. Có thể bạn thấy lạ khi thay thế username: Stringpassword: String bằng username: TextFieldStatepassword: TextFieldState, vì TextFieldState vốn là một cấu trúc dữ liệu có thể thay đổi.

Một đề xuất phổ biến là tránh đặt các phần phụ thuộc giao diện người dùng vào ViewModel. Mặc dù đây thường là một phương pháp hay, nhưng đôi khi 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ỳ thành phần giao diện người dùng nào, 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 sao lưu bằng 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 đó,chúng tôi khuyến khích bạ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 phương pháp vốn dĩ không tốt.

Bạn nên trích xuất các giá trị như username hoặc password từ UiState và giữ một tham chiếu 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 vài giá trị TextFieldState.
  • Truyền các đối tượng TextFieldState đó đến TextFields trong thành phần kết hợp LoginForm.

Phương pháp tuân thủ

Những loại thay đổi về kiến 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 ViewModelUiState.
  • Thay vì nâng cấp trạng thái trực tiếp vào ViewModel và biến trạng thái đó thành nguồn dữ liệu đáng tin cậy cho TextFields, hãy biến ViewModel thành một phần tử giữ dữ liệu đơn giản.
    • Để thực hiện việc này, hãy quan sát các thay đổi đối với từng TextFieldState.text bằng cách thu thập snapshotFlow trong LaunchedEffect.
  • 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ưng uiState: StateFlow<UiState> sẽ không điều khiển TextField.
  • Mọi logic duy trì khác được triển khai trong ViewModel đều có thể giữ nguyên.