Hey, I am using the state-based `BasicTextField` A...
# compose
v
Hey, I am using the state-based
BasicTextField
API in Jetpack Compose with a custom numeric keypad. I want the
TextFieldState
to contain only the raw digits, while an
OutputTransformation
displays them as a 12-hour time.
For example, the input behaves like this:
Copy code
raw       displayed
"5"    ==   5:00
"53"   ==   5:30
"534"  ==   5:34
"1130" ==  11:30
Invalid input should be rejected. For example, after ""5"", entering ""7"" would produce "5:70", so the state should remain ""5"". I have a small parser that is the source of truth for whether the entered digits represent a valid time:
Copy code
private const val MaxTimeDigits = 4
private const val MinuteDigits = 2

private val HourRange = 1..12
private val MinuteRange = 0..59

private data class TwelveHourTime(
    val hour: Int,
    val minute: Int,
)

private fun String.asTwelveHourTime(): TwelveHourTime? {
    if (
        isEmpty() ||
        length > MaxTimeDigits ||
        !all(Char::isDigit)
    ) {
        return null
    }

    val hourLength = (length - MinuteDigits).coerceAtLeast(1)

    val hour = take(hourLength).toInt()
    val minute = drop(hourLength)
        .padEnd(MinuteDigits, '0')
        .toInt()

    return if (hour in HourRange && minute in MinuteRange) {
        TwelveHourTime(hour, minute)
    } else {
        null
    }
}
I then use the parser from an
InputTransformation:
Copy code
private val timeInputTransformation =
    InputTransformation.byValue { current, proposed ->
        val proposedText = proposed.toString()

        if (
            proposedText.isEmpty() ||
            proposedText.asTwelveHourTime() != null
        ) {
            proposed
        } else {
            current
        }
    }
And use an
OutputTransformation
only for formatting:
Copy code
private val timeOutputTransformation = OutputTransformation {
    val time = asCharSequence()
        .toString()
        .asTwelveHourTime()

    if (time != null) {
        replace(
            0,
            length,
            "${time.hour}:${time.minute.toString().padStart(2, '0')}",
        )
    }
}
The field is then:
Copy code
@Composable
fun TimeField() {
    val state = rememberTextFieldState()

    BasicTextField(
        state = state,
        readOnly = true,
        inputTransformation = timeInputTransformation,
        outputTransformation = timeOutputTransformation,
    )

    // Custom keypad would call this for each digit.
    Button(
        onClick = {
            state.edit {
                append("5")

                with(timeInputTransformation) {
                    transformInput()
                }
            }
        },
    ) {
        Text("5")
    }

    Button(
        onClick = {
            state.edit {
                if (length > 0) {
                    delete(length - 1, length)
                }

                with(timeInputTransformation) {
                    transformInput()
                }
            }
        },
    ) {
        Text("Backspace")
    }
}
The custom keypad changes
TextFieldState
programmatically:
Copy code
state.edit {
    append(digit)

    with(timeInputTransformation) {
        transformInput()
    }
}
Since programmatic changes to
TextFieldState
do not automatically go through the
inputTransformation
passed to
BasicTextField
, I manually apply the same transformation after each keypad edit. I’m trying to keep a single source of truth for validation, so invalid times never remain in
TextFieldState
, while
OutputTransformation
is only responsible for formatting. Is this an idiomatic way to structure this with the state-based Compose text field API? In particular: • Is
InputTransformation
the right place to validate/reject invalid time input? • Is manually calling
transformInput()
after a programmatic TextFieldState.edit appropriate? • Or would it be cleaner to keep the validation logic outside
InputTransformation
and reuse it from both the keypad and the transformation?
c
I wouldn't use a TextField at all in this case and have a simple Text composable. If you write the IME yourself and don't rely on anything platform specific. You also need to be careful with Desktop and Web as with an TextField it will be hard for you to deal with Hardeware keyboards.
👍 1
m
FWIW, also posted here, with two answers so far
v
Thanks I got the answer,