androidkotlinandroid-jetpack-composeandroid-compose-textfield

How to use visual transformation in Basictextfield jetpack compose


I want to apply a visual transformation to a BasicTextField in Jetpack Compose. I've tried some code, but I'm facing several issues. For example, the cursor doesn't stay in the correct position, and when pressing backspace, the cursor moves to the wrong place.

I need the BasicTextField to always display a specific string (e.g., "ABC-") that cannot be removed, even with backspace or clear actions. Additionally, I want to limit the input and apply masking to format the text like this: "ABC-1234-2345-6366".

Could someone help me implement this correctly?

@Preview(showBackground = true)
 @Composable
 private fun SampleTextFieldView() {
     var textFieldValue by remember { mutableStateOf(TextFieldValue("ABC-")) }
     BasicFieldView(
         textFieldValue,
         onValueChange = { onValueChange ->
             val digitOnly = onValueChange.text.filter { it.isDigit() }
             val formattedString = buildString {
                 append("Abc-")
                 digitOnly.forEachIndexed { index, digit ->
                     if (index % 4 == 0 && index !=0 ){
                         append("-")
                     }
                     append(digit)
                 }
             }
             textFieldValue = onValueChange.copy(formattedString)
         }
     )
 }
 
 @Composable
 fun BasicFieldView(
     textFieldValue: TextFieldValue,
     onValueChange: (TextFieldValue) -> Unit,
 ) {
     Column(
         modifier = Modifier.fillMaxSize()
     ) {
         BasicTextField(
             modifier = Modifier.fillMaxWidth(),
             value = textFieldValue,
             onValueChange = {
                 onValueChange(it)
             },
         )
     }
 }

Solution

  • First off, you should use the new BasicTextField overload that uses a TextFieldState instead of a TextFieldValue. That fixes a lot of issues around monitoring updates to the value of the text field and accepts an InputTransformation to modify the data the user entered and an OutputTransformation to modify how the text field's value should be displayed.

    Now, to accomodate your requirements the following is needed:


    Everything put together, your BasicFieldView would look like this:

    @Composable
    fun BasicFieldView(
        state: TextFieldState,
        textStyle: TextStyle = TextStyle.Default,
        prefix: String = "ABC-",
        maxLength: Int = 12,
        groupSize: Int = 4,
        groupDelimiter: String = "-",
    ) {
        Column(
            modifier = Modifier.fillMaxSize(),
        ) {
            BasicTextField(
                state = state,
                modifier = Modifier.fillMaxWidth(),
                inputTransformation = InputTransformation.maxLength(maxLength)
                    .then(DigitsOnlyTransformation),
                textStyle = textStyle,
                outputTransformation = GroupingOutputTransformation(groupSize, groupDelimiter),
                decorator = { innerTextField ->
                    Row {
                        Text(text = prefix, style = textStyle)
                        innerTextField()
                    }
                },
            )
        }
    }
    

    The constants are extracted to parameters with default values so you can easily adapt them when needed.

    As you can see the textFieldValue and onValueChange parameters are now replaced with state. As mentioned at the beginning, TextFieldState is the modern way to use TextFields now (it will also be available for the styled Material 3 TextField and OutlinedTextField starting with the upcoming version 1.4.0 of androidx.compose.material3:material3). With a TextFieldState you don't have an onValueChange callback anymore, that is automatically handled internally.

    Just pass a rememberTextFieldState() to BasicFieldView. Save it in a variable and observe its text property for changes. It is a MutableState, so changes will automatically trigger a recomposition wherever it is used. Alternatively you can use val state = TextFieldState() in your view model. If you want to trigger some action whenever the value changes wrap it in a snapshotFlow:

    snapshotFlow { state.text }
    

    Now the resulting flow can be collected outside of Compose or inside a LaunchedEffect.