Skip to content

Commit

Permalink
wip: getting nav consistent, pause to improve BackStackScreen types
Browse files Browse the repository at this point in the history
  • Loading branch information
rjrjr committed Oct 24, 2023
1 parent b996c29 commit 79a66f9
Show file tree
Hide file tree
Showing 16 changed files with 72 additions and 73 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,13 @@ import com.squareup.workflow1.renderChild
import com.squareup.workflow1.ui.Screen
import com.squareup.workflow1.ui.WorkflowUiExperimentalApi
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
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 RootWorkflow : StatefulWorkflow<Unit, State, Nothing, BackStackScreen<Screen>>() {
object RootNavigationWorkflow : StatefulWorkflow<Unit, State, Nothing, BackStackScreen<*>>() {

sealed class State {
object Welcome : State()
Expand All @@ -31,37 +30,29 @@ object RootWorkflow : StatefulWorkflow<Unit, State, Nothing, BackStackScreen<Scr
renderProps: Unit,
renderState: State,
context: RenderContext
): BackStackScreen<Screen> {

// Our list of back stack items. Will always include the "WelcomeScreen".
val backstackScreens = mutableListOf<Screen>()

): BackStackScreen<*> {
// 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.
val welcomeScreen = context.renderChild(WelcomeWorkflow) { output ->
// When WelcomeWorkflow emits LoggedIn, turn it into our login action.
login(output.username)
}
backstackScreens += welcomeScreen

when (renderState) {
return when (renderState) {
// When the state is Welcome, defer to the WelcomeWorkflow.
is Welcome -> {
// We always add the welcome screen to the backstack, so this is a no op.
BackStackScreen(welcomeScreen)
}

// When the state is Todo, defer to the TodoListWorkflow.
is Todo -> {
val todoListScreens = context.renderChild(TodoWorkflow, TodoProps(renderState.username)) {
val todoBackStack = context.renderChild(TodoNavigationWorkflow, TodoProps(renderState.username)) {
// When receiving a Back output, treat it as a logout action.
logout
}
backstackScreens.addAll(todoListScreens)
BackStackScreen(welcomeScreen) + todoBackStack
}
}

// Finally, return the BackStackScreen with a list of BackStackScreen.Items
return backstackScreens.toBackStackScreen()
}

override fun snapshotState(state: State): Snapshot? = null
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,8 @@ class TodoEditRunner(
rendering: TodoEditScreen,
environment: ViewEnvironment
) {
binding.root.setBackHandler(rendering.onBackClick)
binding.save.setOnClickListener { rendering.onSaveClick() }
binding.root.setBackHandler(rendering.onBackPressed)
binding.save.setOnClickListener { rendering.onSavePressed() }
rendering.title.control(binding.todoTitle)
rendering.note.control(binding.todoNote)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,8 @@ data class TodoEditScreen(
/** The contents, or "note" of the todo. */
val note: TextController,

val onBackClick: () -> Unit,
val onSaveClick: () -> Unit
val onBackPressed: () -> Unit,
val onSavePressed: () -> Unit
) : AndroidScreen<TodoEditScreen> {
override val viewFactory =
ScreenViewFactory.fromViewBinding(TodoEditViewBinding::inflate, ::TodoEditRunner)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,8 +56,8 @@ object TodoEditWorkflow : StatefulWorkflow<EditProps, State, Output, TodoEditScr
return TodoEditScreen(
title = renderState.todo.title,
note = renderState.todo.note,
onSaveClick = { context.actionSink.send(postSave) },
onBackClick = { context.actionSink.send(postDiscard) }
onSavePressed = { context.actionSink.send(postSave) },
onBackPressed = { context.actionSink.send(postDiscard) }
)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,8 @@ data class TodoListScreen(
val username: String,
val todoTitles: List<String>,
val onTodoSelected: (Int) -> Unit,
val onBackClick: () -> Unit,
val onAddClick: () -> Unit
val onBackPressed: () -> Unit,
val onAddPressed: () -> Unit
): AndroidScreen<TodoListScreen> {
override val viewFactory =
ScreenViewFactory.fromViewBinding(TodoListViewBinding::inflate, ::TodoListScreenRunner)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,8 @@ class TodoListScreenRunner(
rendering: TodoListScreen,
environment: ViewEnvironment
) {
todoListBinding.root.setBackHandler(rendering.onBackClick)
todoListBinding.add.setOnClickListener { rendering.onAddClick() }
todoListBinding.root.setBackHandler(rendering.onBackPressed)
todoListBinding.add.setOnClickListener { rendering.onAddPressed() }

with(todoListBinding.todoListWelcome) {
text =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,8 @@ object TodoListWorkflow : StatelessWorkflow<ListProps, Output, TodoListScreen>()
username = renderProps.username,
todoTitles = titles.map { it.textValue },
onTodoSelected = { context.actionSink.send(selectTodo(it)) },
onBackClick = { context.actionSink.send(postGoBack) },
onAddClick = { context.actionSink.send(postNewTodo) }
onBackPressed = { context.actionSink.send(postGoBack) },
onAddPressed = { context.actionSink.send(postNewTodo) }
)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,20 +5,21 @@ import com.squareup.workflow1.StatefulWorkflow
import com.squareup.workflow1.action
import com.squareup.workflow1.ui.Screen
import com.squareup.workflow1.ui.WorkflowUiExperimentalApi
import com.squareup.workflow1.ui.container.BackStackScreen
import workflow.tutorial.TodoEditWorkflow.EditProps
import workflow.tutorial.TodoEditWorkflow.Output.Discard
import workflow.tutorial.TodoEditWorkflow.Output.Save
import workflow.tutorial.TodoListWorkflow.ListProps
import workflow.tutorial.TodoListWorkflow.Output
import workflow.tutorial.TodoListWorkflow.Output.NewTodo
import workflow.tutorial.TodoListWorkflow.Output.SelectTodo
import workflow.tutorial.TodoWorkflow.Back
import workflow.tutorial.TodoWorkflow.State
import workflow.tutorial.TodoWorkflow.State.Step
import workflow.tutorial.TodoWorkflow.TodoProps
import workflow.tutorial.TodoNavigationWorkflow.Back
import workflow.tutorial.TodoNavigationWorkflow.State
import workflow.tutorial.TodoNavigationWorkflow.State.Step
import workflow.tutorial.TodoNavigationWorkflow.TodoProps

@OptIn(WorkflowUiExperimentalApi::class)
object TodoWorkflow : StatefulWorkflow<TodoProps, State, Back, List<Screen>>() {
object TodoNavigationWorkflow : StatefulWorkflow<TodoProps, State, Back, BackStackScreen<Screen>>() {

data class TodoProps(val name: String)

Expand Down Expand Up @@ -58,7 +59,7 @@ object TodoWorkflow : StatefulWorkflow<TodoProps, State, Back, List<Screen>>() {
renderProps: TodoProps,
renderState: State,
context: RenderContext
): List<Screen> {
): BackStackScreen<Screen> {
val todoListScreen = context.renderChild(
TodoListWorkflow,
props = ListProps(
Expand All @@ -75,7 +76,7 @@ object TodoWorkflow : StatefulWorkflow<TodoProps, State, Back, List<Screen>>() {

return when (val step = renderState.step) {
// On the "list" step, return just the list screen.
Step.List -> listOf(todoListScreen)
Step.List -> BackStackScreen(todoListScreen)
is Step.Edit -> {
// On the "edit" step, return both the list and edit screens.
val todoEditScreen = context.renderChild(
Expand All @@ -89,7 +90,7 @@ object TodoWorkflow : StatefulWorkflow<TodoProps, State, Back, List<Screen>>() {
is Save -> saveChanges(output.todo, step.index)
}
}
return listOf(todoListScreen, todoEditScreen)
return BackStackScreen(todoListScreen, todoEditScreen)
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ class TutorialActivity : AppCompatActivity() {
class TutorialViewModel(savedState: SavedStateHandle) : ViewModel() {
val renderings: StateFlow<Screen> by lazy {
renderWorkflowIn(
workflow = RootWorkflow,
workflow = RootNavigationWorkflow,
scope = viewModelScope,
savedStateHandle = savedState
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ data class WelcomeScreen(
/** The current name that has been entered. */
val username: TextController,
/** Callback when the login button is tapped. */
val onLoginTapped: () -> Unit
val onLogInPressed: () -> Unit
) : AndroidScreen<WelcomeScreen> {
override val viewFactory =
ScreenViewFactory.fromViewBinding(WelcomeViewBinding::inflate, ::WelcomeScreenRunner)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,6 @@ class WelcomeScreenRunner(
// 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() }
welcomeBinding.login.setOnClickListener { rendering.onLogInPressed() }
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -28,13 +28,13 @@ object WelcomeWorkflow : StatefulWorkflow<Unit, State, LoggedIn, WelcomeScreen>(
context: RenderContext
): WelcomeScreen = WelcomeScreen(
username = renderState.name,
onLoginTapped = {
// Whenever the login button is tapped, emit the onLogin action.
context.actionSink.send(onLogin())
onLogInPressed = {
// Whenever the log in button is tapped, enqueue the logInAction.
context.actionSink.send(logInAction())
}
)

internal fun onLogin() = action {
internal fun logInAction() = action {
// Don't log in if the name isn't filled in.
state.name.textValue.takeIf { it.isNotEmpty() }?.let {
setOutput(LoggedIn(it))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,29 +7,29 @@ import com.squareup.workflow1.testing.testRender
import com.squareup.workflow1.ui.TextController
import com.squareup.workflow1.ui.WorkflowUiExperimentalApi
import com.squareup.workflow1.ui.container.BackStackScreen
import workflow.tutorial.RootWorkflow.State.Todo
import workflow.tutorial.RootWorkflow.State.Welcome
import workflow.tutorial.RootNavigationWorkflow.State.Todo
import workflow.tutorial.RootNavigationWorkflow.State.Welcome
import workflow.tutorial.WelcomeWorkflow.LoggedIn
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertNull
import kotlin.test.assertTrue

@OptIn(WorkflowUiExperimentalApi::class)
class RootWorkflowTest {
class RootNavigationWorkflowTest {

// region Render

@Test fun `welcome rendering`() {
RootWorkflow
RootNavigationWorkflow
// Start in the Welcome state
.testRender(initialState = Welcome, props = Unit)
// The `WelcomeWorkflow` is expected to be started in this render.
.expectWorkflow(
workflowType = WelcomeWorkflow::class,
rendering = WelcomeScreen(
username = TextController("Ada"),
onLoginTapped = {}
onLogInPressed = {}
)
)
// Now, validate that there is a single item in the BackStackScreen, which is our welcome
Expand All @@ -48,15 +48,15 @@ class RootWorkflowTest {
}

@Test fun `login event`() {
RootWorkflow
RootNavigationWorkflow
// Start in the Welcome state
.testRender(initialState = Welcome, props = Unit)
// The WelcomeWorkflow is expected to be started in this render.
.expectWorkflow(
workflowType = WelcomeWorkflow::class,
rendering = WelcomeScreen(
username = TextController("Ada"),
onLoginTapped = {}
onLogInPressed = {}
),
// Simulate the WelcomeWorkflow sending an output of LoggedIn as if the "log in" button
// was tapped.
Expand All @@ -82,15 +82,15 @@ class RootWorkflowTest {
// region Integration

@Test fun `app flow`() {
RootWorkflow.launchForTestingFromStartWith {
RootNavigationWorkflow.launchForTestingFromStartWith {
// First rendering is just the welcome screen. Update the name.
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()
welcomeScreen.onLogInPressed()
}

// Expect the todo list to be rendered. Edit the first todo.
Expand All @@ -113,7 +113,7 @@ class RootWorkflowTest {

// Enter a title and save.
editScreen.title.textValue = "New Title"
editScreen.onSaveClick()
editScreen.onSavePressed()
}

// Expect the todo list. Validate the title was updated.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,20 +7,20 @@ import com.squareup.workflow1.ui.TextController
import com.squareup.workflow1.ui.WorkflowUiExperimentalApi
import workflow.tutorial.TodoEditWorkflow.Output.Save
import workflow.tutorial.TodoListWorkflow.Output.SelectTodo
import workflow.tutorial.TodoWorkflow.State
import workflow.tutorial.TodoWorkflow.State.Step.Edit
import workflow.tutorial.TodoWorkflow.State.Step.List
import workflow.tutorial.TodoWorkflow.TodoProps
import workflow.tutorial.TodoNavigationWorkflow.State
import workflow.tutorial.TodoNavigationWorkflow.State.Step.Edit
import workflow.tutorial.TodoNavigationWorkflow.State.Step.List
import workflow.tutorial.TodoNavigationWorkflow.TodoProps
import kotlin.test.Test
import kotlin.test.assertEquals

@OptIn(WorkflowUiExperimentalApi::class)
class TodoWorkflowTest {
class TodoNavigationWorkflowTest {

@Test fun `selecting todo`() {
val todos = listOf(TodoModel(title = "Title", note = "Note"))

TodoWorkflow
TodoNavigationWorkflow
.testRender(
props = TodoProps(name = "Ada"),
// Start from the list step to validate selecting a todo.
Expand All @@ -36,8 +36,8 @@ class TodoWorkflowTest {
username = "",
todoTitles = listOf("Title"),
onTodoSelected = {},
onBackClick = {},
onAddClick = {}
onBackPressed = {},
onAddPressed = {}
),
// Simulate selecting the first todo.
output = WorkflowOutput(SelectTodo(index = 0))
Expand All @@ -62,7 +62,7 @@ class TodoWorkflowTest {
@Test fun `saving todo`() {
val todos = listOf(TodoModel(title = "Title", note = "Note"))

TodoWorkflow
TodoNavigationWorkflow
.testRender(
props = TodoProps(name = "Ada"),
// Start from the edit step so we can simulate saving.
Expand All @@ -78,8 +78,8 @@ class TodoWorkflowTest {
username = "",
todoTitles = listOf("Title"),
onTodoSelected = {},
onBackClick = {},
onAddClick = {}
onBackPressed = {},
onAddPressed = {}
)
)
// Expect the TodoEditWorkflow to be rendered as well (as we're on the edit step).
Expand All @@ -88,8 +88,8 @@ class TodoWorkflowTest {
rendering = TodoEditScreen(
title = TextController("Title"),
note = TextController("Note"),
onBackClick = {},
onSaveClick = {}
onBackPressed = {},
onSavePressed = {}
),
// Simulate it emitting an output of `.save` to update the state.
output = WorkflowOutput(
Expand Down
Loading

0 comments on commit 79a66f9

Please sign in to comment.