Skip to content

Commit 725ae31

Browse files
committed
wip: Introduces TextController.asMutableTextFieldValueState.
1 parent 7ecc764 commit 725ae31

File tree

4 files changed

+84
-7
lines changed

4 files changed

+84
-7
lines changed

samples/compose-samples/src/androidTest/java/com/squareup/sample/compose/textinput/TextInputTest.kt

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
package com.squareup.sample.compose.textinput
22

3-
import androidx.compose.ui.test.ExperimentalTestApi
43
import androidx.compose.ui.test.assertTextEquals
54
import androidx.compose.ui.test.hasSetTextAction
65
import androidx.compose.ui.test.junit4.createAndroidComposeRule
@@ -30,7 +29,6 @@ class TextInputTest {
3029
.around(composeRule)
3130
.around(IdlingDispatcherRule)
3231

33-
@OptIn(ExperimentalTestApi::class)
3432
@Test
3533
fun allowsTextEditing() {
3634
runBlocking {

samples/compose-samples/src/main/java/com/squareup/sample/compose/textinput/TextInputViewFactory.kt

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import com.squareup.sample.compose.textinput.TextInputWorkflow.Rendering
2020
import com.squareup.workflow1.ui.TextController
2121
import com.squareup.workflow1.ui.compose.ScreenComposableFactory
2222
import com.squareup.workflow1.ui.compose.asMutableState
23+
import com.squareup.workflow1.ui.compose.asMutableTextFieldValueState
2324
import com.squareup.workflow1.ui.compose.tooling.Preview
2425

2526
val TextInputComposableFactory = ScreenComposableFactory<Rendering> { rendering ->
@@ -30,14 +31,14 @@ val TextInputComposableFactory = ScreenComposableFactory<Rendering> { rendering
3031
.animateContentSize(),
3132
horizontalAlignment = Alignment.CenterHorizontally
3233
) {
33-
var text by rendering.textController.asMutableState()
34+
var textFieldValue by rendering.textController.asMutableTextFieldValueState()
3435

35-
Text(text = text)
36+
Text(text = textFieldValue.text)
3637
OutlinedTextField(
3738
label = {},
3839
placeholder = { Text("Enter some text") },
39-
value = text,
40-
onValueChange = { text = it }
40+
value = textFieldValue,
41+
onValueChange = { textFieldValue = it }
4142
)
4243
Spacer(modifier = Modifier.height(8.dp))
4344
Button(onClick = rendering.onSwapText) {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
package com.squareup.workflow1.ui.compose
2+
3+
import androidx.compose.runtime.Composable
4+
import androidx.compose.runtime.LaunchedEffect
5+
import androidx.compose.runtime.MutableState
6+
import androidx.compose.runtime.mutableStateOf
7+
import androidx.compose.runtime.remember
8+
import androidx.compose.runtime.snapshotFlow
9+
import androidx.compose.ui.text.TextRange
10+
import androidx.compose.ui.text.input.TextFieldValue
11+
import com.squareup.workflow1.ui.TextController
12+
import kotlinx.coroutines.launch
13+
14+
/**
15+
* A wrapper extension for [com.squareup.workflow1.ui.compose.asMutableState] that returns
16+
* [TextFieldValue]. This makes it easy to use it with `MarketTextField` since `MarketTextField`
17+
* expects [TextFieldValue].
18+
*
19+
* @param selectionStart The starting index of the selection.
20+
* @param selectionEnd The ending index of the selection.
21+
*
22+
* If [selectionStart] equals [selectionEnd] then nothing is selected, and the cursor is placed at
23+
* [selectionStart]. By default, the cursor will be placed at the end of the text.
24+
*
25+
* Usage:
26+
*
27+
* var fooText by fooTextController.asMutableTextFieldValueState()
28+
* BasicTextField(
29+
* value = fooText,
30+
* onValueChange = { fooText = it },
31+
* )
32+
*
33+
*/
34+
@Composable
35+
public fun TextController.asMutableTextFieldValueState(
36+
selectionStart: Int = textValue.length,
37+
selectionEnd: Int = selectionStart,
38+
): MutableState<TextFieldValue> {
39+
val textFieldValue = remember(this) {
40+
val actualStart = selectionStart.coerceIn(0, textValue.length)
41+
val actualEnd = selectionEnd.coerceIn(actualStart, textValue.length)
42+
mutableStateOf(
43+
TextFieldValue(
44+
text = textValue,
45+
// We need to set the selection manually when creating new `TextFieldValue` whenever
46+
// `TextController` changes because the text inside may not be empty.
47+
selection = TextRange(actualStart, actualEnd),
48+
)
49+
)
50+
}
51+
52+
LaunchedEffect(this) {
53+
launch {
54+
// This is to address the case when value of `TextController` is updated within the workflow.
55+
// By subscribing directly to `onTextChanged` we can use this to also update the textFieldValue.
56+
onTextChanged
57+
.collect { newText ->
58+
// Only update the `textFieldValue` if the new text is different from the current text.
59+
// This ensures the selection is maintained when the text is updated from the UI side,
60+
// and is only reset when the text is changed via `TextController`.
61+
if (textFieldValue.value.text != newText) {
62+
textFieldValue.value = TextFieldValue(
63+
text = newText,
64+
selection = TextRange(newText.length),
65+
)
66+
}
67+
}
68+
}
69+
70+
// Update this `TextController`'s text whenever the `textFieldValue` changes.
71+
snapshotFlow { textFieldValue.value }
72+
.collect { newText ->
73+
textValue = newText.text
74+
}
75+
}
76+
77+
return textFieldValue
78+
}

workflow-ui/core-common/src/main/java/com/squareup/workflow1/ui/TextController.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ import kotlinx.coroutines.flow.drop
2626
* function for your UI platform, e.g.:
2727
*
2828
* - `control()` for an Android EditText view
29-
* - `asMutableState()` from an Android `@Composable` function
29+
* - `asMutableState()` or `asMutableTextFieldValueState()` in an Android `@Composable` function
3030
*
3131
* If your workflow needs to access or change the current text value, get the value from [textValue].
3232
* If your workflow needs to react to changes, it can observe [onTextChanged] by converting it to a

0 commit comments

Comments
 (0)