Skip to content

Commit aa12b64

Browse files
Add an extension on TextController that returns a mutable state of TextFieldValue
This makes it easy to use it with text fields while persisting selection state.
1 parent 030bc23 commit aa12b64

File tree

1 file changed

+65
-0
lines changed

1 file changed

+65
-0
lines changed

workflow-ui/compose/src/main/java/com/squareup/workflow1/ui/compose/TextControllerAsMutableState.kt

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ import androidx.compose.runtime.MutableState
66
import androidx.compose.runtime.mutableStateOf
77
import androidx.compose.runtime.remember
88
import androidx.compose.runtime.snapshotFlow
9+
import androidx.compose.ui.text.TextRange
10+
import androidx.compose.ui.text.input.TextFieldValue
911
import com.squareup.workflow1.ui.TextController
1012
import kotlinx.coroutines.launch
1113

@@ -42,3 +44,66 @@ import kotlinx.coroutines.launch
4244
}
4345
}
4446
}
47+
48+
/**
49+
* Exposes the [textValue][TextController.textValue] of a [TextController]
50+
* as a remembered [MutableState] of [TextFieldValue], suitable for use from `@Composable`
51+
* functions.
52+
*
53+
* Usage:
54+
*
55+
* ```
56+
* var fooText by fooTextController.asMutableTextFieldValueState()
57+
* BasicTextField(
58+
* value = fooText,
59+
* onValueChange = { fooText = it },
60+
* )
61+
* ```
62+
*
63+
* @param initialSelection The initial range of selection. If [TextRange.start] equals
64+
* [TextRange.end], then nothing is selected, and the cursor is placed at
65+
* [TextRange.start]. By default, the cursor will be placed at the end of the text.
66+
*/
67+
@Composable public fun TextController.asMutableTextFieldValueState(
68+
initialSelection: TextRange = TextRange(textValue.length),
69+
): MutableState<TextFieldValue> {
70+
val textFieldValue = remember(this) {
71+
val actualStart = initialSelection.start.coerceIn(0, textValue.length)
72+
val actualEnd = initialSelection.end.coerceIn(actualStart, textValue.length)
73+
mutableStateOf(
74+
TextFieldValue(
75+
text = textValue,
76+
// We need to set the selection manually when creating new `TextFieldValue` whenever
77+
// `TextController` changes because the text inside may not be empty.
78+
selection = TextRange(actualStart, actualEnd),
79+
)
80+
)
81+
}
82+
83+
LaunchedEffect(this) {
84+
launch {
85+
// This is to address the case when value of `TextController` is updated within the workflow.
86+
// By subscribing directly to `onTextChanged` we can use this to also update the textFieldValue.
87+
onTextChanged
88+
.collect { newText ->
89+
// Only update the `textFieldValue` if the new text is different from the current text.
90+
// This ensures the selection is maintained when the text is updated from the UI side,
91+
// and is only reset when the text is changed via `TextController`.
92+
if (textFieldValue.value.text != newText) {
93+
textFieldValue.value = TextFieldValue(
94+
text = newText,
95+
selection = TextRange(newText.length),
96+
)
97+
}
98+
}
99+
}
100+
101+
// Update this `TextController`'s text whenever the `textFieldValue` changes.
102+
snapshotFlow { textFieldValue.value }
103+
.collect { newText ->
104+
textValue = newText.text
105+
}
106+
}
107+
108+
return textFieldValue
109+
}

0 commit comments

Comments
 (0)