Duruma dayalı metin alanlarına taşıma

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

Temel kullanım

Değere dayalı

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

Duruma dayalı

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

  • value, onValueChange ve remember { mutableStateOf("") } yerine rememberTextFieldState() yazın.
  • singleLine = true yerine lineLimits = TextFieldLineLimits.SingleLine yazın.

onValueChange üzerinden filtreleme

Değere dayalı

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

Duruma dayalı

@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'te filtreleme mantığını yeniden uygulayın.
  • state öğesini güncellemek için InputTransformation alıcı kapsamındaki TextFieldBuffer öğesini kullanın.
    • InputTransformation, kullanıcı girişi algılandıktan hemen sonra çağrılır.
    • InputTransformation ile TextFieldBuffer arasında ö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
                    }
                }
            )
        }
    )
}

Duruma dayalı

@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 kısa çizgilerle yeni metni oluşturmaktan hem de dizinin görsel metin ile destek durumu arasında nasıl eşleneceğini hesaplamaktan siz sorumlusunuz.
    • OutputTransformation, ofset eşlemesini otomatik olarak yapar. OutputTransformation.transformOutput alıcı kapsamındaki TextFieldBuffer öğesini kullanarak kısa çizgileri 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 }
    )
}

Duruma dayalı

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

Duruma dayalı

@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 geçerli seçimin etrafındaki metni kalın yapmak için Markdown süslemeleri ekleyen bir düğme bulunur. 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) yazın.
  • TextFieldState.edit çağrısıyla yeni bir TextFieldValue hesaplama mantığını değiştirin.
    • Mevcut metin, mevcut seçime göre birleştirilerek ve Markdown süslemeleri arasına yerleştirilerek yeni bir TextFieldValue oluşturulur.
    • Ayrıca seçim, metnin yeni dizine göre ayarlanır.
    • TextFieldState.edit, TextFieldBuffer kullanarak mevcut durumu düzenlemenin daha doğal bir yoluna sahiptir.
    • 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 değişmez sınıf aracılığıyla bir ekranın veya bileşenin kullanıcı arayüzü durumunu tanımlamak için StateFlow kullanmayı teşvik eden modern uygulama geliştirme yönergelerine uyar.

Bu tür uygulamalarda, metin girişi içeren bir giriş ekranı gibi formlar 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 durum kaldırma paradigmasını kullanan TextFields ile mükemmel bir 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ı, Oluşturma bölümündeki 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ğal olarak değişken bir veri yapısı olduğundan username: String ve password: String'yi username: TextFieldState ve password: TextFieldState ile değiştirmek garip görünebilir.

Genel bir öneri, kullanıcı arayüzü bağımlılıkları ViewModel içine yerleştirmekten kaçınmaktır. Bu genellikle iyi bir uygulama olsa da bazen yanlış yorumlanabilir. Bu durum özellikle, tamamen veri yapısı olan ve TextFieldState gibi herhangi bir kullanıcı arayüzü öğesi 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ü durumu sistemi tarafından desteklenen basit durum tutucularıdır. Bunlar, StateFlow veya RxJava gibi bağımlılıklardan farklı değildir. Bu nedenle,kodunuzda "ViewModel'de kullanıcı arayüzü bağımlılığı yok" ilkesini nasıl uyguladığınızı yeniden değerlendirmenizi öneririz. ViewModel içinde TextFieldState referansı bulundurmak kötü bir uygulama değildir.

UiState kaynağından username veya password gibi değerleri ayıklayıp ViewModel kaynağında 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şeninde TextFields'a 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 yeni TextField'leri kullanmanın avantajları, harcayacağınız zamana kıyasla daha az olabilir. Bu durumda, küçük bir değişiklikle duruma dayalı metin alanlarını kullanmaya devam edebilirsiniz.

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 ve UiState sınıflarınızı aynı tutun.
  • Durumu doğrudan ViewModel'e gönderip TextFields için doğruluk kaynağı haline getirmek yerine ViewModel'ü basit bir veri tutucusuna dönüştürün.
    • Bunu yapmak için bir LaunchedEffect içinde snapshotFlow toplayarak her TextFieldState.text'teki değişiklikleri gözlemleyin.
  • ViewModel, kullanıcı arayüzünden gelen en son değerlere sahip olmaya devam eder ancak uiState: StateFlow<UiState>, TextField değerlerini etkilemez.
  • ViewModel uygulamanızda uygulanan diğer tüm kalıcılık mantıkları aynı kalabilir.