Skip to content

Commit a71d61b

Browse files
Merge pull request #390 from tir38/tir38/update_tutorial4
Add updates and clarification to Tutorial 4
2 parents 16baebc + 26c0ad5 commit a71d61b

File tree

6 files changed

+90
-128
lines changed

6 files changed

+90
-128
lines changed

samples/tutorial/Tutorial4.md

Lines changed: 68 additions & 88 deletions
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,6 @@ To follow this tutorial, launch Android Studio and open this folder (`samples/tu
88

99
Start from the implementation of `tutorial-3-complete` if you're skipping ahead.
1010

11-
## Adding new todo items
12-
13-
A gap in the usability of the todo app is that it does not let the user create new todo items. We will add an "add" button on the right side of the navigation bar for this.
14-
1511
## Refactoring a workflow by splitting it into a parent and child
1612

1713
The `TodoListWorkflow` has started to grow and has multiple concerns it's handling — specifically all of the `TodoListScreen` behavior, as well as the actions that can come from the `TodoEditWorkflow`.
@@ -20,7 +16,7 @@ When a single workflow seems to be doing too many things, a common pattern is to
2016

2117
### TodoWorkflow
2218

23-
Create a new workflow called `Todo` that will be responsible for both the `TodoListWorkflow` and the `TodoEditWorkflow`.
19+
Create a new workflow called `Todo` that will be responsible for both the `TodoListWorkflow` and the `TodoEditWorkflow`.
2420

2521
```kotlin
2622
object TodoWorkflow : StatefulWorkflow<TodoProps, State, Back, List<Any>>() {
@@ -30,7 +26,7 @@ object TodoWorkflow : StatefulWorkflow<TodoProps, State, Back, List<Any>>() {
3026
}
3127
```
3228

33-
#### Moving logic from the TodoList to the TodoWorkflow
29+
#### Moving logic from the TodoListWorkflow to the TodoWorkflow
3430

3531
Move the `ListState` state, input, and outputs from the `TodoListWorkflow` up to the `TodoWorkflow`. It will be owner the list of todo items, and the `TodoListWorkflow` will simply show whatever is passed into its input:
3632

@@ -75,10 +71,10 @@ object TodoWorkflow : StatefulWorkflow<TodoProps, State, Back, List<Any>>() {
7571
}
7672
```
7773

78-
Define the output events from the `TodoListWorkflow` to describe the `new` item action and selecting a todo item, as well as removing the todo list from the `State`:
74+
Define the output events from the `TodoListWorkflow` to include back and a new `SelectTodo` output. Also, We no longer need to maintain the todo list in the `State` and we can remove it:
7975

8076
```kotlin
81-
object TodoListWorkflow : StatefulWorkflow<ListProps, Unit, Output, TodoListScreen>() {
77+
object TodoListWorkflow : StatefulWorkflow<ListProps, State, Output, TodoListScreen>() {
8278

8379
data class ListProps(
8480
val username: String,
@@ -90,17 +86,16 @@ object TodoListWorkflow : StatefulWorkflow<ListProps, Unit, Output, TodoListScre
9086
sealed class Output {
9187
object Back : Output()
9288
data class SelectTodo(val index: Int) : Output()
93-
object NewTodo : Output()
9489
}
9590

9691
override fun initialState(
9792
props: ListProps,
9893
snapshot: Snapshot?
99-
) = Unit
94+
) = State
10095

10196
override fun render(
10297
renderProps: ListProps,
103-
state: Unit,
98+
renderState: State,
10499
context: RenderContext
105100
): TodoListScreen {
106101
//
@@ -112,12 +107,10 @@ object TodoListWorkflow : StatefulWorkflow<ListProps, Unit, Output, TodoListScre
112107
}
113108
```
114109

115-
Because the `State` has no properties anymore, it can't be a data class, so we need to change it to an object. Since the only reason to have a custom type for state is to define the data we want to store, we don't need a custom type anymore so we can just use `Unit`.
116-
117110
Change the `WorkflowAction` behaviors to return an output instead of modifying any state:
118111

119112
```kotlin
120-
object TodoListWorkflow : StatefulWorkflow<ListProps, Unit, Output, TodoListScreen>() {
113+
object TodoListWorkflow : StatefulWorkflow<ListProps, State, Output, TodoListScreen>() {
121114

122115
//
123116

@@ -130,11 +123,53 @@ object TodoListWorkflow : StatefulWorkflow<ListProps, Unit, Output, TodoListScre
130123
// Tell our parent that a todo item was selected.
131124
setOutput(SelectTodo(index))
132125
}
126+
}
127+
```
133128

134-
private fun new() = action {
135-
// Tell our parent a new todo item should be created.
136-
setOutput(NewTodo)
129+
Move the editing actions from the `TodoListWorkflow` to the `TodoWorkflow`. We won't be able to call these methods until we can respond to output from the `TodoEditWorkflow`, but doing this now helps clean up the `TodoListWorkflow`:
130+
131+
```kotlin
132+
object TodoWorkflow : StatefulWorkflow<TodoProps, State, Output, List<Any>>() {
133+
134+
//
135+
136+
private fun discardChanges() = action {
137+
// When a discard action is received, return to the list.
138+
state = state.copy(step = Step.List)
139+
}
140+
141+
private fun saveChanges(
142+
todo: TodoModel,
143+
index: Int
144+
) = action {
145+
// When changes are saved, update the state of that todo item and return to the list.
146+
state = state.copy(
147+
todos = state.todos.toMutableList().also { it[index] = todo },
148+
step = Step.List
149+
)
150+
}
151+
}
152+
```
153+
154+
Because `TodoWorkflow.State` has no properties anymore, it can't be a `data` class, so we need to change it to an `object`. Since the only reason to have a custom type for state is to define the data we want to store, we don't need a custom type anymore so we can just use `Unit`. You might ask why we need a state at all now. We will discuss that in the next section. For now `Unit` will get us moving forward.
155+
156+
```kotlin
157+
object TodoListWorkflow : StatefulWorkflow<ListProps, Unit, Output, TodoListScreen>() {
158+
159+
override fun initialState(
160+
props: ListProps,
161+
snapshot: Snapshot?
162+
) = Unit
163+
164+
override fun render(
165+
renderProps: ListProps,
166+
renderState: Unit,
167+
context: RenderContext
168+
): TodoListScreen {
169+
//
137170
}
171+
172+
override fun snapshotState(state: Unit): Snapshot? = null
138173
}
139174
```
140175

@@ -147,12 +182,12 @@ object TodoListWorkflow : StatefulWorkflow<ListProps, Unit, Output, TodoListScre
147182

148183
override fun render(
149184
renderProps: ListProps,
150-
state: Unit,
185+
renderState: Unit,
151186
context: RenderContext
152187
): TodoListScreen {
153-
val titles = props.todos.map { it.title }
188+
val titles = renderProps.todos.map { it.title }
154189
return TodoListScreen(
155-
username = props.username,
190+
username = renderProps.username,
156191
todoTitles = titles,
157192
onTodoSelected = { context.actionSink.send(selectTodo(it)) },
158193
onBack = { context.actionSink.send(onBack()) }
@@ -201,14 +236,13 @@ object TodoWorkflow : StatefulWorkflow<TodoProps, State, Back, List<Any>>() {
201236
val todoListScreen = context.renderChild(
202237
TodoListWorkflow,
203238
props = ListProps(
204-
username = props.username,
205-
todos = state.todos
239+
username = renderProps.username,
240+
todos = renderState.todos
206241
)
207242
) { output ->
208243
when (output) {
209244
Output.Back -> onBack()
210245
is SelectTodo -> editTodo(output.index)
211-
NewTodo -> newTodo()
212246
}
213247
}
214248

@@ -224,48 +258,23 @@ object TodoWorkflow : StatefulWorkflow<TodoProps, State, Back, List<Any>>() {
224258
// When a todo item is selected, edit it.
225259
state = state.copy(step = Step.Edit(index))
226260
}
227-
228-
private fun newTodo() = action {
229-
// Append a new todo model to the end of the list.
230-
state = state.copy(
231-
todos = state.todos + TodoModel(
232-
title = "New Todo",
233-
note = ""
234-
)
235-
)
236-
}
237261
}
238262
```
239263

240-
Update the `RootWorkflow` to defer to the `TodoWorkflow` for rendering the `Todo` state. This will get us back into a state where we can build again (albeit without editing support):
264+
So far `RootWorkflow` is still deferring to the `TodoListWorkflow`. Update the `RootWorkflow` to defer to the `TodoWorkflow` for rendering the `Todo` state. This will get us back into a state where we can build again (albeit without editing support):
241265

242266
```kotlin
243-
object RootWorkflow : StatefulWorkflow<Unit, State, Nothing, BackStackScreen<*>>() {
267+
object RootWorkflow : StatefulWorkflow<Unit, State, Nothing, BackStackScreen<Any>>() {
244268

245269
//
246270

247271
override fun render(
248272
renderProps: Unit,
249273
renderState: State,
250274
context: RenderContext
251-
): BackStackScreen<*> {
275+
): BackStackScreen<Any> {
252276

253-
// Our list of back stack items. Will always include the "WelcomeScreen".
254-
val backstackScreens = mutableListOf<Any>()
255-
256-
// Render a child workflow of type WelcomeWorkflow. When renderChild is called, the
257-
// infrastructure will create a child workflow with state if one is not already running.
258-
val welcomeScreen = context.renderChild(WelcomeWorkflow) { output ->
259-
// When WelcomeWorkflow emits LoggedIn, turn it into our login action.
260-
login(output.username)
261-
}
262-
backstackScreens += welcomeScreen
263-
264-
when (state) {
265-
// When the state is Welcome, defer to the WelcomeWorkflow.
266-
is Welcome -> {
267-
// We always add the welcome screen to the backstack, so this is a no op.
268-
}
277+
//
269278

270279
// When the state is Todo, defer to the TodoListWorkflow.
271280
is Todo -> {
@@ -277,45 +286,17 @@ object RootWorkflow : StatefulWorkflow<Unit, State, Nothing, BackStackScreen<*>>
277286
}
278287
}
279288

280-
// Finally, return the BackStackScreen with a list of BackStackScreen.Items
281-
return backstackScreens.toBackStackScreen()
289+
//
282290
}
283-
284291
//
285-
286292
}
287293
```
288294

289295
#### Moving Edit Output handling to the TodoWorkflow
290296

291-
The `TodoWorkflow` now can handle the outputs from the `TodoListWorkflow`. Next, let's add handling for the `TodoEditWorkflow` output events.
292-
293-
Since the types of output and actions are pretty different from their origin, make a *second* set of actions on the `TodoWorkflow`:
294-
295-
```kotlin
296-
object TodoWorkflow : StatefulWorkflow<TodoProps, State, Back, List<Any>>() {
297-
298-
//
299-
300-
private fun discardChanges() = action {
301-
// When a discard action is received, return to the list.
302-
state = state.copy(step = Step.List)
303-
}
304-
305-
private fun saveChanges(
306-
todo: TodoModel,
307-
index: Int
308-
) = action {
309-
// When changes are saved, update the state of that todo item and return to the list.
310-
state = state.copy(
311-
todos = state.todos.toMutableList().also { it[index] = todo },
312-
step = Step.List
313-
)
314-
}
315-
}
316-
```
297+
The `TodoWorkflow` now can handle the outputs from the `TodoListWorkflow`. Next, let's add handling for the `TodoEditWorkflow` output events. Earlier we copied `discardChanges` and `saveChanges` into the `TodoWorkflow`. We can now call them.
317298

318-
Update the `render` method to show the `TodoEditWorkflow` screen when on the edit step:
299+
Update the `render` method to show the `TodoEditWorkflow` screen when on the edit step. Handle the `TodoEditWorkflow` output by calling `discardChanges` or `saveChanges`.
319300

320301
```kotlin
321302
object TodoWorkflow : StatefulWorkflow<TodoProps, State, Back, List<Any>>() {
@@ -330,25 +311,24 @@ object TodoWorkflow : StatefulWorkflow<TodoProps, State, Back, List<Any>>() {
330311
val todoListScreen = context.renderChild(
331312
TodoListWorkflow,
332313
props = ListProps(
333-
username = props.username,
334-
todos = state.todos
314+
username = renderProps.username,
315+
todos = renderState.todos
335316
)
336317
) { output ->
337318
when (output) {
338319
Output.Back -> onBack()
339320
is SelectTodo -> editTodo(output.index)
340-
NewTodo -> newTodo()
341321
}
342322
}
343323

344-
return when (val step = state.step) {
324+
return when (val step = renderState.step) {
345325
// On the "list" step, return just the list screen.
346326
Step.List -> listOf(todoListScreen)
347327
is Step.Edit -> {
348328
// On the "edit" step, return both the list and edit screens.
349329
val todoEditScreen = context.renderChild(
350330
TodoEditWorkflow,
351-
EditProps(state.todos[step.index])
331+
EditProps(renderState.todos[step.index])
352332
) { output ->
353333
when (output) {
354334
// Send the discardChanges action when the discard output is received.

samples/tutorial/tutorial-4-complete/src/main/java/workflow/tutorial/RootWorkflow.kt

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ import workflow.tutorial.RootWorkflow.State.Welcome
1313
import workflow.tutorial.TodoWorkflow.TodoProps
1414

1515
@OptIn(WorkflowUiExperimentalApi::class)
16-
object RootWorkflow : StatefulWorkflow<Unit, State, Nothing, BackStackScreen<*>>() {
16+
object RootWorkflow : StatefulWorkflow<Unit, State, Nothing, BackStackScreen<Any>>() {
1717

1818
sealed class State {
1919
object Welcome : State()
@@ -27,10 +27,10 @@ object RootWorkflow : StatefulWorkflow<Unit, State, Nothing, BackStackScreen<*>>
2727

2828
@OptIn(WorkflowUiExperimentalApi::class)
2929
override fun render(
30-
props: Unit,
31-
state: State,
30+
renderProps: Unit,
31+
renderState: State,
3232
context: RenderContext
33-
): BackStackScreen<*> {
33+
): BackStackScreen<Any> {
3434

3535
// Our list of back stack items. Will always include the "WelcomeScreen".
3636
val backstackScreens = mutableListOf<Any>()
@@ -43,15 +43,15 @@ object RootWorkflow : StatefulWorkflow<Unit, State, Nothing, BackStackScreen<*>>
4343
}
4444
backstackScreens += welcomeScreen
4545

46-
when (state) {
46+
when (renderState) {
4747
// When the state is Welcome, defer to the WelcomeWorkflow.
4848
is Welcome -> {
4949
// We always add the welcome screen to the backstack, so this is a no op.
5050
}
5151

5252
// When the state is Todo, defer to the TodoListWorkflow.
5353
is Todo -> {
54-
val todoListScreens = context.renderChild(TodoWorkflow, TodoProps(state.name)) {
54+
val todoListScreens = context.renderChild(TodoWorkflow, TodoProps(renderState.name)) {
5555
// When receiving a Back output, treat it as a logout action.
5656
logout
5757
}

samples/tutorial/tutorial-4-complete/src/main/java/workflow/tutorial/TodoEditWorkflow.kt

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -47,13 +47,13 @@ object TodoEditWorkflow : StatefulWorkflow<EditProps, State, Output, TodoEditScr
4747
}
4848

4949
override fun render(
50-
props: EditProps,
51-
state: State,
50+
renderProps: EditProps,
51+
renderState: State,
5252
context: RenderContext
5353
): TodoEditScreen {
5454
return TodoEditScreen(
55-
title = state.todo.title,
56-
note = state.todo.note,
55+
title = renderState.todo.title,
56+
note = renderState.todo.note,
5757
onTitleChanged = { context.actionSink.send(onTitleChanged(it)) },
5858
onNoteChanged = { context.actionSink.send(onNoteChanged(it)) },
5959
saveChanges = { context.actionSink.send(onSave()) },

samples/tutorial/tutorial-4-complete/src/main/java/workflow/tutorial/TodoListWorkflow.kt

Lines changed: 3 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@ import com.squareup.workflow1.ui.WorkflowUiExperimentalApi
66
import workflow.tutorial.TodoListWorkflow.ListProps
77
import workflow.tutorial.TodoListWorkflow.Output
88
import workflow.tutorial.TodoListWorkflow.Output.Back
9-
import workflow.tutorial.TodoListWorkflow.Output.NewTodo
109
import workflow.tutorial.TodoListWorkflow.Output.SelectTodo
1110

1211
@OptIn(WorkflowUiExperimentalApi::class)
@@ -20,16 +19,15 @@ object TodoListWorkflow : StatelessWorkflow<ListProps, Output, TodoListScreen>()
2019
sealed class Output {
2120
object Back : Output()
2221
data class SelectTodo(val index: Int) : Output()
23-
object NewTodo : Output()
2422
}
2523

2624
override fun render(
27-
props: ListProps,
25+
renderProps: ListProps,
2826
context: RenderContext
2927
): TodoListScreen {
30-
val titles = props.todos.map { it.title }
28+
val titles = renderProps.todos.map { it.title }
3129
return TodoListScreen(
32-
username = props.username,
30+
username = renderProps.username,
3331
todoTitles = titles,
3432
onTodoSelected = { context.actionSink.send(selectTodo(it)) },
3533
onBack = { context.actionSink.send(onBack()) }
@@ -45,9 +43,4 @@ object TodoListWorkflow : StatelessWorkflow<ListProps, Output, TodoListScreen>()
4543
// Tell our parent that a todo item was selected.
4644
setOutput(SelectTodo(index))
4745
}
48-
49-
private fun new() = action {
50-
// Tell our parent a new todo item should be created.
51-
setOutput(NewTodo)
52-
}
5346
}

0 commit comments

Comments
 (0)