$30 off During Our Annual Pro Sale. View Details »

Reimagining text fields in Compose (dcNYC 23)

Reimagining text fields in Compose (dcNYC 23)

The Compose Text team is completely rethinking the text field APIs from scratch. Come learn why, how we're approaching the process, and get a sneak peak at what the future might look like.

Zach Klippenstein

September 14, 2023
Tweet

More Decks by Zach Klippenstein

Other Decks in Programming

Transcript

  1. Reimagining
    text fields
    in Compose
    Zach Klippenstein (he/him), Google
    droidcon NYC 2023

    View Slide

  2. refresher:
    TextField
    var text by remember { mutableStateOf("") }
    TextField(
    text,
    onValueChange = { text = it }
    )

    View Slide

  3. refresher:
    BasicTextField
    var text by remember { mutableStateOf("") }
    BasicTextField(
    text,
    onValueChange = { text = it }
    )

    View Slide

  4. refresher:
    BasicTextField
    BasicTextField(
    text,
    onValueChange = { text = it }
    )

    View Slide

  5. refresher:
    BasicTextField
    var text by remember { mutableStateOf("") }

    View Slide

  6. It’s got some
    bugs…

    View Slide

  7. Tech debt

    View Slide

  8. State management
    class ViewModel {
    val text = MutableStateFlow("")
    fun onTextUpdated(text: String) {
    // suspending call
    if (validate(text)) {
    text.value = text
    }
    }
    }

    View Slide

  9. State mismanagement

    View Slide

  10. State (mis)management
    VisualTransformation
    Single/min/max lines
    Detecting individual edits

    View Slide

  11. Back to first principles…

    View Slide

  12. Plan to
    plan
    scoping & estimates

    View Slide

  13. Project
    kick-off
    brainstorming

    View Slide

  14. User study
    early validation

    diverse backgrounds

    interviews + take-home
    project

    more info in previous
    talk:

    bit.ly/3LnNqrI
    sign up:

    goo.gle/AndroidDevUX

    View Slide

  15. New APIs
    feedback requested!

    View Slide

  16. caveat:
    Under construction!
    BasicTextField v2
    State
    Filtering
    Visual transformations
    Passwords
    Line limits
    Scrolling

    View Slide

  17. BasicTextField v2
    var text by remember { mutableStateOf("") }
    BasicTextField(
    text,
    onValueChanged = { text = it }
    )
    becomes
    var text by remember { mutableStateOf("") }
    BasicTextField(
    text,
    onValueChanged = { text = it }
    )

    View Slide

  18. BasicTextField v2
    var text by remember { mutableStateOf("") }
    BasicTextField(
    text,
    onValueChanged = { text = it }
    )
    becomes
    val state: TextFieldState = rememberTextFieldState("")
    BasicTextField(state)

    View Slide

  19. BasicTextField v2
    val text = rememberTextFieldState("hello")
    is essentially
    val text = remember(TextFieldState.Saver) {
    TextFieldState("hello")
    }

    View Slide

  20. Reading text
    var text by remember { mutableStateOf("") }

    Text("Hello $text!")
    becomes
    val state = rememberTextFieldState("")

    Text("Hello ${state.text}!")

    View Slide

  21. State hoisting
    currently
    class ViewModel {
    var textState by mutableStateOf("")
    }

    View Slide

  22. State hoisting
    …becomes
    class ViewModel {
    val textState = TextFieldState("")
    }
    @Composable fun Screen(viewModel: ViewModel) {
    TextField(viewModel.textState)
    }

    View Slide

  23. Observing changes
    currently
    class ViewModel {
    private var textState by mutableStateOf("")
    suspend fun run() {
    snapshotFlow { textState }
    .collectLatest { processText(it) }
    }
    }

    View Slide

  24. Observing changes
    becomes
    class ViewModel {
    val textState = TextFieldState("")
    suspend fun run() {
    textState.textAsFlow()
    .collectLatest { processText(it) }
    // or
    textState.forEachTextValue { processText(it) }
    }
    }

    View Slide

  25. Observing changes
    example: debouncing
    textState.textAsFlow()
    .debounce(500)
    .collectLatest { runSearch(it) }
    // or
    textState.forEachTextValue {
    delay(500)
    runSearch(it)
    }

    View Slide

  26. Setting text
    class ViewModel {
    private var textState by mutableStateOf("")
    fun prefill(text: String) {
    textState = prefill
    }
    }
    becomes
    class ViewModel {
    val textState = TextFieldState("")
    fun prefill(text: String) {
    textState.setTextAndPlaceCursorAtEnd(text)
    }
    }

    View Slide

  27. Inserting/deleting text
    text = buildString {
    append(text)
    deleteRange(2, 6)
    insert(10, "foo")
    }
    becomes
    textState.edit {
    delete(2, 6)
    insert(10, "foo")
    }

    View Slide

  28. TextFieldState
    @Stable
    class TextFieldState(
    initialText: String = "",
    initialSelection: TextRange = …
    ) {
    val text: CharSequence
    val selection: TextRange
    fun edit(block: TextFieldBuffer.() -> Unit)
    }

    View Slide

  29. TextFieldBuffer
    aka “StringBuilder with selection”
    class TextFieldBuffer: Appendable {
    val length: Int
    val selection: TextRange
    fun append(text: CharSequence?)
    fun replace(start: Int, end: Int, text: CharSequence)
    fun insert(index: Int, text: CharSequence)
    fun delete(start: Int, end: Int)
    fun replace(regex: Regex, text: CharSequence)
    fun setSelection(range: TextRange)
    fun placeCursorBeforeCharAt(offset: Int)
    // lots more to come…
    }

    View Slide

  30. State
    Snapshot-backed (@Stable)
    Observe via snapshots or flows
    Edit via mutable buffer
    Hoistable

    View Slide

  31. Filtering
    currently
    BasicTextField(
    text,
    onValueChange = {
    if (it.isDigitsOnly()) {
    text = it
    }
    }
    )

    View Slide

  32. Filtering
    …becomes
    BasicTextField(
    text,
    inputTransformation = {
    replace("""\D""".toRegex(), "")
    }
    )

    View Slide

  33. Filtering
    …or factored out
    object DigitsOnly : InputTransformation {
    override fun TextFieldBuffer.transformInput() {
    replace("""\D""".toRegex(), "")
    }
    }

    View Slide

  34. Filtering
    built-in
    BasicTextField(
    state,
    inputTransformation = InputTransformation.digitsOnly()
    )

    View Slide

  35. Filtering
    chaining
    BasicTextField(
    state,
    inputTransformation = digitsOnly() then maxChars(10)
    )

    View Slide

  36. Filtering
    keyboard
    BasicTextField(
    state,
    inputTransformation = digitsOnly(),
    keyboardOptions = KeyboardOptions(
    keyboardType = Number
    )
    )

    View Slide

  37. Filtering
    keyboard
    object DigitsOnly : InputTransformation {
    override val keyboardOptions = KeyboardOptions(
    keyboardType = Number
    )
    override fun TextFieldBuffer.transformInput() {

    }
    }

    View Slide

  38. InputTransformation
    definition
    fun interface InputTransformation {
    val keyboardOptions: KeyboardOptions? get() = null
    fun TextFieldBuffer.transformInput()
    }

    View Slide

  39. example
    Formatting a phone number

    View Slide

  40. Visual transformations
    currently
    fun interface VisualTransformation {
    fun filter(text: AnnotatedString): TransformedText
    }
    class TransformedText(
    val text: AnnotatedString,
    val offsetMapping: OffsetMapping
    )
    interface OffsetMapping {
    fun originalToTransformed(offset: Int): Int
    fun transformedToOriginal(offset: Int): Int
    }

    View Slide

  41. View Slide

  42. View Slide

  43. View Slide

  44. OutputTransformation
    example
    BasicTextField2(
    state,
    outputTransformation = {
    if (length > 0) insert(0, "(")
    if (length > 4) insert(4, ") ")
    if (length > 9) insert(9, "-")
    }
    )

    View Slide

  45. OutputTransformation
    with InputTransformation
    BasicTextField2(
    state,
    outputTransformation = {
    if (length > 0) insert(0, "(")
    if (length > 4) insert(4, ") ")
    if (length > 9) insert(9, "-")
    },
    inputTransformation = digitsOnly() then maxChars(10)
    )

    View Slide

  46. OutputTransformation
    fun interface OutputTransformation {
    fun TextFieldBuffer.transformOutput()
    }

    View Slide

  47. Passwords
    currently
    BasicTextField(
    password,
    onValueChange = { password = it },
    visualTransformation = PasswordVisualTransformation(),
    )

    View Slide

  48. Passwords
    currently
    BasicTextField(
    password,
    onValueChange = { password = it },
    visualTransformation = PasswordVisualTransformation(),
    modifier = Modifier.semantics { password() },
    keyboardOptions = KeyboardOptions(
    keyboardType = Password
    )
    )

    View Slide

  49. Passwords
    …becomes
    BasicSecureTextField(state)
    Obfuscated text (customizable)

    No cut/copy/drag (paste allowed)

    IME password mode

    Autofill support (planned)

    View Slide

  50. Line limits
    no more nonsense
    BasicTextField(
    …,
    singleLine = true,
    minLines = 5,
    maxLines = 10
    )

    View Slide

  51. Line limits
    BasicTextField(
    …,
    singleLine = true
    )
    becomes
    BasicTextField(
    …,
    lineLimits = SingleLine
    )

    View Slide

  52. Line limits
    BasicTextField(
    …,
    singleLine = false, maxLines = 10
    )
    becomes
    BasicTextField(
    …,
    lineLimits = MultiLine(maxHeightInLines = 10)
    )

    View Slide

  53. Line limits
    SingleLine
    MultiLine(min, max)
    Never wraps

    No newlines

    Fixed height

    Horizontal scrolling

    Wraps long lines

    Newlines allowed

    Height shrinks and grows

    Vertical scrolling

    View Slide

  54. ScrollState
    val state = rememberTextFieldState("")
    val scrollState = rememberScrollState()
    BasicTextField(
    state,
    scrollState = scrollState
    )
    LaunchedEffect(scrollState) {
    scrollState.scrollTo(…)
    }
    Horizontal in SingleLine mode

    Vertical in MultiLine mode

    View Slide

  55. Recap
    BasicTextField2

    TextFieldState +
    TextFieldBuffer

    InputTransformation

    OutputTransformation

    BasicSecureTextField

    LineLimits

    ScrollState

    also…
    User studies are great!
    Sign up:
    Available in 1.6.x alphas
    goo.gle/AndroidDevUX

    View Slide

  56. Contact
    Kotlin Slack: #
    Socials:
    compose
    issuetracker.google.com
    – androiddev.social/@zachklipp
    – twitter.com/@halilozercan
    Slides
    bit.ly/3PboGEk

    View Slide