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

Reimagining text fields in Compose (dcSF 23)

Sponsored · Your Podcast. Everywhere. Effortlessly. Share. Educate. Inspire. Entertain. You do you. We'll handle the rest. →

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/

Avatar for Zach Klippenstein

Zach Klippenstein

June 07, 2023

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