Duruma dayalı metin alanlarına taşıma

Bu sayfada, değere dayalı TextField'ları duruma dayalı TextField'lara nasıl taşıyabileceğinize dair örnekler verilmektedir. Değere ve duruma dayalı TextField'ler 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 dayalı

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

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

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.
  • onValueChange içinde filtreleme mantığını InputTransformation kullanarak yeniden uygulayın.
  • state öğesini güncellemek için InputTransformation alıcısının kapsamındaki TextFieldBuffer öğesini kullanın.
    • InputTransformation, kullanıcı girişi algılandıktan hemen sonra tam olarak çağrılmalıdır.
    • InputTransformation aracılığıyla TextFieldBuffer önerilen değişiklikler hemen uygulanır. Böylece, yazılım klavyesi ile TextField arasında senkronizasyon sorunu yaşanmaz.

Kredi kartı biçimlendiricisi 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.
  • Kısa çizgi 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 sorumlusunuz.
    • OutputTransformation, ofset eşlemeyi otomatik olarak gerçekleştirir. OutputTransformation.transformOutput alıcı kapsamındaki TextFieldBuffer 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 }
    )
}

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.
  • TextFieldState.setTextAndPlaceCursorAtEnd ile değer atamasını 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, bir düğme, imlecin etrafındaki veya mevcut seçimdeki metni kalınlaştırmak için Markdown süslemeleri ekler. Ayrıca, değişikliklerden sonra seçim konumunu da korur.

  • Değer geri çağırma döngüsünü rememberTextFieldState() ile değiştirin.
  • maxLines = 10 yerine lineLimits = TextFieldLineLimits.MultiLine(maxHeightInLines = 10) koyun.
  • Yeni bir TextFieldValue değerini hesaplama mantığını TextFieldState.edit çağrısıyla 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 kullanılarak mevcut durumu düzenlemenin daha doğal bir yolunu sunar.
    • Seçim, süslemelerin nereye ekleneceğini açıkça tanımlar.
    • Ardından, onValueChange yaklaşımına benzer şekilde seçimi düzenleyin.

ViewModel StateFlow mimarisi

Birçok uygulama, Modern uygulama geliştirme kurallarına uyar. Bu kurallar, 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 kullanılmasını teşvik eder.

Bu tür uygulamalarda, metin girişi içeren bir giriş ekranı gibi formlar genellikle aşağıdaki şekilde 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 yükseltme 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 başlıklı blog yayınında ayrıntılı olarak açıklanmaktadır.

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

Kullanıcı arayüzü bağımlılıklarını ViewModel içine yerleştirmemek yaygın bir öneridir. Bu genellikle iyi bir uygulama olsa da bazen yanlış yorumlanabilir. Bu durum, özellikle yalnızca 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 Snapshot durum sistemi tarafından desteklenen basit durum tutuculardır. StateFlow veya RxJava gibi bağımlılıklardan farklı değildir. Bu nedenle,kodunuzda "ViewModel'de kullanıcı arayüzü bağımlılıkları yok" ilkesini nasıl uyguladığınızı yeniden değerlendirmenizi öneririz. TextFieldState içinde ViewModel referansı tutmak kötü bir uygulama değildir.

username veya password gibi değerleri UiState öğesinden çı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> yerine birkaç TextFieldState değeri girin.
  • Bu TextFieldState nesnelerini LoginForm composable'ında TextFields öğesine iletin.

Uygunluk yaklaşımı

Bu tür mimari değişiklikler her zaman kolay olmayabilir. Bu değişiklikleri yapma özgürlüğünüz olmayabilir veya zaman yatırımı, yeni TextField kullanmanın faydalarından daha fazla olabilir. Bu durumda, eyalete dayalı metin alanlarını küçük bir değişiklikle 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 içine yerleştirip TextFields için doğruluk kaynağı yapmak yerine ViewModel öğesini basit bir veri tutucuya dönüştürün.
    • Bunu yapmak için TextFieldState.text değerlerindeki değişiklikleri gözlemleyin. Bunun için LaunchedEffect içinde snapshotFlow toplayın.
  • ViewModel, kullanıcı arayüzündeki en son değerleri kullanmaya devam eder ancak uiState: StateFlow<UiState>, TextField'ları yönlendirmez.
  • ViewModel içinde uygulanan diğer tüm kalıcılık mantıkları aynı kalabilir.