Migracja do pól tekstowych zależnych od stanu

Na tej stronie znajdziesz przykłady migracji TextField na podstawie wartości na TextField na podstawie stanu. Informacje o różnicach między polami TextField opartymi na wartościach a polami TextField oparte na stanie znajdziesz na stronie Konfigurowanie pól tekstowych.

Podstawowe użycie

Na podstawie wartości

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

Na podstawie stanu

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

  • Zamień fragment value, onValueChangeremember { mutableStateOf("") na rememberTextFieldState().
  • Zawartość komórki singleLine = true zastąp komórką lineLimits = TextFieldLineLimits.SingleLine.

Filtrowanie według onValueChange

Na podstawie wartości

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

Na podstawie stanu

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

  • Zastąp pętlę wywołania z wartością przez rememberTextFieldState().
  • Ponownie zaimplementuj logikę filtrowania w funkcji onValueChange, używając funkcji InputTransformation.
  • Aby zaktualizować state, użyj elementu TextFieldBuffer w zakresie odbiornika elementu InputTransformation.
    • InputTransformation jest wywoływany dokładnie po wykryciu interakcji użytkownika.
    • Zmiany zaproponowane przez InputTransformation za pomocą TextFieldBuffer są stosowane natychmiast, co pozwala uniknąć problemów z synchronizacją między klawiaturą programową a TextField.

Formatowanie karty kredytowej TextField

Na podstawie wartości

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

Na podstawie stanu

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

  • Zastąp filtrowanie w elementach onValueChange elementami InputTransformation, aby ustawić maksymalną długość danych wejściowych.
  • Zastąp VisualTransformation kodem OutputTransformation, aby dodać łączniki.
    • W przypadku VisualTransformation jesteś odpowiedzialny za utworzenie nowego tekstu z łącznikami oraz za obliczenie mapowania indeksów między tekstem wizualnym a stanem podrzędnym.
    • OutputTransformation automatycznie zajmuje się mapowaniem przesunięcia. Musisz tylko dodać kreskę w odpowiednich miejscach, używając TextFieldBuffer z zakresu odbiornika OutputTransformation.transformOutput.

Aktualizowanie stanu (proste)

Na podstawie wartości

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

Na podstawie stanu

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

  • Zastąp pętlę wywołania zwrotnego wartości wartością rememberTextFieldState().
  • Zmień przypisanie wartości za pomocą TextFieldState.setTextAndPlaceCursorAtEnd.

Aktualizacja stanu (kompleksowa)

Na podstawie wartości

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

Na podstawie stanu

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

W tym przypadku przycisk dodaje ozdobienia Markdown, aby pogrubić tekst wokół kursora lub bieżącego zaznaczenia. Utrzymuje też pozycję zaznaczenia po wprowadzeniu zmian.

  • Zastąp pętlę wywołania z wartością przez rememberTextFieldState().
  • Zawartość komórki maxLines = 10 zastąp komórką lineLimits = TextFieldLineLimits.MultiLine(maxHeightInLines = 10).
  • Zmień logikę obliczania nowego parametru TextFieldValue za pomocą wywołania funkcji TextFieldState.edit.
    • Nowy element TextFieldValue jest generowany przez złączenie istniejącego tekstu na podstawie bieżącego zaznaczenia i wstawienie w środku znaków specjalnych Markdown.
    • Zaznaczenie jest też dostosowywane zgodnie z nowymi indeksami tekstu.
    • TextFieldState.edit ma bardziej naturalny sposób edycji bieżącego stanu za pomocą funkcji TextFieldBuffer.
    • Zaznaczenie określa wyraźnie, gdzie wstawić dekoracje.
    • Następnie dostosuj wybór w sposób podobny do onValueChange.

Architektura StateFlow ViewModel

Wiele aplikacji stosuje się do wytycznych dotyczących tworzenia nowoczesnych aplikacji, które zalecają używanie StateFlow do definiowania stanu interfejsu użytkownika ekranu lub komponentu za pomocą jednej niezmiennej klasy zawierającej wszystkie informacje.

W takich aplikacjach formularz, np. ekran logowania z pola tekstowymi, jest zwykle zaprojektowany w ten sposób:

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

Ta konstrukcja doskonale pasuje do TextFields, które korzystają z paradygmatu value, onValueChange. W przypadku wprowadzania tekstu takie podejście ma jednak nieprzewidywalne wady. Problemy związane z głęboką synchronizacją w ramach tego podejścia są szczegółowo opisane w poście na blogu Skuteczne zarządzanie stanem w komponencie TextField w komponencie Compose.

Problem polega na tym, że nowy projekt TextFieldState nie jest bezpośrednio zgodny ze stanem interfejsu użytkownika ViewModel z obsługą StateFlow. Zastąpienie username: Stringpassword: String wartościami username: TextFieldStatepassword: TextFieldState może wydawać się dziwne, ponieważ TextFieldState to struktura danych, która może ulegać zmianom.

Zwykle zaleca się unikanie umieszczania zależności interfejsu użytkownika w ViewModel. Chociaż jest to ogólnie dobra praktyka, czasami może być źle interpretowana. Dotyczy to zwłaszcza zależności w komponencie Compose, które są czysto strukturami danych i nie zawierają żadnych elementów interfejsu użytkownika, np. TextFieldState.

Klasy takie jak MutableState lub TextFieldState to proste uchwyty stanu, które są obsługiwane przez system stanu migawek w Compose. Nie różnią się one od zależności takich jak StateFlow czy RxJava. Dlatego zachęcamy do ponownego przeanalizowania sposobu stosowania w kodziku zasady „brak zależności od interfejsu użytkownika w klasie ViewModel”. Nie jest złą praktyką umieszczanie w dokumentach ViewModel odwołań do TextFieldState.

.

Zalecamy wyodrębnienie wartości takich jak username lub password z elementu UiState i utworzenie dla nich osobnej referencji w elementach 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)
    }
}

  • Zastąp MutableStateFlow<UiState> kilkoma wartościami TextFieldState.
  • Przekaż te obiekty TextFieldState do TextFields w komponowalnym elemencie LoginForm.

podejście zgodne z standardami,

Takie zmiany w architekturze nie zawsze są łatwe. Możesz nie mieć możliwości wprowadzenia tych zmian lub czas potrzebny na ich wdrożenie może przewyższać korzyści płynące z użycia nowych TextField. W takim przypadku nadal możesz używać pól tekstowych zależnych od stanu, ale z niewielkimi zmianami.

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

  • Zajęcia ViewModel i UiState pozostaną takie same.
  • Zamiast przenosić stan bezpośrednio do ViewModel i czynić go źródłem prawdy dla TextFields, zrób z ViewModel zwykłego posiadacza danych.
    • Aby to zrobić, obserwuj zmiany w każdym TextFieldState.text, zbierając snapshotFlow w komponencie LaunchedEffect.
  • Twoja ViewModel będzie nadal mieć najnowsze wartości z interfejsu użytkownika, ale jej uiState: StateFlow<UiState> nie będzie sterować TextField.
  • Inna logika trwałości zaimplementowana w Twoim ViewModel może pozostać bez zmian.