Skip to content

Commit

Permalink
Modernize tutorial final code
Browse files Browse the repository at this point in the history
It's time to get the Tutorial caught up with undeprecated API. This PR modifies only the final tutorial module. Once we're happy with the code, I'll back port the changes and update the prose in follow ups.

- Use `AndroidScreen` and drop `ViewRegistry`
- Better, more consistent use of `BackStackScreen`
- Use `View.setBackHandler`
- Use `TextController`
- Delete a lot of `// Exactly what the function name and the code say` comments
- More consistent naming, code style for actions and event handlers in `Screen` renderings
  - Event handler fields are named `onVerbPhrase`, like `onBackPressed`
  - Action names are verb phrases describing the action that is being taken, not the event that is being handled
  - Output names are generally in terms of the semantic event being reported to the parent, rather than describing what the parent will do

Thus:

```kotlin
return TodoListScreen(
  onRowPressed = { context.actionSink.send(reportSelection(it)) }
```

```kotlin
  private fun reportSelection(index: Int) = action {
    // Tell our parent that a todo item was selected.
    setOutput(TodoSelected(index))
  }
```

I did not introduce `RenderContext.eventHandler {}`, that seems like it would just be confusing to a newcomer.

Also not introducing big new blocks of material, in particular not introducing `Overlay`. I do think we should do that, but for this release I just want to focus on getting the deprecated code deleted.
  • Loading branch information
rjrjr committed Dec 12, 2023
1 parent e5d44fb commit 5d1dfb6
Show file tree
Hide file tree
Showing 35 changed files with 592 additions and 645 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ class DungeonAppWorkflow(
data class DisplayBoardsListScreen(
val boards: List<Board>,
val onBoardSelected: (index: Int) -> Unit
) : Screen
): Screen

