Duruma dayalı metin alanlarına taşıma

Bu sayfada, değere dayalı TextField'leri eyalete dayalı TextField'lere nasıl taşıyabileceğinizle ilgili örnekler verilmiştir. Değere ve duruma dayalı TextField arasındaki farklar hakkında bilgi için Metin alanlarını yapılandırma sayfasını inceleyin.

Temel kullanım

Değere dayalı

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

Duruma bağlı

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

  • value, onValueChange ve remember { mutableStateOf("") değerlerini rememberTextFieldState() ile değiştirin.
  • singleLine = true yerine lineLimits = TextFieldLineLimits.SingleLine koyun.

onValueChange ile filtreleme

Değere dayalı

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

Eyalet bazlı

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

  • Değer geri çağırma döngüsünü rememberTextFieldState() ile değiştirin.
  • InputTransformation kullanarak onValueChange içinde filtreleme mantığını yeniden uygulayın.
  • state değerini güncellemek için InputTransformation alıcı kapsamındaki TextFieldBuffer değerini kullanın.
    • InputTransformation, kullanıcı girişi algılandıktan hemen sonra çağrılır.
    • InputTransformation tarafından TextFieldBuffer aracılığıyla önerilen değişiklikler hemen uygulanır. Böylece yazılım klavyesi ile TextField arasında senkronizasyon sorunu yaşanmaz.

Kredi kartı biçimlendirici TextField

Değere dayalı

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

Eyalet bazlı

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

  • Girişin maksimum uzunluğunu ayarlamak için onValueChange içindeki filtrelemeyi InputTransformation ile değiştirin.
  • Tire eklemek için VisualTransformation yerine OutputTransformation yazın.
    • VisualTransformation ile hem tireli yeni metni oluşturmaktan hem de dizinlerin görsel metin ile destekleyici durum arasında nasıl eşlendiğini hesaplamaktan siz sorumlu olursunuz.
    • OutputTransformation, ofset eşlemeyi otomatik olarak gerçekleştirir. OutputTransformation.transformOutput alıcı kapsamındaki TextFieldBuffer öğesini kullanarak tireleri doğru yerlere eklemeniz yeterlidir.

Durumu güncelleme (basit)

Değere dayalı

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

Eyalet bazlı

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

  • Değer geri çağırma döngüsünü rememberTextFieldState() ile değiştirin.
  • Değer atamasını TextFieldState.setTextAndPlaceCursorAtEnd ile değiştirin.

Durumu güncelleme (karmaşık)

Değere dayalı

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

Eyalet bazlı

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

Bu kullanım alanında, imlecin veya mevcut seçimin etrafındaki metni kalın yapmak için Markdown süslemeleri ekleyen bir düğme kullanılır. Ayrıca, değişikliklerden sonra seçim konumunu korur.

  • Değer geri çağırma döngüsünü rememberTextFieldState() ile değiştirin.
  • maxLines = 10 yerine lineLimits = TextFieldLineLimits.MultiLine(maxHeightInLines = 10) koyun.
  • TextFieldState.edit çağrısıyla yeni bir TextFieldValue hesaplama mantığını değiştirin.
    • Mevcut metin, geçerli seçime göre birleştirilerek ve araya Markdown süslemeleri eklenerek yeni bir TextFieldValue oluşturulur.
    • Ayrıca seçim, metnin yeni dizinlerine göre ayarlanır.
    • TextFieldState.edit, TextFieldBuffer kullanarak mevcut durumu daha doğal bir şekilde düzenler.
    • Seçim, süslemelerin nereye ekleneceğini açıkça tanımlar.
    • Ardından, onValueChange yaklaşımına benzer şekilde seçimi ayarlayın.

ViewModel StateFlow mimarisi

Birçok uygulama, tüm bilgileri taşıyan tek bir sabit sınıf aracılığıyla bir ekranın veya bileşenin kullanıcı arayüzü durumunu tanımlamak için StateFlow kullanılmasını teşvik eden Modern uygulama geliştirme yönergelerini izler.

Bu tür uygulamalarda, metin girişi içeren giriş ekranı gibi bir form genellikle aşağıdaki gibi tasarlanır:

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

Bu tasarım, value, onValueChange state hoisting paradigmasını kullanan TextFields ile mükemmel uyum sağlar. Ancak metin girişi söz konusu olduğunda bu yaklaşımın öngörülemeyen dezavantajları vardır. Bu yaklaşımla ilgili derin senkronizasyon sorunları, Compose'da TextField için etkili durum yönetimi blog yayınında ayrıntılı olarak açıklanmıştır.

Sorun, yeni TextFieldState tasarımının StateFlow destekli ViewModel kullanıcı arayüzü durumuyla doğrudan uyumlu olmamasıdır. TextFieldState, doğası gereği değiştirilebilir bir veri yapısı olduğundan username: String ve password: String yerine username: TextFieldState ve password: TextFieldState kullanmak garip görünebilir.

Kullanıcı arayüzü bağımlılıklarını ViewModel içine yerleştirmekten kaçınmanız önerilir. Bu genellikle iyi bir uygulama olsa da bazen yanlış yorumlanabilir. Bu durum özellikle tamamen veri yapıları olan ve TextFieldState gibi kullanıcı arayüzü öğeleri içermeyen Compose bağımlılıkları için geçerlidir.

MutableState veya TextFieldState gibi sınıflar, Compose'un anlık görüntü durum sistemi tarafından desteklenen basit durum bilgisi depolayıcılardır. StateFlow veya RxJava gibi bağımlılıklardan farkları yoktur. Bu nedenle, "ViewModel'de kullanıcı arayüzü bağımlılığı olmaması" ilkesini kodunuzda nasıl uyguladığınızı yeniden değerlendirmenizi öneririz. ViewModel içinde bir TextFieldState referansı tutmak, doğası gereği kötü bir uygulama değildir.

UiState içinden username veya password gibi değerleri çıkarmanızı ve ViewModel içinde bunlar için ayrı bir referans tutmanızı öneririz.

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> değerini birkaç TextFieldState değeriyle değiştirin.
  • Bu TextFieldState nesnelerini LoginForm bileşenindeki TextFields öğesine iletin.

Uyumlu yaklaşım

Bu tür mimari değişiklikler her zaman kolay değildir. Bu değişiklikleri yapma özgürlüğünüz olmayabilir veya zaman yatırımı, yeni TextField'leri kullanmanın avantajlarından daha ağır basabilir. Bu durumda, duruma dayalı metin alanlarını küçük bir değişiklikle kullanabilirsiniz.

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(passwordState) {
            snapshotFlow { passwordState.text.toString() }.collectLatest {
                loginViewModel.updatePassword(it)
            }
        }
        SecureTextField(passwordState)
    }
}

  • ViewModel ve UiState sınıflarınızı aynı tutun.
  • Durumu doğrudan ViewModel içine yerleştirip TextFields için doğru kaynak haline getirmek yerine ViewModel öğesini basit bir veri tutucuya dönüştürün.
    • Bunun için her bir TextFieldState.text ile ilgili değişiklikleri gözlemlemek üzere LaunchedEffect içinde snapshotFlow toplayın.
  • ViewModel, kullanıcı arayüzündeki en son değerlere sahip olmaya devam eder ancak uiState: StateFlow<UiState>, TextField'leri yönlendirmez.
  • ViewModel uygulamanızda uygulanan diğer kalıcılık mantıkları aynı kalabilir.