Skip to content

Commit

Permalink
Merge pull request #632 from square/ray/embrace-the-viewmodel
Browse files Browse the repository at this point in the history
WorkflowLayout.start needs a Lifecycle, BackStackScreen is supported.
  • Loading branch information
rjrjr committed Jan 31, 2022
2 parents be479f2 + 01eb576 commit 61b94d1
Show file tree
Hide file tree
Showing 24 changed files with 171 additions and 107 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -36,10 +36,7 @@ class HelloBindingActivity : AppCompatActivity() {
val model: HelloBindingModel by viewModels()
setContentView(
WorkflowLayout(this).apply {
start(
renderings = model.renderings,
environment = viewEnvironment
)
start(lifecycle, model.renderings, viewEnvironment)
}
)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ class HelloComposeWorkflowActivity : AppCompatActivity() {
super.onCreate(savedInstanceState)
val model: HelloComposeModel by viewModels()
setContentView(
WorkflowLayout(this).apply { start(model.renderings) }
WorkflowLayout(this).apply { start(lifecycle, model.renderings) }
)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,7 @@ class InlineRenderingActivity : AppCompatActivity() {

val model: HelloBindingModel by viewModels()
setContentView(
WorkflowLayout(this).apply {
start(renderings = model.renderings)
}
WorkflowLayout(this).apply { start(lifecycle, model.renderings) }
)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,10 +38,7 @@ class NestedRenderingsActivity : AppCompatActivity() {
val model: NestedRenderingsModel by viewModels()
setContentView(
WorkflowLayout(this).apply {
start(
renderings = model.renderings,
environment = viewEnvironment
)
start(lifecycle, model.renderings, viewEnvironment)
}
)
}
Expand Down

This file was deleted.

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -8,5 +8,5 @@ import com.squareup.workflow1.ui.WorkflowUiExperimentalApi

@OptIn(WorkflowUiExperimentalApi::class)
val SampleContainers = ViewRegistry(
BackButtonViewFactory, OverviewDetailContainer, PanelContainer, ScrimContainer
OverviewDetailContainer, PanelContainer, ScrimContainer
)
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ class PoetryActivity : AppCompatActivity() {

val model: PoetryModel by viewModels()
setContentView(
WorkflowLayout(this).apply { start(model.renderings, viewRegistry) }
WorkflowLayout(this).apply { start(lifecycle, model.renderings, viewRegistry) }
)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ class RavenActivity : AppCompatActivity() {

val model: RavenModel by viewModels()
setContentView(
WorkflowLayout(this).apply { start(model.renderings, viewRegistry) }
WorkflowLayout(this).apply { start(lifecycle, model.renderings, viewRegistry) }
)

lifecycleScope.launch {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
package com.squareup.sample.hellobackbutton

import android.os.Parcelable
import com.squareup.sample.container.BackButtonScreen
import com.squareup.sample.hellobackbutton.AreYouSureWorkflow.Finished
import com.squareup.sample.hellobackbutton.AreYouSureWorkflow.State
import com.squareup.sample.hellobackbutton.AreYouSureWorkflow.State.Quitting
Expand All @@ -10,6 +9,7 @@ import com.squareup.workflow1.Snapshot
import com.squareup.workflow1.StatefulWorkflow
import com.squareup.workflow1.WorkflowAction.Companion.noAction
import com.squareup.workflow1.action
import com.squareup.workflow1.ui.BackButtonScreen
import com.squareup.workflow1.ui.WorkflowUiExperimentalApi
import com.squareup.workflow1.ui.modal.AlertContainerScreen
import com.squareup.workflow1.ui.modal.AlertScreen
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ class HelloBackButtonActivity : AppCompatActivity() {

val model: HelloBackButtonModel by viewModels()
setContentView(
WorkflowLayout(this).apply { start(model.renderings, viewRegistry) }
WorkflowLayout(this).apply { start(lifecycle, model.renderings, viewRegistry) }
)

lifecycleScope.launch {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ class DungeonActivity : AppCompatActivity() {
val model: TimeMachineModel by viewModels { component.timeMachineModelFactory }

setContentView(
WorkflowLayout(this).apply { start(model.renderings, component.viewRegistry) }
WorkflowLayout(this).apply { start(lifecycle, model.renderings, component.viewRegistry) }
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -23,10 +23,10 @@ class HelloWorkflowFragment : Fragment() {
// This ViewModel will survive configuration changes. It's instantiated
// by the first call to ViewModelProvider.get(), and that original instance is returned by
// succeeding calls, until this Fragment session ends.
val model: HelloViewModel = ViewModelProvider(this).get(HelloViewModel::class.java)
val model: HelloViewModel = ViewModelProvider(this)[HelloViewModel::class.java]

return WorkflowLayout(inflater.context).apply {
start(model.renderings)
start(lifecycle, model.renderings)
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ class HelloWorkflowActivity : AppCompatActivity() {
// succeeding calls.
val model: HelloViewModel by viewModels()
setContentView(
WorkflowLayout(this).apply { start(model.renderings) }
WorkflowLayout(this).apply { start(lifecycle, model.renderings) }
)
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ class StubVisibilityActivity : AppCompatActivity() {

val model: StubVisibilityModel by viewModels()
setContentView(
WorkflowLayout(this).apply { start(model.renderings) }
WorkflowLayout(this).apply { start(lifecycle, model.renderings) }
)
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,11 @@ import org.junit.rules.RuleChain
import org.junit.runner.RunWith
import java.util.concurrent.atomic.AtomicReference

/**
* This app is our most complex sample, which makes it a great candidate for
* integration testing — especially of modals, back stacks, back button handling,
* and view state management.
*/
@OptIn(WorkflowUiExperimentalApi::class)
@RunWith(AndroidJUnit4::class)
class TicTacToeEspressoTest {
Expand Down Expand Up @@ -155,7 +160,7 @@ class TicTacToeEspressoTest {
.check(matches(isDisplayed()))
}

@Test fun canGoBackInModalView() {
@Test fun canGoBackInModalViewAndSeeRestoredViewState() {
// Log in and hit the 2fa screen.
inAnyView(withId(R.id.login_email)).type("foo@2fa")
inAnyView(withId(R.id.login_password)).type("password")
Expand All @@ -168,7 +173,7 @@ class TicTacToeEspressoTest {
inAnyView(withId(R.id.login_email)).check(matches(withText("foo@2fa")))
}

@Test fun configChangePreservesBackStackViewStateCache() {
@Test fun canGoBackInModalViewAfterConfigChangeAndSeeRestoredViewState() {
// Log in and hit the 2fa screen.
inAnyView(withId(R.id.login_email)).type("foo@2fa")
inAnyView(withId(R.id.login_password)).type("password")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ class TicTacToeActivity : AppCompatActivity() {
idlingResource = component.idlingResource

setContentView(
WorkflowLayout(this).apply { start(model.renderings, viewRegistry) }
WorkflowLayout(this).apply { start(lifecycle, model.renderings, viewRegistry) }
)

lifecycleScope.launch {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ class ToDoActivity : AppCompatActivity() {

setContentView(
WorkflowLayout(this).apply {
start(model.ensureWorkflow(traceFilesDir = filesDir), viewRegistry)
start(lifecycle, model.ensureWorkflow(traceFilesDir = filesDir), viewRegistry)
}
)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,11 @@ import android.view.ViewGroup
import android.view.ViewGroup.LayoutParams.MATCH_PARENT
import android.view.ViewGroup.LayoutParams.WRAP_CONTENT
import androidx.annotation.IdRes
import com.squareup.workflow1.ui.BackButtonScreen
import com.squareup.workflow1.ui.BuilderViewFactory
import com.squareup.workflow1.ui.ViewEnvironment
import com.squareup.workflow1.ui.ViewRegistry
import com.squareup.workflow1.ui.WorkflowUiExperimentalApi
import com.squareup.workflow1.ui.backPressedHandler
import com.squareup.workflow1.ui.bindShowRendering
import com.squareup.workflow1.ui.buildView
import com.squareup.workflow1.ui.modal.ModalViewContainer.Companion.binding
Expand Down Expand Up @@ -62,22 +62,23 @@ public open class ModalViewContainer @JvmOverloads constructor(
initialModalRendering: Any,
initialViewEnvironment: ViewEnvironment
): DialogRef<Any> {
// Put a no-op backPressedHandler behind the given rendering, to
// ensure that the `onBackPressed` call below will not leak up to handlers
// that should be blocked by this modal session.
val wrappedRendering = BackButtonScreen(initialModalRendering) { }

val view = initialViewEnvironment[ViewRegistry]
// Notice that we don't pass a custom initializeView function to set the
// WorkflowLifecycleOwner here. ModalContainer will do that itself, on the parent of the view
// created here.
.buildView(
initialRendering = initialModalRendering,
initialRendering = wrappedRendering,
initialViewEnvironment = initialViewEnvironment,
contextForNewView = this.context,
container = this
)
.apply {
start()
// If the modal's root view has no backPressedHandler, add a no-op one to
// ensure that the `onBackPressed` call below will not leak up to handlers
// that should be blocked by this modal session.
if (backPressedHandler == null) backPressedHandler = { }
}

return buildDialogForView(view)
Expand Down Expand Up @@ -109,7 +110,14 @@ public open class ModalViewContainer @JvmOverloads constructor(
}

override fun updateDialog(dialogRef: DialogRef<Any>) {
with(dialogRef) { (extra as View).showRendering(modalRendering, viewEnvironment) }
with(dialogRef) {
// Have to preserve the wrapping done in buildDialog. (We can't put the
// BackButtonScreen in the DialogRef because the superclass needs to be
// able to do compatibility checks against it when deciding whether
// or not to update the existing dialog.)
val wrappedRendering = BackButtonScreen(modalRendering) { }
(extra as View).showRendering(wrappedRendering, viewEnvironment)
}
}

@PublishedApi
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import org.junit.Test
import kotlin.test.assertFailsWith

@OptIn(WorkflowUiExperimentalApi::class)
class BackStackScreenTest {
internal class BackStackScreenTest {
@Test fun `top is last`() {
assertThat(BackStackScreen(1, 2, 3, 4).top).isEqualTo(4)
}
Expand Down
12 changes: 12 additions & 0 deletions workflow-ui/core-android/api/core-android.api
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,15 @@ public abstract interface class com/squareup/workflow1/ui/AndroidViewRendering {
public abstract fun getViewFactory ()Lcom/squareup/workflow1/ui/ViewFactory;
}

public final class com/squareup/workflow1/ui/BackButtonScreen : com/squareup/workflow1/ui/AndroidViewRendering {
public fun <init> (Ljava/lang/Object;ZLkotlin/jvm/functions/Function0;)V
public synthetic fun <init> (Ljava/lang/Object;ZLkotlin/jvm/functions/Function0;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
public final fun getOnBackPressed ()Lkotlin/jvm/functions/Function0;
public final fun getShadow ()Z
public fun getViewFactory ()Lcom/squareup/workflow1/ui/ViewFactory;
public final fun getWrapped ()Ljava/lang/Object;
}

public final class com/squareup/workflow1/ui/BackPressHandlerKt {
public static final fun getBackPressedHandler (Landroid/view/View;)Lkotlin/jvm/functions/Function0;
public static final fun onBackPressedDispatcherOwnerOrNull (Landroid/content/Context;)Landroidx/activity/OnBackPressedDispatcherOwner;
Expand Down Expand Up @@ -203,8 +212,11 @@ public abstract interface class com/squareup/workflow1/ui/ViewStarter {
public final class com/squareup/workflow1/ui/WorkflowLayout : android/widget/FrameLayout {
public fun <init> (Landroid/content/Context;Landroid/util/AttributeSet;)V
public synthetic fun <init> (Landroid/content/Context;Landroid/util/AttributeSet;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
public final fun start (Landroidx/lifecycle/Lifecycle;Lkotlinx/coroutines/flow/Flow;Lcom/squareup/workflow1/ui/ViewEnvironment;)V
public final fun start (Landroidx/lifecycle/Lifecycle;Lkotlinx/coroutines/flow/Flow;Lcom/squareup/workflow1/ui/ViewRegistry;)V
public final fun start (Lkotlinx/coroutines/flow/Flow;Lcom/squareup/workflow1/ui/ViewEnvironment;)V
public final fun start (Lkotlinx/coroutines/flow/Flow;Lcom/squareup/workflow1/ui/ViewRegistry;)V
public static synthetic fun start$default (Lcom/squareup/workflow1/ui/WorkflowLayout;Landroidx/lifecycle/Lifecycle;Lkotlinx/coroutines/flow/Flow;Lcom/squareup/workflow1/ui/ViewEnvironment;ILjava/lang/Object;)V
public static synthetic fun start$default (Lcom/squareup/workflow1/ui/WorkflowLayout;Lkotlinx/coroutines/flow/Flow;Lcom/squareup/workflow1/ui/ViewEnvironment;ILjava/lang/Object;)V
public final fun update (Ljava/lang/Object;Lcom/squareup/workflow1/ui/ViewEnvironment;)V
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
package com.squareup.workflow1.ui

/**
* Adds optional back button handling to a [wrapped] rendering, possibly overriding that
* the wrapped rendering's own back button handler.
*
* @param shadow If `true`, [onBackPressed] is set as the
* [backPressedHandler][android.view.View.backPressedHandler] after
* the [wrapped] rendering's view is built / updated, effectively overriding it.
* If false (the default), [onBackPressed] is set afterward, to allow the wrapped rendering to
* take precedence if it sets a `backPressedHandler` of its own -- the handler provided
* here serves as a default.
*
* @param onBackPressed The function to fire when the device back button
* is pressed, or null to set no handler -- or clear a handler that was set previously.
* Defaults to `null`.
*/
@WorkflowUiExperimentalApi
public class BackButtonScreen<W : Any>(
public val wrapped: W,
public val shadow: Boolean = false,
public val onBackPressed: (() -> Unit)? = null
) : AndroidViewRendering<BackButtonScreen<*>> {
override val viewFactory: ViewFactory<BackButtonScreen<*>> = DecorativeViewFactory(
type = BackButtonScreen::class,
map = { outer -> outer.wrapped },
doShowRendering = { view, innerShowRendering, outerRendering, viewEnvironment ->
if (!outerRendering.shadow) {
// Place our handler before invoking innerShowRendering, so that
// its later calls to view.backPressedHandler will take precedence
// over ours.
view.backPressedHandler = outerRendering.onBackPressed
}

innerShowRendering.invoke(outerRendering.wrapped, viewEnvironment)

if (outerRendering.shadow) {
// Place our handler after invoking innerShowRendering, so that ours wins.
view.backPressedHandler = outerRendering.onBackPressed
}
}
)
}
Loading

0 comments on commit 61b94d1

Please sign in to comment.