override fun initialState(
props: Props,
Expand Down
2 changes: 1 addition & 1 deletion samples/tutorial/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ To help with the setup, we have created a few helper modules:

- `tutorial-views`: A set of 3 views for the 3 screens we will be building, `Welcome`, `TodoList`,
and `TodoEdit`.
- `tutorial-base`: This is the starting point to build out the tutorial. It contains layouts that host the views from `TutorialViews` to see how they display.
- `tutorial-base`: This is the starting point to build out the tutorial.
- `tutorial-final`: This is an example of the completed tutorial - could be used as a reference if
you get stuck.

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@ import com.squareup.workflow1.StatefulWorkflow
import com.squareup.workflow1.action
import com.squareup.workflow1.renderChild
import com.squareup.workflow1.ui.WorkflowUiExperimentalApi
import com.squareup.workflow1.ui.backstack.BackStackScreen
import com.squareup.workflow1.ui.backstack.toBackStackScreen
import com.squareup.workflow1.ui.container.BackStackScreen
import com.squareup.workflow1.ui.container.toBackStackScreen
import workflow.tutorial.RootWorkflow.State
import workflow.tutorial.RootWorkflow.State.Todo
import workflow.tutorial.RootWorkflow.State.Welcome
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,10 @@ import androidx.lifecycle.viewModelScope
import com.squareup.workflow1.ui.ViewRegistry
import com.squareup.workflow1.ui.WorkflowLayout
import com.squareup.workflow1.ui.WorkflowUiExperimentalApi
import com.squareup.workflow1.ui.backstack.BackStackContainer
import com.squareup.workflow1.ui.renderWorkflowIn
import kotlinx.coroutines.flow.StateFlow

private val viewRegistry = ViewRegistry(
BackStackContainer,
WelcomeLayoutRunner,
TodoListLayoutRunner
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@ import com.squareup.workflow1.StatefulWorkflow
import com.squareup.workflow1.action
import com.squareup.workflow1.renderChild
import com.squareup.workflow1.ui.WorkflowUiExperimentalApi
import com.squareup.workflow1.ui.backstack.BackStackScreen
import com.squareup.workflow1.ui.backstack.toBackStackScreen
import com.squareup.workflow1.ui.container.BackStackScreen
import com.squareup.workflow1.ui.container.toBackStackScreen
import workflow.tutorial.RootWorkflow.State
import workflow.tutorial.RootWorkflow.State.Todo
import workflow.tutorial.RootWorkflow.State.Welcome
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,10 @@ import androidx.lifecycle.viewModelScope
import com.squareup.workflow1.ui.ViewRegistry
import com.squareup.workflow1.ui.WorkflowLayout
import com.squareup.workflow1.ui.WorkflowUiExperimentalApi
import com.squareup.workflow1.ui.backstack.BackStackContainer
import com.squareup.workflow1.ui.renderWorkflowIn
import kotlinx.coroutines.flow.StateFlow

private val viewRegistry = ViewRegistry(
BackStackContainer,
WelcomeLayoutRunner,
TodoListLayoutRunner,
TodoEditLayoutRunner
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@ import com.squareup.workflow1.StatefulWorkflow
import com.squareup.workflow1.action
import com.squareup.workflow1.renderChild
import com.squareup.workflow1.ui.WorkflowUiExperimentalApi
import com.squareup.workflow1.ui.backstack.BackStackScreen
import com.squareup.workflow1.ui.backstack.toBackStackScreen
import com.squareup.workflow1.ui.container.BackStackScreen
import com.squareup.workflow1.ui.container.toBackStackScreen
import workflow.tutorial.RootWorkflow.State
import workflow.tutorial.RootWorkflow.State.Todo
import workflow.tutorial.RootWorkflow.State.Welcome
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,10 @@ import androidx.lifecycle.viewModelScope
import com.squareup.workflow1.ui.ViewRegistry
import com.squareup.workflow1.ui.WorkflowLayout
import com.squareup.workflow1.ui.WorkflowUiExperimentalApi
import com.squareup.workflow1.ui.backstack.BackStackContainer
import com.squareup.workflow1.ui.renderWorkflowIn
import kotlinx.coroutines.flow.StateFlow

private val viewRegistry = ViewRegistry(
BackStackContainer,
WelcomeLayoutRunner,
TodoListLayoutRunner,
TodoEditLayoutRunner
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
package workflow.tutorial

import com.squareup.workflow1.Snapshot
import com.squareup.workflow1.StatefulWorkflow
import com.squareup.workflow1.action
import com.squareup.workflow1.renderChild
import com.squareup.workflow1.ui.WorkflowUiExperimentalApi
import com.squareup.workflow1.ui.container.BackStackScreen
import com.squareup.workflow1.ui.container.plus
import workflow.tutorial.RootNavigationWorkflow.State
import workflow.tutorial.RootNavigationWorkflow.State.Todo
import workflow.tutorial.RootNavigationWorkflow.State.Welcome
import workflow.tutorial.TodoNavigationWorkflow.TodoProps

@OptIn(WorkflowUiExperimentalApi::class)
object RootNavigationWorkflow : StatefulWorkflow<Unit, State, Nothing, BackStackScreen<*>>() {

sealed class State {
object Welcome : State()
data class Todo(val username: String) : State()
}

override fun initialState(
props: Unit,
snapshot: Snapshot?
): State = Welcome

@OptIn(WorkflowUiExperimentalApi::class)
override fun render(
renderProps: Unit,
renderState: State,
context: RenderContext
): BackStackScreen<*> {
// Render a child workflow of type WelcomeWorkflow. When renderChild is called, the
// infrastructure will start a child workflow session if one is not already running.
val welcomeScreen = context.renderChild(WelcomeWorkflow) { loggedIn ->
// When WelcomeWorkflow emits LoggedIn, enqueue our log in action.
logIn(loggedIn.username)
}

return when (renderState) {
// When the state is Welcome, defer to the WelcomeWorkflow.
is Welcome -> {
BackStackScreen(welcomeScreen)
}

// When the state is Todo, defer to the TodoListWorkflow.
is Todo -> {
val todoBackStack = context.renderChild(
child = TodoNavigationWorkflow,
props = TodoProps(renderState.username)
) {
// When TodoNavigationWorkflow emits Back, enqueue our log out action.
logOut
}
BackStackScreen(welcomeScreen) + todoBackStack
}
}
}

override fun snapshotState(state: State): Snapshot? = null

private fun logIn(username: String) = action {
state = Todo(username)
}

private val logOut = action {
state = Welcome
}
}

This file was deleted.

This file was deleted.

Original file line number Diff line number Diff line change
@@ -1,15 +1,21 @@
package workflow.tutorial

import com.squareup.workflow1.ui.AndroidScreen
import com.squareup.workflow1.ui.ScreenViewFactory
import com.squareup.workflow1.ui.TextController
import com.squareup.workflow1.ui.WorkflowUiExperimentalApi
import workflow.tutorial.views.databinding.TodoEditViewBinding

@OptIn(WorkflowUiExperimentalApi::class)
data class TodoEditScreen(
/** The title of this todo item. */
val title: String,
val title: TextController,
/** The contents, or "note" of the todo. */
val note: String,

/** Callbacks for when the title or note changes. */
val onTitleChanged: (String) -> Unit,
val onNoteChanged: (String) -> Unit,
val note: TextController,

val discardChanges: () -> Unit,
val saveChanges: () -> Unit
)
val onBackPressed: () -> Unit,
val onSavePressed: () -> Unit
) : AndroidScreen<TodoEditScreen> {
override val viewFactory =
ScreenViewFactory.fromViewBinding(TodoEditViewBinding::inflate, ::TodoEditScreenRunner)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package workflow.tutorial

import com.squareup.workflow1.ui.ScreenViewRunner
import com.squareup.workflow1.ui.ViewEnvironment
import com.squareup.workflow1.ui.WorkflowUiExperimentalApi
import com.squareup.workflow1.ui.control
import com.squareup.workflow1.ui.setBackHandler
import workflow.tutorial.views.databinding.TodoEditViewBinding

@OptIn(WorkflowUiExperimentalApi::class)
class TodoEditScreenRunner(
private val binding: TodoEditViewBinding
) : ScreenViewRunner<TodoEditScreen> {

override fun showRendering(
rendering: TodoEditScreen,
environment: ViewEnvironment
) {
binding.root.setBackHandler(rendering.onBackPressed)
binding.save.setOnClickListener { rendering.onSavePressed() }
rendering.title.control(binding.todoTitle)
rendering.note.control(binding.todoNote)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,14 @@ package workflow.tutorial
import com.squareup.workflow1.Snapshot
import com.squareup.workflow1.StatefulWorkflow
import com.squareup.workflow1.action
import com.squareup.workflow1.ui.WorkflowUiExperimentalApi
import workflow.tutorial.TodoEditWorkflow.Output
import workflow.tutorial.TodoEditWorkflow.Output.Discard
import workflow.tutorial.TodoEditWorkflow.Output.Save
import workflow.tutorial.TodoEditWorkflow.Output.DiscardChanges
import workflow.tutorial.TodoEditWorkflow.Output.SaveChanges
import workflow.tutorial.TodoEditWorkflow.EditProps
import workflow.tutorial.TodoEditWorkflow.State

@OptIn(WorkflowUiExperimentalApi::class)
object TodoEditWorkflow : StatefulWorkflow<EditProps, State, Output, TodoEditScreen>() {

data class EditProps(
Expand All @@ -22,8 +24,8 @@ object TodoEditWorkflow : StatefulWorkflow<EditProps, State, Output, TodoEditScr
)

sealed class Output {
object Discard : Output()
data class Save(val todo: TodoModel) : Output()
object DiscardChanges : Output()
data class SaveChanges(val todo: TodoModel) : Output()
}

override fun initialState(
Expand Down Expand Up @@ -52,35 +54,20 @@ object TodoEditWorkflow : StatefulWorkflow<EditProps, State, Output, TodoEditScr
context: RenderContext
): TodoEditScreen {
return TodoEditScreen(
title = renderState.todo.title,
note = renderState.todo.note,
onTitleChanged = { context.actionSink.send(onTitleChanged(it)) },
onNoteChanged = { context.actionSink.send(onNoteChanged(it)) },
saveChanges = { context.actionSink.send(onSave()) },
discardChanges = { context.actionSink.send(onDiscard()) }
title = renderState.todo.title,
note = renderState.todo.note,
onSavePressed = { context.actionSink.send(requestSave) },
onBackPressed = { context.actionSink.send(requestDiscard) }
)
}

override fun snapshotState(state: State): Snapshot? = null

internal fun onTitleChanged(title: String) = action {
state = state.withTitle(title)
private val requestDiscard = action {
setOutput(DiscardChanges)
}

internal fun onNoteChanged(note: String) = action {
state = state.withNote(note)
internal val requestSave = action {
setOutput(SaveChanges(state.todo))
}

private fun onDiscard() = action {
// Emit the Discard output when the discard action is received.
setOutput(Discard)
}

internal fun onSave() = action {
// Emit the Save output with the current todo state when the save action is received.
setOutput(Save(state.todo))
}

private fun State.withTitle(title: String) = copy(todo = todo.copy(title = title))
private fun State.withNote(note: String) = copy(todo = todo.copy(note = note))
}
Loading

0 comments on commit 5d1dfb6

Please sign in to comment.