Slide 1

Slide 1 text

Reimagining text fields in Compose Zach Klippenstein (he/him), Google

Slide 2

Slide 2 text

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

Slide 3

Slide 3 text

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

Slide 4

Slide 4 text

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

Slide 5

Slide 5 text

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

Slide 6

Slide 6 text

It’s got some bugs…

Slide 7

Slide 7 text

Tech debt

Slide 8

Slide 8 text

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

Slide 9

Slide 9 text

State mismanagement

Slide 10

Slide 10 text

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 // ????

Slide 11

Slide 11 text

Back to first principles…

Slide 12

Slide 12 text

week T-42 Plan to plan scoping & estimates

Slide 13

Slide 13 text

week T-18 Project kick-off brainstorming

Slide 14

Slide 14 text

week T-6 User study early validation diverse backgrounds

Slide 15

Slide 15 text

What did we want to learn? fundamentals

Slide 16

Slide 16 text

Prioritize features bare minimum API ✓ State management ✓ Filtering ⨯ User gestures ⨯ Styled text …

Slide 17

Slide 17 text

Professional researcher Pool of participants – Guides plan & goals – Executes study – Single-point-of- contact – Collects recordings & surveys – Summarizes results –

Slide 18

Slide 18 text

Interviews 16 participants 1 hour opening 1 hour closing

Slide 19

Slide 19 text

Take-home project four tasks ~4 hours, total

Slide 20

Slide 20 text

Sign up! goo.gle/AndroidDevUX

Slide 21

Slide 21 text

week T-0 Today iterate

Slide 22

Slide 22 text

week T+? Sneak peek feedback welcome!

Slide 23

Slide 23 text

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

Slide 24

Slide 24 text

BasicTextField v2 BasicTextField( text, onValueChanged = { text = it } ) becomes BasicTextField(state)

Slide 25

Slide 25 text

BasicTextField v2 var text by remember { mutableStateOf("") } becomes val state = rememberTextFieldState("")

Slide 26

Slide 26 text

Using text in composition var text by remember { mutableStateOf("") } … Text("Hello $text!") becomes val state = rememberTextFieldState("") … Text("Hello ${state.text}!")

Slide 27

Slide 27 text

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

Slide 28

Slide 28 text

State hoisting currently class ViewModel { private val textFlow = MutableStateFlow("") val text: String @Composable get() = textFlow.collectAsState().value fun updateText(text: String) { textFlow.value = text } }

Slide 29

Slide 29 text

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

Slide 30

Slide 30 text

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

Slide 31

Slide 31 text

Observing changes currently class ViewModel { private val textFlow = MutableStateFlow("") suspend fun run() { textFlow.collectLatest { processText(it) } } }

Slide 32

Slide 32 text

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

Slide 33

Slide 33 text

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

Slide 34

Slide 34 text

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) } }

Slide 35

Slide 35 text

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") }

Slide 36

Slide 36 text

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

Slide 37

Slide 37 text

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) }

Slide 38

Slide 38 text

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… }

Slide 39

Slide 39 text

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

Slide 40

Slide 40 text

Filtering currently BasicTextField( text, onValueChange = { // Actually a _lot_ more complicated than this if (it.isDigitsOnly()) { text = it } } )

Slide 41

Slide 41 text

Filtering …becomes BasicTextField( text, filter = TextFieldFilter { newText -> // Actually a _lot_ more complicated than this newText.replace("""\D""".toRegex(), "") } )

Slide 42

Slide 42 text

Filtering …or factored out object DigitsOnly : TextFieldFilter { override fun filter(text: TextFieldBuffer) { // Still simplified, but now encapsulated text.replace("""\D""".toRegex(), "") } }

Slide 43

Slide 43 text

Filtering built-in BasicTextField( state, filter = DigitsOnly )

Slide 44

Slide 44 text

Filtering chaining BasicTextField( state, filter = DigitsOnly then maxChars(10) )

Slide 45

Slide 45 text

Filtering keyboard BasicTextField( state, filter = DigitsOnly, keyboardOptions = KeyboardOptions( keyboardType = Number ) )

Slide 46

Slide 46 text

Filtering keyboard object DigitsOnly : TextFieldFilter { override val keyboardOptions = KeyboardOptions( keyboardType = Number ) override fun filter(text: TextFieldBuffer) { … } }

Slide 47

Slide 47 text

Line limits no more nonsense BasicTextField( …, singleLine = true, minLines = 5 )

Slide 48

Slide 48 text

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

Slide 49

Slide 49 text

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

Slide 50

Slide 50 text

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 –

Slide 51

Slide 51 text

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

Slide 52

Slide 52 text

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

Slide 53

Slide 53 text

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

Slide 54

Slide 54 text

BasicSecureTextField …becomes BasicSecureTextField(state) Obfuscated text (customizable) – No cut/copy/drag (paste allowed) – IME password mode – Autofill support –

Slide 55

Slide 55 text

Visual transformations TODO…

Slide 56

Slide 56 text

Visual transformations CodepointTransformation – 1:1 object PasswordTransformation : CodepointTransformation { override fun transform( codepointIndex: Int, codepoint: Int ): Int = if (showCharAt(codepointIndex)) { codepoint } else { '•'.toCodepoint() } }

Slide 57

Slide 57 text

Visual transformations ????? – n:m, m > 1 object FancyTransformation : ?????Transformation { override fun ?????.transform(text: CharSequence) { // text.insertVisualText(4, "…") } } ?????

Slide 58

Slide 58 text

Recap BasicTextField v2 TextFieldState + TextFieldBuffer Filtering LineLimits ScrollState BasicSecureTextField Visual transformations…???

Slide 59

Slide 59 text

One more thing…

Slide 60

Slide 60 text

No content

Slide 61

Slide 61 text

Recap Right now: lots of bugs Redesign: Super experimental Available in 1.6.x alphas User studies are great! goo.gle/AndroidDevUX

Slide 62

Slide 62 text

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