From 6b66f942829bacabe1787b3e1a2d7d8f7f352031 Mon Sep 17 00:00:00 2001 From: Ray Ryan Date: Tue, 10 Oct 2023 11:47:57 -0700 Subject: [PATCH] Modernize tutorial final code 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` - Use `View.setBackHandler` - Use `TextController` - More consistent naming, code style for actions and event handlers in `Screen` renderings 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. --- samples/tutorial/README.md | 2 +- .../java/workflow/tutorial/RootWorkflow.kt | 4 +- .../workflow/tutorial/TutorialActivity.kt | 2 - .../java/workflow/tutorial/RootWorkflow.kt | 4 +- .../workflow/tutorial/TutorialActivity.kt | 2 - .../java/workflow/tutorial/RootWorkflow.kt | 4 +- .../workflow/tutorial/TutorialActivity.kt | 2 - .../java/workflow/tutorial/RootWorkflow.kt | 11 +- .../workflow/tutorial/TodoEditLayoutRunner.kt | 33 ---- .../java/workflow/tutorial/TodoEditRunner.kt | 24 +++ .../java/workflow/tutorial/TodoEditScreen.kt | 24 ++- .../workflow/tutorial/TodoEditWorkflow.kt | 23 +-- .../java/workflow/tutorial/TodoListScreen.kt | 14 +- ...ayoutRunner.kt => TodoListScreenRunner.kt} | 19 +- .../workflow/tutorial/TodoListWorkflow.kt | 13 +- .../main/java/workflow/tutorial/TodoModel.kt | 15 +- .../java/workflow/tutorial/TodoWorkflow.kt | 47 ++--- .../workflow/tutorial/TutorialActivity.kt | 33 ++-- .../workflow/tutorial/WelcomeLayoutRunner.kt | 38 ---- .../java/workflow/tutorial/WelcomeScreen.kt | 16 +- .../workflow/tutorial/WelcomeScreenRunner.kt | 23 +++ .../java/workflow/tutorial/WelcomeWorkflow.kt | 27 ++- .../workflow/tutorial/RootWorkflowTest.kt | 40 ++-- .../workflow/tutorial/TodoEditWorkflowTest.kt | 46 ++--- .../workflow/tutorial/TodoWorkflowTest.kt | 187 ++++++++++-------- .../workflow/tutorial/WelcomeWorkflowTest.kt | 39 +--- .../src/main/res/layout/todo_list_view.xml | 27 ++- .../src/main/res/values/strings.xml | 1 + .../squareup/workflow1/ui/TextController.kt | 2 +- 29 files changed, 339 insertions(+), 383 deletions(-) delete mode 100644 samples/tutorial/tutorial-final/src/main/java/workflow/tutorial/TodoEditLayoutRunner.kt create mode 100644 samples/tutorial/tutorial-final/src/main/java/workflow/tutorial/TodoEditRunner.kt rename samples/tutorial/tutorial-final/src/main/java/workflow/tutorial/{TodoListLayoutRunner.kt => TodoListScreenRunner.kt} (65%) delete mode 100644 samples/tutorial/tutorial-final/src/main/java/workflow/tutorial/WelcomeLayoutRunner.kt create mode 100644 samples/tutorial/tutorial-final/src/main/java/workflow/tutorial/WelcomeScreenRunner.kt diff --git a/samples/tutorial/README.md b/samples/tutorial/README.md index d1fac33e3a..325e64ab7c 100644 --- a/samples/tutorial/README.md +++ b/samples/tutorial/README.md @@ -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. diff --git a/samples/tutorial/tutorial-2-complete/src/main/java/workflow/tutorial/RootWorkflow.kt b/samples/tutorial/tutorial-2-complete/src/main/java/workflow/tutorial/RootWorkflow.kt index 6e0e9ae48f..d8e4054091 100644 --- a/samples/tutorial/tutorial-2-complete/src/main/java/workflow/tutorial/RootWorkflow.kt +++ b/samples/tutorial/tutorial-2-complete/src/main/java/workflow/tutorial/RootWorkflow.kt @@ -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 diff --git a/samples/tutorial/tutorial-2-complete/src/main/java/workflow/tutorial/TutorialActivity.kt b/samples/tutorial/tutorial-2-complete/src/main/java/workflow/tutorial/TutorialActivity.kt index a88f619eb1..aed8f11baa 100644 --- a/samples/tutorial/tutorial-2-complete/src/main/java/workflow/tutorial/TutorialActivity.kt +++ b/samples/tutorial/tutorial-2-complete/src/main/java/workflow/tutorial/TutorialActivity.kt @@ -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 ) diff --git a/samples/tutorial/tutorial-3-complete/src/main/java/workflow/tutorial/RootWorkflow.kt b/samples/tutorial/tutorial-3-complete/src/main/java/workflow/tutorial/RootWorkflow.kt index 05ee1a4386..23559c10f9 100644 --- a/samples/tutorial/tutorial-3-complete/src/main/java/workflow/tutorial/RootWorkflow.kt +++ b/samples/tutorial/tutorial-3-complete/src/main/java/workflow/tutorial/RootWorkflow.kt @@ -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 diff --git a/samples/tutorial/tutorial-3-complete/src/main/java/workflow/tutorial/TutorialActivity.kt b/samples/tutorial/tutorial-3-complete/src/main/java/workflow/tutorial/TutorialActivity.kt index 9aa52a2e1e..42e65f358d 100644 --- a/samples/tutorial/tutorial-3-complete/src/main/java/workflow/tutorial/TutorialActivity.kt +++ b/samples/tutorial/tutorial-3-complete/src/main/java/workflow/tutorial/TutorialActivity.kt @@ -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 diff --git a/samples/tutorial/tutorial-4-complete/src/main/java/workflow/tutorial/RootWorkflow.kt b/samples/tutorial/tutorial-4-complete/src/main/java/workflow/tutorial/RootWorkflow.kt index d311bd88b5..107b1ba6c9 100644 --- a/samples/tutorial/tutorial-4-complete/src/main/java/workflow/tutorial/RootWorkflow.kt +++ b/samples/tutorial/tutorial-4-complete/src/main/java/workflow/tutorial/RootWorkflow.kt @@ -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 diff --git a/samples/tutorial/tutorial-4-complete/src/main/java/workflow/tutorial/TutorialActivity.kt b/samples/tutorial/tutorial-4-complete/src/main/java/workflow/tutorial/TutorialActivity.kt index 99c67c824c..d002274b4a 100644 --- a/samples/tutorial/tutorial-4-complete/src/main/java/workflow/tutorial/TutorialActivity.kt +++ b/samples/tutorial/tutorial-4-complete/src/main/java/workflow/tutorial/TutorialActivity.kt @@ -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 diff --git a/samples/tutorial/tutorial-final/src/main/java/workflow/tutorial/RootWorkflow.kt b/samples/tutorial/tutorial-final/src/main/java/workflow/tutorial/RootWorkflow.kt index 3a52a96006..db5b344b94 100644 --- a/samples/tutorial/tutorial-final/src/main/java/workflow/tutorial/RootWorkflow.kt +++ b/samples/tutorial/tutorial-final/src/main/java/workflow/tutorial/RootWorkflow.kt @@ -4,16 +4,17 @@ 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.Screen 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 import workflow.tutorial.TodoWorkflow.TodoProps @OptIn(WorkflowUiExperimentalApi::class) -object RootWorkflow : StatefulWorkflow>() { +object RootWorkflow : StatefulWorkflow>() { sealed class State { object Welcome : State() @@ -30,10 +31,10 @@ object RootWorkflow : StatefulWorkflow { + ): BackStackScreen { // Our list of back stack items. Will always include the "WelcomeScreen". - val backstackScreens = mutableListOf() + val backstackScreens = mutableListOf() // Render a child workflow of type WelcomeWorkflow. When renderChild is called, the // infrastructure will create a child workflow with state if one is not already running. diff --git a/samples/tutorial/tutorial-final/src/main/java/workflow/tutorial/TodoEditLayoutRunner.kt b/samples/tutorial/tutorial-final/src/main/java/workflow/tutorial/TodoEditLayoutRunner.kt deleted file mode 100644 index ef67bddd96..0000000000 --- a/samples/tutorial/tutorial-final/src/main/java/workflow/tutorial/TodoEditLayoutRunner.kt +++ /dev/null @@ -1,33 +0,0 @@ -package workflow.tutorial - -import com.squareup.workflow1.ui.LayoutRunner -import com.squareup.workflow1.ui.LayoutRunner.Companion.bind -import com.squareup.workflow1.ui.ViewEnvironment -import com.squareup.workflow1.ui.ViewFactory -import com.squareup.workflow1.ui.WorkflowUiExperimentalApi -import com.squareup.workflow1.ui.backPressedHandler -import com.squareup.workflow1.ui.setTextChangedListener -import com.squareup.workflow1.ui.updateText -import workflow.tutorial.views.databinding.TodoEditViewBinding - -@OptIn(WorkflowUiExperimentalApi::class) -class TodoEditLayoutRunner( - private val binding: TodoEditViewBinding -) : LayoutRunner { - - override fun showRendering( - rendering: TodoEditScreen, - viewEnvironment: ViewEnvironment - ) { - binding.root.backPressedHandler = rendering.discardChanges - binding.save.setOnClickListener { rendering.saveChanges() } - binding.todoTitle.updateText(rendering.title) - binding.todoTitle.setTextChangedListener { rendering.onTitleChanged(it.toString()) } - binding.todoNote.updateText(rendering.note) - binding.todoNote.setTextChangedListener { rendering.onNoteChanged(it.toString()) } - } - - companion object : ViewFactory by bind( - TodoEditViewBinding::inflate, ::TodoEditLayoutRunner - ) -} diff --git a/samples/tutorial/tutorial-final/src/main/java/workflow/tutorial/TodoEditRunner.kt b/samples/tutorial/tutorial-final/src/main/java/workflow/tutorial/TodoEditRunner.kt new file mode 100644 index 0000000000..46b8d40c60 --- /dev/null +++ b/samples/tutorial/tutorial-final/src/main/java/workflow/tutorial/TodoEditRunner.kt @@ -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 TodoEditRunner( + private val binding: TodoEditViewBinding +) : ScreenViewRunner { + + override fun showRendering( + rendering: TodoEditScreen, + environment: ViewEnvironment + ) { + binding.root.setBackHandler(rendering.onBackClick) + binding.save.setOnClickListener { rendering.onSaveClick() } + rendering.title.control(binding.todoTitle) + rendering.note.control(binding.todoNote) + } +} diff --git a/samples/tutorial/tutorial-final/src/main/java/workflow/tutorial/TodoEditScreen.kt b/samples/tutorial/tutorial-final/src/main/java/workflow/tutorial/TodoEditScreen.kt index b64bb8798d..a4c892ee04 100644 --- a/samples/tutorial/tutorial-final/src/main/java/workflow/tutorial/TodoEditScreen.kt +++ b/samples/tutorial/tutorial-final/src/main/java/workflow/tutorial/TodoEditScreen.kt @@ -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 onBackClick: () -> Unit, + val onSaveClick: () -> Unit +) : AndroidScreen { + override val viewFactory = + ScreenViewFactory.fromViewBinding(TodoEditViewBinding::inflate, ::TodoEditRunner) +} diff --git a/samples/tutorial/tutorial-final/src/main/java/workflow/tutorial/TodoEditWorkflow.kt b/samples/tutorial/tutorial-final/src/main/java/workflow/tutorial/TodoEditWorkflow.kt index d0f2c48d96..a3d36015f5 100644 --- a/samples/tutorial/tutorial-final/src/main/java/workflow/tutorial/TodoEditWorkflow.kt +++ b/samples/tutorial/tutorial-final/src/main/java/workflow/tutorial/TodoEditWorkflow.kt @@ -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.EditProps import workflow.tutorial.TodoEditWorkflow.State +@OptIn(WorkflowUiExperimentalApi::class) object TodoEditWorkflow : StatefulWorkflow() { data class EditProps( @@ -54,33 +56,20 @@ object TodoEditWorkflow : StatefulWorkflow Unit`. */ +@OptIn(WorkflowUiExperimentalApi::class) data class TodoListScreen( val username: String, val todoTitles: List, val onTodoSelected: (Int) -> Unit, - val onBack: () -> Unit -) + val onBackClick: () -> Unit, + val onAddClick: () -> Unit +): AndroidScreen { + override val viewFactory = + ScreenViewFactory.fromViewBinding(TodoListViewBinding::inflate, ::TodoListScreenRunner) +} diff --git a/samples/tutorial/tutorial-final/src/main/java/workflow/tutorial/TodoListLayoutRunner.kt b/samples/tutorial/tutorial-final/src/main/java/workflow/tutorial/TodoListScreenRunner.kt similarity index 65% rename from samples/tutorial/tutorial-final/src/main/java/workflow/tutorial/TodoListLayoutRunner.kt rename to samples/tutorial/tutorial-final/src/main/java/workflow/tutorial/TodoListScreenRunner.kt index 7f1b4e85e4..56359d9d67 100644 --- a/samples/tutorial/tutorial-final/src/main/java/workflow/tutorial/TodoListLayoutRunner.kt +++ b/samples/tutorial/tutorial-final/src/main/java/workflow/tutorial/TodoListScreenRunner.kt @@ -1,19 +1,17 @@ package workflow.tutorial import androidx.recyclerview.widget.LinearLayoutManager -import com.squareup.workflow1.ui.LayoutRunner -import com.squareup.workflow1.ui.LayoutRunner.Companion.bind +import com.squareup.workflow1.ui.ScreenViewRunner import com.squareup.workflow1.ui.ViewEnvironment -import com.squareup.workflow1.ui.ViewFactory import com.squareup.workflow1.ui.WorkflowUiExperimentalApi -import com.squareup.workflow1.ui.backPressedHandler +import com.squareup.workflow1.ui.setBackHandler import workflow.tutorial.views.TodoListAdapter import workflow.tutorial.views.databinding.TodoListViewBinding @OptIn(WorkflowUiExperimentalApi::class) -class TodoListLayoutRunner( +class TodoListScreenRunner( private val todoListBinding: TodoListViewBinding -) : LayoutRunner { +) : ScreenViewRunner { private val adapter = TodoListAdapter() @@ -24,9 +22,10 @@ class TodoListLayoutRunner( override fun showRendering( rendering: TodoListScreen, - viewEnvironment: ViewEnvironment + environment: ViewEnvironment ) { - todoListBinding.root.backPressedHandler = rendering.onBack + todoListBinding.root.setBackHandler(rendering.onBackClick) + todoListBinding.add.setOnClickListener { rendering.onAddClick() } with(todoListBinding.todoListWelcome) { text = resources.getString(R.string.todo_list_welcome, rendering.username) @@ -36,8 +35,4 @@ class TodoListLayoutRunner( adapter.onTodoSelected = rendering.onTodoSelected adapter.notifyDataSetChanged() } - - companion object : ViewFactory by bind( - TodoListViewBinding::inflate, ::TodoListLayoutRunner - ) } diff --git a/samples/tutorial/tutorial-final/src/main/java/workflow/tutorial/TodoListWorkflow.kt b/samples/tutorial/tutorial-final/src/main/java/workflow/tutorial/TodoListWorkflow.kt index b492cdfb05..af9eb23a4f 100644 --- a/samples/tutorial/tutorial-final/src/main/java/workflow/tutorial/TodoListWorkflow.kt +++ b/samples/tutorial/tutorial-final/src/main/java/workflow/tutorial/TodoListWorkflow.kt @@ -29,14 +29,15 @@ object TodoListWorkflow : StatelessWorkflow() ): TodoListScreen { val titles = renderProps.todos.map { it.title } return TodoListScreen( - username = renderProps.username, - todoTitles = titles, - onTodoSelected = { context.actionSink.send(selectTodo(it)) }, - onBack = { context.actionSink.send(onBack()) } + username = renderProps.username, + todoTitles = titles.map { it.textValue }, + onTodoSelected = { context.actionSink.send(selectTodo(it)) }, + onBackClick = { context.actionSink.send(postGoBack) }, + onAddClick = { context.actionSink.send(postNewTodo) } ) } - private fun onBack() = action { + private val postGoBack = action { // When an onBack action is received, emit a Back output. setOutput(Back) } @@ -46,7 +47,7 @@ object TodoListWorkflow : StatelessWorkflow() setOutput(SelectTodo(index)) } - private fun new() = action { + private val postNewTodo = action { // Tell our parent a new todo item should be created. setOutput(NewTodo) } diff --git a/samples/tutorial/tutorial-final/src/main/java/workflow/tutorial/TodoModel.kt b/samples/tutorial/tutorial-final/src/main/java/workflow/tutorial/TodoModel.kt index d9eb22fef2..5461761ed8 100644 --- a/samples/tutorial/tutorial-final/src/main/java/workflow/tutorial/TodoModel.kt +++ b/samples/tutorial/tutorial-final/src/main/java/workflow/tutorial/TodoModel.kt @@ -1,6 +1,15 @@ package workflow.tutorial +import com.squareup.workflow1.ui.TextController +import com.squareup.workflow1.ui.WorkflowUiExperimentalApi + +@OptIn(WorkflowUiExperimentalApi::class) data class TodoModel( - val title: String, - val note: String -) + val title: TextController, + val note: TextController +) { + constructor( + title: String, + note: String + ) : this(TextController(title), TextController(note)) +} diff --git a/samples/tutorial/tutorial-final/src/main/java/workflow/tutorial/TodoWorkflow.kt b/samples/tutorial/tutorial-final/src/main/java/workflow/tutorial/TodoWorkflow.kt index 287e37ec0b..7ae79932d6 100644 --- a/samples/tutorial/tutorial-final/src/main/java/workflow/tutorial/TodoWorkflow.kt +++ b/samples/tutorial/tutorial-final/src/main/java/workflow/tutorial/TodoWorkflow.kt @@ -3,6 +3,7 @@ package workflow.tutorial import com.squareup.workflow1.Snapshot import com.squareup.workflow1.StatefulWorkflow import com.squareup.workflow1.action +import com.squareup.workflow1.ui.Screen import com.squareup.workflow1.ui.WorkflowUiExperimentalApi import workflow.tutorial.TodoEditWorkflow.EditProps import workflow.tutorial.TodoEditWorkflow.Output.Discard @@ -17,7 +18,7 @@ import workflow.tutorial.TodoWorkflow.State.Step import workflow.tutorial.TodoWorkflow.TodoProps @OptIn(WorkflowUiExperimentalApi::class) -object TodoWorkflow : StatefulWorkflow>() { +object TodoWorkflow : StatefulWorkflow>() { data class TodoProps(val name: String) @@ -43,27 +44,27 @@ object TodoWorkflow : StatefulWorkflow>() { props: TodoProps, snapshot: Snapshot? ) = State( - todos = listOf( - TodoModel( - title = "Take the cat for a walk", - note = "Cats really need their outside sunshine time. Don't forget to walk " + - "Charlie. Hamilton is less excited about the prospect." - ) - ), - step = Step.List + todos = listOf( + TodoModel( + title = "Take the cat for a walk", + note = "Cats really need their outside sunshine time. Don't forget to walk " + + "Charlie. Hamilton is less excited about the prospect." + ) + ), + step = Step.List ) override fun render( renderProps: TodoProps, renderState: State, context: RenderContext - ): List { + ): List { val todoListScreen = context.renderChild( - TodoListWorkflow, - props = ListProps( - username = renderProps.name, - todos = renderState.todos - ) + TodoListWorkflow, + props = ListProps( + username = renderProps.name, + todos = renderState.todos + ) ) { output -> when (output) { Output.Back -> onBack() @@ -78,8 +79,8 @@ object TodoWorkflow : StatefulWorkflow>() { is Step.Edit -> { // On the "edit" step, return both the list and edit screens. val todoEditScreen = context.renderChild( - TodoEditWorkflow, - EditProps(renderState.todos[step.index]) + TodoEditWorkflow, + EditProps(renderState.todos[step.index]) ) { output -> when (output) { // Send the discardChanges action when the discard output is received. @@ -108,10 +109,10 @@ object TodoWorkflow : StatefulWorkflow>() { private fun newTodo() = action { // Append a new todo model to the end of the list. state = state.copy( - todos = state.todos + TodoModel( - title = "New Todo", - note = "" - ) + todos = state.todos + TodoModel( + title = "New Todo", + note = "" + ) ) } @@ -126,8 +127,8 @@ object TodoWorkflow : StatefulWorkflow>() { ) = action { // When changes are saved, update the state of that todo item and return to the list. state = state.copy( - todos = state.todos.toMutableList().also { it[index] = todo }, - step = Step.List + todos = state.todos.toMutableList().also { it[index] = todo }, + step = Step.List ) } } diff --git a/samples/tutorial/tutorial-final/src/main/java/workflow/tutorial/TutorialActivity.kt b/samples/tutorial/tutorial-final/src/main/java/workflow/tutorial/TutorialActivity.kt index 99c67c824c..86071497fc 100644 --- a/samples/tutorial/tutorial-final/src/main/java/workflow/tutorial/TutorialActivity.kt +++ b/samples/tutorial/tutorial-final/src/main/java/workflow/tutorial/TutorialActivity.kt @@ -1,5 +1,3 @@ -@file:OptIn(WorkflowUiExperimentalApi::class) - package workflow.tutorial import android.os.Bundle @@ -8,20 +6,13 @@ import androidx.appcompat.app.AppCompatActivity import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope -import com.squareup.workflow1.ui.ViewRegistry +import com.squareup.workflow1.ui.Screen 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 -) - +@OptIn(WorkflowUiExperimentalApi::class) class TutorialActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { @@ -32,17 +23,19 @@ class TutorialActivity : AppCompatActivity() { val model: TutorialViewModel by viewModels() setContentView( - WorkflowLayout(this).apply { start(model.renderings, viewRegistry) } + WorkflowLayout(this).apply { + take(lifecycle, model.renderings) + } ) } -} -class TutorialViewModel(savedState: SavedStateHandle) : ViewModel() { - val renderings: StateFlow by lazy { - renderWorkflowIn( - workflow = RootWorkflow, - scope = viewModelScope, - savedStateHandle = savedState - ) + class TutorialViewModel(savedState: SavedStateHandle) : ViewModel() { + val renderings: StateFlow by lazy { + renderWorkflowIn( + workflow = RootWorkflow, + scope = viewModelScope, + savedStateHandle = savedState + ) + } } } diff --git a/samples/tutorial/tutorial-final/src/main/java/workflow/tutorial/WelcomeLayoutRunner.kt b/samples/tutorial/tutorial-final/src/main/java/workflow/tutorial/WelcomeLayoutRunner.kt deleted file mode 100644 index 7eb9af6340..0000000000 --- a/samples/tutorial/tutorial-final/src/main/java/workflow/tutorial/WelcomeLayoutRunner.kt +++ /dev/null @@ -1,38 +0,0 @@ -package workflow.tutorial - -import com.squareup.workflow1.ui.LayoutRunner -import com.squareup.workflow1.ui.LayoutRunner.Companion.bind -import com.squareup.workflow1.ui.ViewEnvironment -import com.squareup.workflow1.ui.ViewFactory -import com.squareup.workflow1.ui.WorkflowUiExperimentalApi -import com.squareup.workflow1.ui.setTextChangedListener -import com.squareup.workflow1.ui.updateText -import workflow.tutorial.views.databinding.WelcomeViewBinding - -@OptIn(WorkflowUiExperimentalApi::class) -class WelcomeLayoutRunner( - private val welcomeBinding: WelcomeViewBinding -) : LayoutRunner { - - override fun showRendering( - rendering: WelcomeScreen, - viewEnvironment: ViewEnvironment - ) { - // updateText and setTextChangedListener are helpers provided by the workflow library that take - // care of the complexity of correctly interacting with EditTexts in a declarative manner. - welcomeBinding.username.updateText(rendering.username) - welcomeBinding.username.setTextChangedListener { - rendering.onUsernameChanged(it.toString()) - } - welcomeBinding.login.setOnClickListener { rendering.onLoginTapped() } - } - - /** - * Define a [ViewFactory] that will inflate an instance of [WelcomeViewBinding] and an instance - * of [WelcomeLayoutRunner] when asked, then wire them up so that [showRendering] will be called - * whenever the workflow emits a new [WelcomeScreen]. - */ - companion object : ViewFactory by bind( - WelcomeViewBinding::inflate, ::WelcomeLayoutRunner - ) -} diff --git a/samples/tutorial/tutorial-final/src/main/java/workflow/tutorial/WelcomeScreen.kt b/samples/tutorial/tutorial-final/src/main/java/workflow/tutorial/WelcomeScreen.kt index af5918e4bc..28f868dc5e 100644 --- a/samples/tutorial/tutorial-final/src/main/java/workflow/tutorial/WelcomeScreen.kt +++ b/samples/tutorial/tutorial-final/src/main/java/workflow/tutorial/WelcomeScreen.kt @@ -1,10 +1,18 @@ 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.WelcomeViewBinding + +@OptIn(WorkflowUiExperimentalApi::class) data class WelcomeScreen( /** The current name that has been entered. */ - val username: String, - /** Callback when the name changes in the UI. */ - val onUsernameChanged: (String) -> Unit, + val username: TextController, /** Callback when the login button is tapped. */ val onLoginTapped: () -> Unit -) +) : AndroidScreen { + override val viewFactory = + ScreenViewFactory.fromViewBinding(WelcomeViewBinding::inflate, ::WelcomeScreenRunner) +} diff --git a/samples/tutorial/tutorial-final/src/main/java/workflow/tutorial/WelcomeScreenRunner.kt b/samples/tutorial/tutorial-final/src/main/java/workflow/tutorial/WelcomeScreenRunner.kt new file mode 100644 index 0000000000..92a9695747 --- /dev/null +++ b/samples/tutorial/tutorial-final/src/main/java/workflow/tutorial/WelcomeScreenRunner.kt @@ -0,0 +1,23 @@ +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 workflow.tutorial.views.databinding.WelcomeViewBinding + +@OptIn(WorkflowUiExperimentalApi::class) +class WelcomeScreenRunner( + private val welcomeBinding: WelcomeViewBinding +) : ScreenViewRunner { + + override fun showRendering( + rendering: WelcomeScreen, + environment: ViewEnvironment + ) { + // TextController is a helper provided by the workflow library that takes + // care of the complexity of correctly interacting with EditTexts in a declarative manner. + rendering.username.control(welcomeBinding.username) + welcomeBinding.login.setOnClickListener { rendering.onLoginTapped() } + } +} diff --git a/samples/tutorial/tutorial-final/src/main/java/workflow/tutorial/WelcomeWorkflow.kt b/samples/tutorial/tutorial-final/src/main/java/workflow/tutorial/WelcomeWorkflow.kt index d59983c393..95feba0664 100644 --- a/samples/tutorial/tutorial-final/src/main/java/workflow/tutorial/WelcomeWorkflow.kt +++ b/samples/tutorial/tutorial-final/src/main/java/workflow/tutorial/WelcomeWorkflow.kt @@ -3,13 +3,16 @@ package workflow.tutorial import com.squareup.workflow1.Snapshot import com.squareup.workflow1.StatefulWorkflow import com.squareup.workflow1.action +import com.squareup.workflow1.ui.TextController +import com.squareup.workflow1.ui.WorkflowUiExperimentalApi import workflow.tutorial.WelcomeWorkflow.LoggedIn import workflow.tutorial.WelcomeWorkflow.State +@OptIn(WorkflowUiExperimentalApi::class) object WelcomeWorkflow : StatefulWorkflow() { data class State( - val name: String + val name: TextController ) data class LoggedIn(val username: String) @@ -17,30 +20,24 @@ object WelcomeWorkflow : StatefulWorkflow( override fun initialState( props: Unit, snapshot: Snapshot? - ): State = State(name = "") + ): State = State(name = TextController("")) override fun render( renderProps: Unit, renderState: State, context: RenderContext ): WelcomeScreen = WelcomeScreen( - username = renderState.name, - onUsernameChanged = { context.actionSink.send(onNameChanged(it)) }, - onLoginTapped = { - // Whenever the login button is tapped, emit the onLogin action. - context.actionSink.send(onLogin()) - } + username = renderState.name, + onLoginTapped = { + // Whenever the login button is tapped, emit the onLogin action. + context.actionSink.send(onLogin()) + } ) - // Needs to be internal so we can access it from the tests. - internal fun onNameChanged(name: String) = action { - state = state.copy(name = name) - } - internal fun onLogin() = action { // Don't log in if the name isn't filled in. - if (state.name.isNotEmpty()) { - setOutput(LoggedIn(state.name)) + state.name.textValue.takeIf { it.isNotEmpty() }?.let { + setOutput(LoggedIn(it)) } } diff --git a/samples/tutorial/tutorial-final/src/test/java/workflow/tutorial/RootWorkflowTest.kt b/samples/tutorial/tutorial-final/src/test/java/workflow/tutorial/RootWorkflowTest.kt index f7446036f1..e83b8fb5bb 100644 --- a/samples/tutorial/tutorial-final/src/test/java/workflow/tutorial/RootWorkflowTest.kt +++ b/samples/tutorial/tutorial-final/src/test/java/workflow/tutorial/RootWorkflowTest.kt @@ -4,8 +4,9 @@ import com.squareup.workflow1.WorkflowOutput import com.squareup.workflow1.testing.expectWorkflow import com.squareup.workflow1.testing.launchForTestingFromStartWith import com.squareup.workflow1.testing.testRender +import com.squareup.workflow1.ui.TextController import com.squareup.workflow1.ui.WorkflowUiExperimentalApi -import com.squareup.workflow1.ui.backstack.BackStackScreen +import com.squareup.workflow1.ui.container.BackStackScreen import workflow.tutorial.RootWorkflow.State.Todo import workflow.tutorial.RootWorkflow.State.Welcome import workflow.tutorial.WelcomeWorkflow.LoggedIn @@ -27,8 +28,7 @@ class RootWorkflowTest { .expectWorkflow( workflowType = WelcomeWorkflow::class, rendering = WelcomeScreen( - username = "Ada", - onUsernameChanged = {}, + username = TextController("Ada"), onLoginTapped = {} ) ) @@ -39,7 +39,7 @@ class RootWorkflowTest { assertEquals(1, backstack.size) val welcomeScreen = backstack[0] as WelcomeScreen - assertEquals("Ada", welcomeScreen.username) + assertEquals("Ada", welcomeScreen.username.textValue) } // Assert that no action was produced during this render, meaning our state remains unchanged .verifyActionResult { _, output -> @@ -55,8 +55,7 @@ class RootWorkflowTest { .expectWorkflow( workflowType = WelcomeWorkflow::class, rendering = WelcomeScreen( - username = "Ada", - onUsernameChanged = {}, + username = TextController("Ada"), onLoginTapped = {} ), // Simulate the WelcomeWorkflow sending an output of LoggedIn as if the "log in" button @@ -70,7 +69,7 @@ class RootWorkflowTest { assertEquals(1, backstack.size) val welcomeScreen = backstack[0] as WelcomeScreen - assertEquals("Ada", welcomeScreen.username) + assertEquals("Ada", welcomeScreen.username.textValue) } // Assert that the state transitioned to Todo. .verifyActionResult { newState, _ -> @@ -89,15 +88,8 @@ class RootWorkflowTest { assertEquals(1, rendering.frames.size) val welcomeScreen = rendering.frames[0] as WelcomeScreen - // Enter a name. - welcomeScreen.onUsernameChanged("Ada") - } - - // Log in and go to the todo list. - awaitNextRendering().let { rendering -> - assertEquals(1, rendering.frames.size) - val welcomeScreen = rendering.frames[0] as WelcomeScreen - + // Enter a name and tap login + welcomeScreen.username.textValue = "Ada" welcomeScreen.onLoginTapped() } @@ -119,19 +111,9 @@ class RootWorkflowTest { assertTrue(rendering.frames[1] is TodoListScreen) val editScreen = rendering.frames[2] as TodoEditScreen - // Update the title. - editScreen.onTitleChanged("New Title") - } - - // Save the selected todo. - awaitNextRendering().let { rendering -> - assertEquals(3, rendering.frames.size) - assertTrue(rendering.frames[0] is WelcomeScreen) - assertTrue(rendering.frames[1] is TodoListScreen) - val editScreen = rendering.frames[2] as TodoEditScreen - - // Save the changes by tapping the save button. - editScreen.saveChanges() + // Enter a title and save. + editScreen.title.textValue = "New Title" + editScreen.onSaveClick() } // Expect the todo list. Validate the title was updated. diff --git a/samples/tutorial/tutorial-final/src/test/java/workflow/tutorial/TodoEditWorkflowTest.kt b/samples/tutorial/tutorial-final/src/test/java/workflow/tutorial/TodoEditWorkflowTest.kt index 1572de6230..cdedc47c99 100644 --- a/samples/tutorial/tutorial-final/src/test/java/workflow/tutorial/TodoEditWorkflowTest.kt +++ b/samples/tutorial/tutorial-final/src/test/java/workflow/tutorial/TodoEditWorkflowTest.kt @@ -1,49 +1,29 @@ package workflow.tutorial import com.squareup.workflow1.applyTo +import com.squareup.workflow1.ui.WorkflowUiExperimentalApi import org.junit.Test import workflow.tutorial.TodoEditWorkflow.EditProps import workflow.tutorial.TodoEditWorkflow.Output.Save import workflow.tutorial.TodoEditWorkflow.State import kotlin.test.assertEquals -import kotlin.test.assertNull +@OptIn(WorkflowUiExperimentalApi::class) class TodoEditWorkflowTest { // Start with a todo of "Title" "Note" private val startState = State(todo = TodoModel(title = "Title", note = "Note")) - @Test fun `title is updated`() { - // These will be ignored by the action. - val props = EditProps(TodoModel(title = "", note = "")) - - // Update the title to "Updated Title" - val (newState, actionApplied) = TodoEditWorkflow.onTitleChanged("Updated Title") - .applyTo(props, startState) - - assertNull(actionApplied.output) - assertEquals(TodoModel(title = "Updated Title", note = "Note"), newState.todo) - } - - @Test fun `note is updated`() { - // These will be ignored by the action. - val props = EditProps(TodoModel(title = "", note = "")) - - // Update the note to "Updated Note" - val (newState, actionApplied) = TodoEditWorkflow.onNoteChanged("Updated Note") - .applyTo(props, startState) - - assertNull(actionApplied.output) - assertEquals(TodoModel(title = "Title", note = "Updated Note"), newState.todo) - } - @Test fun `save emits model`() { val props = EditProps(TodoModel(title = "Title", note = "Note")) - val (_, actionApplied) = TodoEditWorkflow.onSave() + val (_, actionApplied) = TodoEditWorkflow.postSave .applyTo(props, startState) - assertEquals(Save(TodoModel(title = "Title", note = "Note")), actionApplied.output?.value) + val expected = Save(TodoModel(title = "Title", note = "Note")).todo + val actual = (actionApplied.output?.value as Save).todo + assertEquals(expected.title.textValue, actual.title.textValue) + assertEquals(expected.note.textValue, actual.note.textValue) } @Test fun `changed props updated local state`() { @@ -51,22 +31,22 @@ class TodoEditWorkflowTest { var state = TodoEditWorkflow.initialState(initialProps, null) // The initial state is a copy of the provided todo: - assertEquals("Title", state.todo.title) - assertEquals("Note", state.todo.note) + assertEquals("Title", state.todo.title.textValue) + assertEquals("Note", state.todo.note.textValue) // Create a new internal state, simulating the change from actions: state = State(TodoModel(title = "Updated Title", note = "Note")) // Update the workflow properties with the same value. The state should not be updated: state = TodoEditWorkflow.onPropsChanged(initialProps, initialProps, state) - assertEquals("Updated Title", state.todo.title) - assertEquals("Note", state.todo.note) + assertEquals("Updated Title", state.todo.title.textValue) + assertEquals("Note", state.todo.note.textValue) // The parent provided different properties. The internal state should be updated with the // newly-provided properties. val updatedProps = EditProps(initialTodo = TodoModel(title = "New Title", note = "New Note")) state = TodoEditWorkflow.onPropsChanged(initialProps, updatedProps, state) - assertEquals("New Title", state.todo.title) - assertEquals("New Note", state.todo.note) + assertEquals("New Title", state.todo.title.textValue) + assertEquals("New Note", state.todo.note.textValue) } } diff --git a/samples/tutorial/tutorial-final/src/test/java/workflow/tutorial/TodoWorkflowTest.kt b/samples/tutorial/tutorial-final/src/test/java/workflow/tutorial/TodoWorkflowTest.kt index 0cddadd52b..9214615a4f 100644 --- a/samples/tutorial/tutorial-final/src/test/java/workflow/tutorial/TodoWorkflowTest.kt +++ b/samples/tutorial/tutorial-final/src/test/java/workflow/tutorial/TodoWorkflowTest.kt @@ -3,6 +3,7 @@ package workflow.tutorial import com.squareup.workflow1.WorkflowOutput import com.squareup.workflow1.testing.expectWorkflow import com.squareup.workflow1.testing.testRender +import com.squareup.workflow1.ui.TextController import com.squareup.workflow1.ui.WorkflowUiExperimentalApi import workflow.tutorial.TodoEditWorkflow.Output.Save import workflow.tutorial.TodoListWorkflow.Output.SelectTodo @@ -20,102 +21,118 @@ class TodoWorkflowTest { val todos = listOf(TodoModel(title = "Title", note = "Note")) TodoWorkflow - .testRender( - props = TodoProps(name = "Ada"), - // Start from the list step to validate selecting a todo. - initialState = State( - todos = todos, - step = List - ) + .testRender( + props = TodoProps(name = "Ada"), + // Start from the list step to validate selecting a todo. + initialState = State( + todos = todos, + step = List ) - // We only expect the TodoListWorkflow to be rendered. - .expectWorkflow( - workflowType = TodoListWorkflow::class, - rendering = TodoListScreen( - username = "", - todoTitles = listOf("Title"), - onTodoSelected = {}, - onBack = {} - ), - // Simulate selecting the first todo. - output = WorkflowOutput(SelectTodo(index = 0)) + ) + // We only expect the TodoListWorkflow to be rendered. + .expectWorkflow( + workflowType = TodoListWorkflow::class, + rendering = TodoListScreen( + username = "", + todoTitles = listOf("Title"), + onTodoSelected = {}, + onBackClick = {}, + onAddClick = {} + ), + // Simulate selecting the first todo. + output = WorkflowOutput(SelectTodo(index = 0)) + ) + .render { rendering -> + // Just validate that there is one item in the back stack. + // Additional validation could be done on the screens returned, if desired. + assertEquals(1, rendering.size) + } + // Assert that the state was updated after the render pass with the output from the + // TodoListWorkflow. + .verifyActionResult { newState, _ -> + assertEqualState( + State( + todos = listOf(TodoModel(title = "Title", note = "Note")), + step = Edit(0) + ), newState ) - .render { rendering -> - // Just validate that there is one item in the back stack. - // Additional validation could be done on the screens returned, if desired. - assertEquals(1, rendering.size) - } - // Assert that the state was updated after the render pass with the output from the - // TodoListWorkflow. - .verifyActionResult { newState, _ -> - assertEquals( - State( - todos = listOf(TodoModel(title = "Title", note = "Note")), - step = Edit(0) - ), - newState - ) - } + } } @Test fun `saving todo`() { val todos = listOf(TodoModel(title = "Title", note = "Note")) TodoWorkflow - .testRender( - props = TodoProps(name = "Ada"), - // Start from the edit step so we can simulate saving. - initialState = State( - todos = todos, - step = Edit(index = 0) - ) + .testRender( + props = TodoProps(name = "Ada"), + // Start from the edit step so we can simulate saving. + initialState = State( + todos = todos, + step = Edit(index = 0) ) - // We always expect the TodoListWorkflow to be rendered. - .expectWorkflow( - workflowType = TodoListWorkflow::class, - rendering = TodoListScreen( - username = "", - todoTitles = listOf("Title"), - onTodoSelected = {}, - onBack = {} - ) + ) + // We always expect the TodoListWorkflow to be rendered. + .expectWorkflow( + workflowType = TodoListWorkflow::class, + rendering = TodoListScreen( + username = "", + todoTitles = listOf("Title"), + onTodoSelected = {}, + onBackClick = {}, + onAddClick = {} ) - // Expect the TodoEditWorkflow to be rendered as well (as we're on the edit step). - .expectWorkflow( - workflowType = TodoEditWorkflow::class, - rendering = TodoEditScreen( - title = "Title", - note = "Note", - onTitleChanged = {}, - onNoteChanged = {}, - discardChanges = {}, - saveChanges = {} - ), - // Simulate it emitting an output of `.save` to update the state. - output = WorkflowOutput( - Save( - TodoModel( - title = "Updated Title", - note = "Updated Note" - ) - ) + ) + // Expect the TodoEditWorkflow to be rendered as well (as we're on the edit step). + .expectWorkflow( + workflowType = TodoEditWorkflow::class, + rendering = TodoEditScreen( + title = TextController("Title"), + note = TextController("Note"), + onBackClick = {}, + onSaveClick = {} + ), + // Simulate it emitting an output of `.save` to update the state. + output = WorkflowOutput( + Save( + TodoModel( + title = "Updated Title", + note = "Updated Note" ) - ) - .render { rendering -> - // Just validate that there are two items in the back stack. - // Additional validation could be done on the screens returned, if desired. - assertEquals(2, rendering.size) - } - // Validate that the state was updated after the render pass with the output from the - // TodoEditWorkflow. - .verifyActionResult { newState, _ -> - assertEquals( - State( - todos = listOf(TodoModel(title = "Updated Title", note = "Updated Note")), - step = List - ), - newState ) - } + ) + ) + .render { rendering -> + // Just validate that there are two items in the back stack. + // Additional validation could be done on the screens returned, if desired. + assertEquals(2, rendering.size) + } + // Validate that the state was updated after the render pass with the output from the + // TodoEditWorkflow. + .verifyActionResult { newState, _ -> + assertEqualState( + State( + todos = listOf(TodoModel(title = "Updated Title", note = "Updated Note")), + step = List + ), + newState + ) + } + } + + private fun assertEqualState(expected: State, actual: State) { + assertEquals(expected.todos.size, actual.todos.size) + expected.todos.forEachIndexed { index, todo -> + assertEquals( + expected.todos[index].title.textValue, + actual.todos[index].title.textValue, + "todos[$index].title" + ) + assertEquals( + expected.todos[index].note.textValue, + actual.todos[index].note.textValue, + "todos[$index].note" + ) + } + assertEquals(expected.step, actual.step) } } diff --git a/samples/tutorial/tutorial-final/src/test/java/workflow/tutorial/WelcomeWorkflowTest.kt b/samples/tutorial/tutorial-final/src/test/java/workflow/tutorial/WelcomeWorkflowTest.kt index c39748c4df..99ebf9b370 100644 --- a/samples/tutorial/tutorial-final/src/test/java/workflow/tutorial/WelcomeWorkflowTest.kt +++ b/samples/tutorial/tutorial-final/src/test/java/workflow/tutorial/WelcomeWorkflowTest.kt @@ -2,29 +2,20 @@ package workflow.tutorial import com.squareup.workflow1.applyTo import com.squareup.workflow1.testing.testRender +import com.squareup.workflow1.ui.TextController +import com.squareup.workflow1.ui.WorkflowUiExperimentalApi import org.junit.Test import workflow.tutorial.WelcomeWorkflow.LoggedIn import kotlin.test.assertEquals import kotlin.test.assertNull +@OptIn(WorkflowUiExperimentalApi::class) class WelcomeWorkflowTest { // region Actions - @Test fun `name updates`() { - val startState = WelcomeWorkflow.State("") - val action = WelcomeWorkflow.onNameChanged("myName") - val (state, actionApplied) = action.applyTo(state = startState, props = Unit) - - // No output is expected when the name changes. - assertNull(actionApplied.output) - - // The name has been updated from the action. - assertEquals("myName", state.name) - } - @Test fun `login works`() { - val startState = WelcomeWorkflow.State("myName") + val startState = WelcomeWorkflow.State(TextController("myName")) val action = WelcomeWorkflow.onLogin() val (_, actionApplied) = action.applyTo(state = startState, props = Unit) @@ -33,14 +24,14 @@ class WelcomeWorkflowTest { } @Test fun `login does nothing when name is empty`() { - val startState = WelcomeWorkflow.State("") + val startState = WelcomeWorkflow.State(TextController("")) val action = WelcomeWorkflow.onLogin() val (state, actionApplied) = action.applyTo(state = startState, props = Unit) // Since the name is empty, onLogin will not emit an output. assertNull(actionApplied.output) // The name is empty, as was specified in the initial state. - assertEquals("", state.name) + assertEquals("", state.name.textValue) } // endregion @@ -51,7 +42,7 @@ class WelcomeWorkflowTest { // Use the initial state provided by the welcome workflow. WelcomeWorkflow.testRender(props = Unit) .render { screen -> - assertEquals("", screen.username) + assertEquals("", screen.username.textValue) // Simulate tapping the log in button. No output will be emitted, as the name is empty. screen.onLoginTapped() @@ -61,25 +52,11 @@ class WelcomeWorkflowTest { } } - @Test fun `rendering name change`() { - // Use the initial state provided by the welcome workflow. - WelcomeWorkflow.testRender(props = Unit) - // Next, simulate the name updating, expecting the state to be changed to reflect the - // updated name. - .render { screen -> - screen.onUsernameChanged("Ada") - } - .verifyActionResult { state, _ -> - // https://github.com/square/workflow-kotlin/issues/230 - assertEquals("Ada", (state as WelcomeWorkflow.State).name) - } - } - @Test fun `rendering login`() { // Start with a name already entered. WelcomeWorkflow .testRender( - initialState = WelcomeWorkflow.State(name = "Ada"), + initialState = WelcomeWorkflow.State(name = TextController("Ada")), props = Unit ) // Simulate a log in button tap. diff --git a/samples/tutorial/tutorial-views/src/main/res/layout/todo_list_view.xml b/samples/tutorial/tutorial-views/src/main/res/layout/todo_list_view.xml index e26318102b..974e15c61e 100644 --- a/samples/tutorial/tutorial-views/src/main/res/layout/todo_list_view.xml +++ b/samples/tutorial/tutorial-views/src/main/res/layout/todo_list_view.xml @@ -1,9 +1,25 @@ - + > + +