Lock in $30 Savings on PRO—Offer Ends Soon! ⏳

Reimagining text fields in Compose (dcSF 23)

Reimagining text fields in Compose (dcSF 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.

Watch the talk here: https://www.droidcon.com/2023/07/20/reimagining-text-fields-in-compose/

Zach Klippenstein

June 07, 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. VisualTransformation/ OffsetMapping state: "s u p e r l a

    t i v e" ^ mapped: "s u p e r⎩c a l i f r a g i⎭l a t i v e" offset: 0 1 2 3 4 4 4 4 4 4 4 4 4 4 5 6 7 8 9 10 cursorOffset = 4 // ????
  3. Professional researcher Pool of participants – Guides plan & goals

    – Executes study – Single-point-of- contact – Collects recordings & surveys – Summarizes results –
  4. Using text in composition var text by remember { mutableStateOf("")

    } … Text("Hello $text!") becomes val state = rememberTextFieldState("") … Text("Hello ${state.text}!")
  5. State hoisting currently class ViewModel { private val textFlow =

    MutableStateFlow("") val text: String @Composable get() = textFlow.collectAsState().value fun updateText(text: String) { textFlow.value = text } }
  6. State hoisting …becomes class ViewModel { val textState = TextFieldState("")

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

    mutableStateOf("") suspend fun run() { snapshotFlow { textState } .collectLateset { processText(it) } } }
  8. Observing changes currently class ViewModel { private val textFlow =

    MutableStateFlow("") suspend fun run() { textFlow.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 val textFlow = MutableStateFlow("")

    fun prefill(text: String) { textFlow.value = prefill } } becomes class ViewModel { val textState = TextFieldState("") fun clearText(text: String) { textState.setTextAndPlaceCursorAtEnd(text) } }
  11. Inserting/deleting text currently text = text .removeRange(2, 6) .replace(10, 10,

    "foo") // insert or text = buildString { append(text) deleteRange(2, 6) insert(10, "foo") }
  12. TextFieldState @Composable fun rememberTextFieldState( initialText: String = "", initialSelection: TextRange

    = … ): 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: CharSequence, Appendable {

    val length: Int var 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) // lots more to come… }
  14. Filtering currently BasicTextField( text, onValueChange = { // Actually a

    _lot_ more complicated than this if (it.isDigitsOnly()) { text = it } } )
  15. Filtering …becomes BasicTextField( text, filter = TextFieldFilter { newText ->

    // Actually a _lot_ more complicated than this newText.replace("""\D""".toRegex(), "") } )
  16. Filtering …or factored out object DigitsOnly : TextFieldFilter { override

    fun filter(text: TextFieldBuffer) { // Still simplified, but now encapsulated text.replace("""\D""".toRegex(), "") } }
  17. Filtering keyboard object DigitsOnly : TextFieldFilter { override val keyboardOptions

    = KeyboardOptions( keyboardType = Number ) override fun filter(text: TextFieldBuffer) { … } }
  18. Line limits BasicTextField( …, singleLine = false, maxLines = 10

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

    state, scrollState = scrollState ) LaunchedEffect(scrollState) { scrollState.scrollTo(…) } Horizontal in SingleLine mode – Vertical in MultiLine mode –
  21. BasicSecureTextField currently BasicTextField( password, onValueChange = { password = it

    }, visualTransformation = PasswordVisualTransformation(), )
  22. BasicSecureTextField currently BasicTextField( password, onValueChange = { password = it

    }, visualTransformation = PasswordVisualTransformation(), modifier = Modifier.semantics { password() }, keyboardOptions = KeyboardOptions( keyboardType = Password ) )
  23. Visual transformations CodepointTransformation – 1:1 object PasswordTransformation : CodepointTransformation {

    override fun transform( codepointIndex: Int, codepoint: Int ): Int = if (showCharAt(codepointIndex)) { codepoint } else { '•'.toCodepoint() } }
  24. Visual transformations ????? – n:m, m > 1 object FancyTransformation

    : ?????Transformation { override fun ?????.transform(text: CharSequence) { // text.insertVisualText(4, "…") } } ?????
  25. Recap Right now: lots of bugs Redesign: Super experimental Available

    in 1.6.x alphas User studies are great! goo.gle/AndroidDevUX