Upgrade to Pro — share decks privately, control downloads, hide ads and more …

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. State management class ViewModel { val text = MutableStateFlow("") fun

    onTextUpdated(text: String) { // suspending call if (validate(text)) { text.value = text } } }
  2. User study early validation – diverse backgrounds – interviews +

    take-home project – more info in previous talk: – bit.ly/3LnNqrI sign up: – goo.gle/AndroidDevUX
  3. BasicTextField v2 var text by remember { mutableStateOf("") } BasicTextField(

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

    text, onValueChanged = { text = it } ) becomes val state: TextFieldState = rememberTextFieldState("") BasicTextField(state)
  5. BasicTextField v2 val text = rememberTextFieldState("hello") is essentially val text

    = remember(TextFieldState.Saver) { TextFieldState("hello") }
  6. Reading text var text by remember { mutableStateOf("") } …

    Text("Hello $text!") becomes val state = rememberTextFieldState("") … Text("Hello ${state.text}!")
  7. State hoisting …becomes class ViewModel { val textState = TextFieldState("")

    } @Composable fun Screen(viewModel: ViewModel) { TextField(viewModel.textState) }
  8. Observing changes currently class ViewModel { private var textState by

    mutableStateOf("") suspend fun run() { snapshotFlow { textState } .collectLatest { processText(it) } } }
  9. Observing changes becomes class ViewModel { val textState = TextFieldState("")

    suspend fun run() { textState.textAsFlow() .collectLatest { processText(it) } // or textState.forEachTextValue { processText(it) } } }
  10. 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) } }
  11. Inserting/deleting text text = buildString { append(text) deleteRange(2, 6) insert(10,

    "foo") } becomes textState.edit { delete(2, 6) insert(10, "foo") }
  12. TextFieldState @Stable class TextFieldState( initialText: String = "", initialSelection: TextRange

    = … ) { val text: CharSequence val selection: TextRange fun edit(block: TextFieldBuffer.() -> Unit) }
  13. 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… }
  14. Filtering …or factored out object DigitsOnly : InputTransformation { override

    fun TextFieldBuffer.transformInput() { replace("""\D""".toRegex(), "") } }
  15. Filtering keyboard object DigitsOnly : InputTransformation { override val keyboardOptions

    = KeyboardOptions( keyboardType = Number ) override fun TextFieldBuffer.transformInput() { … } }
  16. 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 }
  17. OutputTransformation example BasicTextField2( state, outputTransformation = { if (length >

    0) insert(0, "(") if (length > 4) insert(4, ") ") if (length > 9) insert(9, "-") } )
  18. 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) )
  19. Passwords currently BasicTextField( password, onValueChange = { password = it

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

    }, visualTransformation = PasswordVisualTransformation(), modifier = Modifier.semantics { password() }, keyboardOptions = KeyboardOptions( keyboardType = Password ) )
  21. Line limits BasicTextField( …, singleLine = false, maxLines = 10

    ) becomes BasicTextField( …, lineLimits = MultiLine(maxHeightInLines = 10) )
  22. 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 –
  23. ScrollState val state = rememberTextFieldState("") val scrollState = rememberScrollState() BasicTextField(

    state, scrollState = scrollState ) LaunchedEffect(scrollState) { scrollState.scrollTo(…) } Horizontal in SingleLine mode – Vertical in MultiLine mode –
  24. 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