Skip to content

Commit 1dfeb59

Browse files
authored
Merge pull request square#557 from square/ray/compose-textcontroller
Introduces TextController.asMutableState.
2 parents 314f84c + 270d11f commit 1dfeb59

File tree

4 files changed

+69
-19
lines changed

4 files changed

+69
-19
lines changed

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

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,12 +10,16 @@ import androidx.compose.material.Button
1010
import androidx.compose.material.OutlinedTextField
1111
import androidx.compose.material.Text
1212
import androidx.compose.runtime.Composable
13+
import androidx.compose.runtime.getValue
14+
import androidx.compose.runtime.setValue
1315
import androidx.compose.ui.Alignment
1416
import androidx.compose.ui.Modifier
1517
import androidx.compose.ui.tooling.preview.Preview
1618
import androidx.compose.ui.unit.dp
1719
import com.squareup.sample.compose.textinput.TextInputWorkflow.Rendering
20+
import com.squareup.workflow1.ui.TextController
1821
import com.squareup.workflow1.ui.WorkflowUiExperimentalApi
22+
import com.squareup.workflow1.ui.compose.asMutableState
1923
import com.squareup.workflow1.ui.compose.composeViewFactory
2024
import com.squareup.workflow1.ui.compose.tooling.Preview
2125

@@ -28,12 +32,14 @@ val TextInputViewFactory = composeViewFactory<Rendering> { rendering, _ ->
2832
.animateContentSize(),
2933
horizontalAlignment = Alignment.CenterHorizontally
3034
) {
31-
Text(text = rendering.text)
35+
var text by rendering.textController.asMutableState()
36+
37+
Text(text = text)
3238
OutlinedTextField(
3339
label = {},
3440
placeholder = { Text("Enter some text") },
35-
value = rendering.text,
36-
onValueChange = rendering.onTextChanged
41+
value = text,
42+
onValueChange = { text = it }
3743
)
3844
Spacer(modifier = Modifier.height(8.dp))
3945
Button(onClick = rendering.onSwapText) {
@@ -46,8 +52,7 @@ val TextInputViewFactory = composeViewFactory<Rendering> { rendering, _ ->
4652
@Preview(showBackground = true)
4753
@Composable private fun TextInputViewFactoryPreview() {
4854
TextInputViewFactory.Preview(Rendering(
49-
text = "Hello world",
50-
onTextChanged = {},
55+
textController = TextController("Hello world"),
5156
onSwapText = {}
5257
))
5358
}

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

Lines changed: 7 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -5,33 +5,27 @@ import com.squareup.sample.compose.textinput.TextInputWorkflow.State
55
import com.squareup.workflow1.Snapshot
66
import com.squareup.workflow1.StatefulWorkflow
77
import com.squareup.workflow1.action
8+
import com.squareup.workflow1.ui.TextController
9+
import com.squareup.workflow1.ui.WorkflowUiExperimentalApi
810

11+
@OptIn(WorkflowUiExperimentalApi::class)
912
object TextInputWorkflow : StatefulWorkflow<Unit, State, Nothing, Rendering>() {
1013

1114
data class State(
12-
val textA: String = "",
13-
val textB: String = "",
15+
val textA: TextController = TextController(),
16+
val textB: TextController = TextController(),
1417
val showingTextA: Boolean = true
1518
)
1619

1720
data class Rendering(
18-
val text: String,
19-
val onTextChanged: (String) -> Unit,
21+
val textController: TextController,
2022
val onSwapText: () -> Unit
2123
)
2224

2325
private val swapText = action {
2426
state = state.copy(showingTextA = !state.showingTextA)
2527
}
2628

27-
private fun changeText(text: String) = action {
28-
state = if (state.showingTextA) {
29-
state.copy(textA = text)
30-
} else {
31-
state.copy(textB = text)
32-
}
33-
}
34-
3529
override fun initialState(
3630
props: Unit,
3731
snapshot: Snapshot?
@@ -42,8 +36,7 @@ object TextInputWorkflow : StatefulWorkflow<Unit, State, Nothing, Rendering>() {
4236
renderState: State,
4337
context: RenderContext
4438
): Rendering = Rendering(
45-
text = if (renderState.showingTextA) renderState.textA else renderState.textB,
46-
onTextChanged = { context.actionSink.send(changeText(it)) },
39+
textController = if (renderState.showingTextA) renderState.textA else renderState.textB,
4740
onSwapText = { context.actionSink.send(swapText) }
4841
)
4942

workflow-ui/compose/api/compose.api

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,10 @@ public final class com/squareup/workflow1/ui/compose/RenderAsStateKt {
3131
public static final fun renderAsState (Lcom/squareup/workflow1/Workflow;Ljava/lang/Object;Ljava/util/List;Lkotlinx/coroutines/CoroutineScope;Lkotlin/jvm/functions/Function2;Landroidx/compose/runtime/Composer;II)Landroidx/compose/runtime/State;
3232
}
3333

34+
public final class com/squareup/workflow1/ui/compose/TextControllerAsMutableStateKt {
35+
public static final fun asMutableState (Lcom/squareup/workflow1/ui/TextController;Landroidx/compose/runtime/Composer;I)Landroidx/compose/runtime/MutableState;
36+
}
37+
3438
public final class com/squareup/workflow1/ui/compose/WorkflowRenderingKt {
3539
public static final fun WorkflowRendering (Ljava/lang/Object;Lcom/squareup/workflow1/ui/ViewEnvironment;Landroidx/compose/ui/Modifier;Landroidx/compose/runtime/Composer;II)V
3640
}
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
@file:OptIn(WorkflowUiExperimentalApi::class)
2+
3+
package com.squareup.workflow1.ui.compose
4+
5+
import androidx.compose.runtime.Composable
6+
import androidx.compose.runtime.LaunchedEffect
7+
import androidx.compose.runtime.MutableState
8+
import androidx.compose.runtime.mutableStateOf
9+
import androidx.compose.runtime.remember
10+
import androidx.compose.runtime.snapshotFlow
11+
import com.squareup.workflow1.ui.TextController
12+
import com.squareup.workflow1.ui.WorkflowUiExperimentalApi
13+
import kotlinx.coroutines.flow.collect
14+
import kotlinx.coroutines.launch
15+
16+
/**
17+
* Exposes the [textValue][TextController.textValue] of a [TextController]
18+
* as a remembered [MutableState], suitable for use from `@Composable`
19+
* functions.
20+
*
21+
* Usage:
22+
*
23+
* var text by rendering.textController.asMutableState()
24+
*
25+
* OutlinedTextField(
26+
* label = {},
27+
* placeholder = { Text("Enter some text") },
28+
* value = text,
29+
* onValueChange = { text = it }
30+
* )
31+
*/
32+
@Composable public fun TextController.asMutableState(): MutableState<String> {
33+
// keys are set to `this` to reset the state if a different controller is passed in…
34+
return remember(this) { mutableStateOf(textValue) }.also { state ->
35+
// …and to restart the effect.
36+
LaunchedEffect(this) {
37+
// Push changes from the workflow to the state.
38+
launch {
39+
onTextChanged.collect { state.value = it }
40+
}
41+
// And the other way – push changes to the state to the workflow.
42+
// This won't cause an infinite loop because both MutableState and
43+
// MutableSnapshotFlow ignore duplicate values.
44+
snapshotFlow { state.value }
45+
.collect { textValue = it }
46+
}
47+
}
48+
}

0 commit comments

Comments
 (0)