Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
package com.squareup.sample.compose.textinput

import androidx.compose.ui.test.ExperimentalTestApi
import androidx.compose.ui.test.assertTextEquals
import androidx.compose.ui.test.hasSetTextAction
import androidx.compose.ui.test.junit4.createAndroidComposeRule
Expand Down Expand Up @@ -30,7 +29,6 @@ class TextInputTest {
.around(composeRule)
.around(IdlingDispatcherRule)

@OptIn(ExperimentalTestApi::class)
@Test
fun allowsTextEditing() {
runBlocking {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ import androidx.compose.ui.unit.dp
import com.squareup.sample.compose.textinput.TextInputWorkflow.Rendering
import com.squareup.workflow1.ui.TextController
import com.squareup.workflow1.ui.compose.ScreenComposableFactory
import com.squareup.workflow1.ui.compose.asMutableState
import com.squareup.workflow1.ui.compose.asMutableTextFieldValueState
import com.squareup.workflow1.ui.compose.tooling.Preview

val TextInputComposableFactory = ScreenComposableFactory<Rendering> { rendering ->
Expand All @@ -30,14 +30,14 @@ val TextInputComposableFactory = ScreenComposableFactory<Rendering> { rendering
.animateContentSize(),
horizontalAlignment = Alignment.CenterHorizontally
) {
var text by rendering.textController.asMutableState()
var textFieldValue by rendering.textController.asMutableTextFieldValueState()

Text(text = text)
Text(text = textFieldValue.text)
OutlinedTextField(
label = {},
placeholder = { Text("Enter some text") },
value = text,
onValueChange = { text = it }
value = textFieldValue,
onValueChange = { textFieldValue = it }
)
Spacer(modifier = Modifier.height(8.dp))
Button(onClick = rendering.onSwapText) {
Expand Down
4 changes: 4 additions & 0 deletions workflow-ui/compose/api/compose.api
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,10 @@ public final class com/squareup/workflow1/ui/compose/TextControllerAsMutableStat
public static final fun asMutableState (Lcom/squareup/workflow1/ui/TextController;Landroidx/compose/runtime/Composer;I)Landroidx/compose/runtime/MutableState;
}

public final class com/squareup/workflow1/ui/compose/TextControllerAsMutableTextFieldValueStateKt {
public static final fun asMutableTextFieldValueState (Lcom/squareup/workflow1/ui/TextController;IILandroidx/compose/runtime/Composer;II)Landroidx/compose/runtime/MutableState;
}

public final class com/squareup/workflow1/ui/compose/ViewEnvironmentWithComposeSupportKt {
public static final fun RootScreen (Lcom/squareup/workflow1/ui/ViewEnvironment;Lcom/squareup/workflow1/ui/Screen;Landroidx/compose/ui/Modifier;Landroidx/compose/runtime/Composer;II)V
public static final fun withComposeInteropSupport (Lcom/squareup/workflow1/ui/ViewEnvironment;Lkotlin/jvm/functions/Function3;)Lcom/squareup/workflow1/ui/ViewEnvironment;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
package com.squareup.workflow1.ui.compose

import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.snapshotFlow
import androidx.compose.ui.text.TextRange
import androidx.compose.ui.text.input.TextFieldValue
import com.squareup.workflow1.ui.TextController
import kotlinx.coroutines.launch

/**
* A wrapper extension for [com.squareup.workflow1.ui.compose.asMutableState] that returns
* [TextFieldValue]. This makes it easy to use it with `MarketTextField` since `MarketTextField`
* expects [TextFieldValue].
*
* @param selectionStart The starting index of the selection.
* @param selectionEnd The ending index of the selection.
*
* If [selectionStart] equals [selectionEnd] then nothing is selected, and the cursor is placed at
* [selectionStart]. By default, the cursor will be placed at the end of the text.
*
* Usage:
*
* var fooText by fooTextController.asMutableTextFieldValueState()
* BasicTextField(
* value = fooText,
* onValueChange = { fooText = it },
* )
*
*/
@Composable
public fun TextController.asMutableTextFieldValueState(
selectionStart: Int = textValue.length,
selectionEnd: Int = selectionStart,
): MutableState<TextFieldValue> {
val textFieldValue = remember(this) {
val actualStart = selectionStart.coerceIn(0, textValue.length)
val actualEnd = selectionEnd.coerceIn(actualStart, textValue.length)
mutableStateOf(
TextFieldValue(
text = textValue,
// We need to set the selection manually when creating new `TextFieldValue` whenever
// `TextController` changes because the text inside may not be empty.
selection = TextRange(actualStart, actualEnd),
)
)
}

LaunchedEffect(this) {
launch {
// This is to address the case when value of `TextController` is updated within the workflow.
// By subscribing directly to `onTextChanged` we can use this to also update the textFieldValue.
onTextChanged
.collect { newText ->
// Only update the `textFieldValue` if the new text is different from the current text.
// This ensures the selection is maintained when the text is updated from the UI side,
// and is only reset when the text is changed via `TextController`.
if (textFieldValue.value.text != newText) {
textFieldValue.value = TextFieldValue(
text = newText,
selection = TextRange(newText.length),
)
}
}
}

// Update this `TextController`'s text whenever the `textFieldValue` changes.
snapshotFlow { textFieldValue.value }
.collect { newText ->
textValue = newText.text
}
}

return textFieldValue
}
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ import kotlinx.coroutines.flow.drop
* function for your UI platform, e.g.:
*
* - `control()` for an Android EditText view
* - `asMutableState()` from an Android `@Composable` function
* - `asMutableState()` or `asMutableTextFieldValueState()` in an Android `@Composable` function
*
* If your workflow needs to access or change the current text value, get the value from [textValue].
* If your workflow needs to react to changes, it can observe [onTextChanged] by converting it to a
Expand Down
Loading