Переход на текстовые поля, основанные на состоянии

На этой странице приведены примеры того, как можно перенести TextField s на основе значений в TextField s на основе состояний. Информацию о различиях между TextField s на основе значений и состояний см. на странице «Настройка текстовых полей» .

Основное использование

Ценностно-ориентированный

@Composable
fun OldSimpleTextField() {
    var state by rememberSaveable { mutableStateOf("") }
    TextField(
        value = state,
        onValueChange = { state = it },
        singleLine = true,
    )
}

Государственные

@Composable
fun NewSimpleTextField() {
    TextField(
        state = rememberTextFieldState(),
        lineLimits = TextFieldLineLimits.SingleLine
    )
}

  • Замените value, onValueChange и remember { mutableStateOf("") } на rememberTextFieldState() .
  • Замените singleLine = true на lineLimits = TextFieldLineLimits.SingleLine .

Фильтрация через onValueChange

Ценностно-ориентированный

@Composable
fun OldNoLeadingZeroes() {
    var input by rememberSaveable { mutableStateOf("") }
    TextField(
        value = input,
        onValueChange = { newText ->
            input = newText.trimStart { it == '0' }
        }
    )
}

Государственные

@Preview
@Composable
fun NewNoLeadingZeros() {
    TextField(
        state = rememberTextFieldState(),
        inputTransformation = InputTransformation {
            while (length > 0 && charAt(0) == '0') delete(0, 1)
        }
    )
}

  • Замените цикл обратного вызова значения на rememberTextFieldState() .
  • Повторно реализуйте логику фильтрации в onValueChange с помощью InputTransformation .
  • Используйте TextFieldBuffer из области действия приемника InputTransformation для обновления state .
    • InputTransformation вызывается сразу после обнаружения пользовательского ввода.
    • Изменения, предлагаемые InputTransformation через TextFieldBuffer , применяются немедленно, что позволяет избежать проблем синхронизации между программной клавиатурой и TextField .

Форматировщик кредитных карт TextField

Ценностно-ориентированный

@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
                    }
                }
            )
        }
    )
}

Государственные

@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, "-")
        },
    )
}

  • Замените фильтрацию в onValueChange на InputTransformation , чтобы задать максимальную длину ввода.
  • Замените VisualTransformation на OutputTransformation , чтобы добавить тире.
    • При использовании VisualTransformation вы несете ответственность как за создание нового текста с тире, так и за расчет того, как индексы сопоставляются между визуальным текстом и фоновым состоянием.
    • OutputTransformation автоматически заботится о сопоставлении смещения. Вам просто нужно добавить тире в правильных местах, используя TextFieldBuffer из области действия приемника OutputTransformation.transformOutput .

Обновление состояния (простое)

Ценностно-ориентированный

@Composable
fun OldTextFieldStateUpdate(userRepository: UserRepository) {
    var username by remember { mutableStateOf("") }
    LaunchedEffect(Unit) {
        username = userRepository.fetchUsername()
    }
    TextField(
        value = username,
        onValueChange = { username = it }
    )
}

Государственные

@Composable
fun NewTextFieldStateUpdate(userRepository: UserRepository) {
    val usernameState = rememberTextFieldState()
    LaunchedEffect(Unit) {
        usernameState.setTextAndPlaceCursorAtEnd(userRepository.fetchUsername())
    }
    TextField(state = usernameState)
}

  • Замените цикл обратного вызова значения на rememberTextFieldState() .
  • Измените назначение значения с помощью TextFieldState.setTextAndPlaceCursorAtEnd .

Обновление состояния (сложное)

Ценностно-ориентированный

@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
    )
}

Государственные

@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)
    )
}

В этом варианте использования кнопка добавляет украшения Markdown, чтобы сделать текст жирным вокруг курсора или текущего выделения. Она также сохраняет позицию выделения после изменений.

  • Замените цикл обратного вызова значения на rememberTextFieldState() .
  • Замените maxLines = 10 на lineLimits = TextFieldLineLimits.MultiLine(maxHeightInLines = 10) .
  • Измените логику вычисления нового TextFieldValue с помощью вызова TextFieldState.edit .
    • Новое TextFieldValue генерируется путем объединения существующего текста на основе текущего выделения и вставки между ними декоративных элементов Markdown.
    • Также подборка корректируется в соответствии с новыми индексами текста.
    • TextFieldState.edit имеет более естественный способ редактирования текущего состояния с использованием TextFieldBuffer .
    • Выбор явно определяет, куда вставлять украшения.
    • Затем настройте выбор, аналогично подходу onValueChange .

Архитектура ViewModel StateFlow

Многие приложения следуют рекомендациям по разработке современных приложений , которые рекомендуют использовать StateFlow для определения состояния пользовательского интерфейса экрана или компонента с помощью одного неизменяемого класса, который несет в себе всю информацию.

В подобных приложениях форма, например экран входа в систему с текстовым вводом, обычно проектируется следующим образом:

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()
        )
    }
}

Этот дизайн идеально подходит для TextFields , которые используют парадигму подъема состояния value, onValueChange . Однако у этого подхода есть непредсказуемые недостатки, когда дело доходит до ввода текста. Глубокие проблемы синхронизации с этим подходом подробно описаны в записи блога Effective state management for TextField in Compose .

Проблема в том, что новый дизайн TextFieldState напрямую несовместим с состоянием ViewModel UI, поддерживаемым StateFlow . Может показаться странным заменить username: String и password: String на username: TextFieldState и password: TextFieldState , поскольку TextFieldState по своей сути является изменяемыми структурами данных.

Распространенная рекомендация — избегать размещения зависимостей UI в ViewModel . Хотя это, как правило, хорошая практика, иногда ее можно неправильно истолковать. Это особенно верно для зависимостей Compose, которые являются чисто структурами данных и не несут с собой никаких элементов UI, таких как TextFieldState .

Такие классы, как MutableState или TextFieldState являются простыми держателями состояний, которые поддерживаются системой состояний Compose's Snapshot. Они ничем не отличаются от зависимостей, таких как StateFlow или RxJava . Поэтому мы призываем вас пересмотреть то, как вы применяете принцип «никаких зависимостей UI в ViewModel» в своем коде. Сохранение ссылки на TextFieldState в вашей ViewModel не является изначально плохой практикой.

Мы рекомендуем вам извлекать такие значения, как username или password из UiState и сохранять для них отдельную ссылку в 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)
    }
}

  • Замените MutableStateFlow<UiState> парой значений TextFieldState .
  • Передайте эти объекты TextFieldState в TextFields в компонуемом объекте LoginForm .

Соответствующий подход

Эти типы архитектурных изменений не всегда просты. У вас может не быть свободы вносить эти изменения, или затраты времени могут перевесить преимущества использования новых TextField s. В этом случае вы все еще можете использовать текстовые поля на основе состояния с небольшой настройкой.

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)
    }
}

  • Сохраните классы ViewModel и UiState прежними.
  • Вместо того чтобы помещать состояние непосредственно в ViewModel и делать его источником истины для TextFields , превратите ViewModel в простой держатель данных.
    • Для этого наблюдайте за изменениями в каждом TextFieldState.text , собирая snapshotFlow в LaunchedEffect .
  • Ваша ViewModel по-прежнему будет иметь последние значения из UI, но ее uiState: StateFlow<UiState> не будет управлять TextField .
  • Любая другая логика сохранения, реализованная в вашей ViewModel может оставаться прежней.