Giter Site home page Giter Site logo

square / workflow-kotlin Goto Github PK

View Code? Open in Web Editor NEW
980.0 23.0 99.0 16.91 MB

A Swift and Kotlin library for making composable state machines, and UIs driven by those state machines.

Home Page: https://square.github.io/workflow

License: Apache License 2.0

Shell 0.18% Ruby 0.13% Kotlin 99.64% Java 0.06%
reactive ui state-machine kotlin android workflow

workflow-kotlin's Introduction

workflow

Kotlin CI Maven Central GitHub license Kotlinlang slack

Workflow is an application framework that provides architectural primitives.

Workflow is:

  • Written in and used for Kotlin and Swift
  • A unidirectional data flow library that uses immutable data within each Workflow. Data flows in a single direction from source to UI, and events in a single direction from the UI to the business logic.
  • A library that supports writing business logic and complex UI navigation logic as state machines, thereby enabling confident reasoning about state and validation of correctness.
  • Optimized for composability and scalability of features and screens.
  • Corresponding UI frameworks that bind Rendering data classes for “views” (including event callbacks) to Mobile UI frameworks for Android and iOS.
  • A corresponding testing framework that facilitates simple-to-write unit tests for all application business logic and helps ensure correctness.

1.0.0-rc is ready and the core is stable. There are still experimental / under construction areas of the API for UI integration however. These classes and functions are marked with @WorkflowUIExperimentalApi. They are suitable for production use (we've been shipping them for months at the very heart of our flagship app), but may require signature tweaks as we iterate a bit more on Dialog management, and configuring transition effects. If they do change, we will take care to minimize the impact via deprecation, etc.

Using Workflows in your project

Maven Artifacts

Artifacts are hosted on Maven Central. If you're using Gradle, ensure mavenCentral() appears in your repositories block, and then add dependencies on the following artifacts:

Maven Coordinates Depend on this if…
com.squareup.workflow1:workflow-core-jvm:x.y.z You are writing a library module/project that uses Workflows, but you don't need to interact with the runtime from the outside.
com.squareup.workflow1:workflow-rx2:x.y.z You need to interact with RxJava2 from your Workflows.
com.squareup.workflow1:workflow-testing-jvm:x.y.z You are writing tests. This should only be included as a test dependency.
com.squareup.workflow1:workflow-ui-core-android:x.y.z You're writing an Android app that uses Workflows.

Lower-level Artifacts

Most code shouldn't need to depend on these directly. They should generally only be used to build higher-level integrations with UI frameworks.

Maven Coordinates Depend on this if…
com.squareup.workflow1:workflow-runtime-jvm:x.y.z You need to interact directly with the runtime, i.e. streams of renderings and outputs.
com.squareup.workflow1:workflow-ui-core-common-jvm:x.y.z You are writing workflow-ui-android for another UI framework. Defines the core types used by that artifact.

Jetpack Compose support

Jetpack Compose is the new UI toolkit for Android. It is comparable to SwiftUI for iOS. The main UI artifacts in this repository support standard Android Views, but various types of Compose integrations are provided under the compose folder.

You'll find workflow + compose info and documentation there.

Resources

Support & Contact

Workflow discussion happens in the Workflow Community slack. Use this open invitation.

Workflow maintainers also hang out in the #squarelibraries channel on the Kotlin Slack.

Releasing and Deploying

See RELEASING.md.

License

Copyright 2019 Square Inc.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

workflow-kotlin's People

Contributors

0legg avatar afollestad avatar aquageek avatar armaxis avatar asthatrivedi avatar bencochran avatar bnvinay92 avatar charbgr avatar davidapgar avatar dependabot-preview[bot] avatar dhavalshreyas avatar hujim avatar justindsn avatar merge-when-green[bot] avatar milanjovic92 avatar mongoose700 avatar pyricau avatar rbusarow avatar renovate[bot] avatar rjrjr avatar saied89 avatar salvatoret avatar seanfreiburg avatar sssurvey avatar steve-the-edwards avatar timdonnelly avatar tir38 avatar vrallev avatar wugeorgeq avatar zach-klippenstein avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

workflow-kotlin's Issues

Workflows rewrite kdoc isn't helpful enough when starting from scratch

Internal feedback has been that the existing kdocs on the new workflows is not comprehensive enough to figure out how to use it.

Some of said feedback:

Documentation needs to be written for library users (app authors), but at every iteration of the API the documentation is instead some kind of platonic description of internal architecture with no indication of how to use it.

it’s frustrating that every time I sit down to pick up a new version of the API I face the same problem of literally not being able to make sense of it without a guided tour

so, here’s one quick thing that you should be able to easily point out and document: in 0.8.0, how do I get an instance of a workflow?
like, my first one. a “root”.
I can tell you I’ve looked at Workflow, WorkflowContext, WorkflowAction. so if it’s one of those, please plant a big flag on it and point to it from the others, b/c I’ve missed it somehow

Figure out how to keep gradle dependencies fresh after 1.0 release.

We used to use Dependabot to ensure we kept our dependencies on their latest versions, but it does not support our Dependencies.kt file. There are a number of options here, which need to be evaluated and actually tested to see what works. One is the refreshVersions gradle plugin, which Zach tried and ran into issues with. There are a bunch of other similar plugins linked here. This is a Gradle Action for detecting out-of-date versions, but doesn't automatically update anything.

cc @rjrjr

Kotlin IntelliJ plugin crashes on StatefulWorkflow and action builder

There is a bug in the plugin that crashes the parser when it encounters action. @kirillzh filed JetBrains an issue about it.

I've created a minimal Android Studio project that demonstrates this bug: https://github.com/zach-klippenstein/repro-KT-34524

Open the project in Android Studio and open the file app/src/main/java/com/example/kt_34524repro/SampleWorkflow.kt to see the bug in action.

There are a few other issues that look to be related or duplicates:

Dialogs isn't dismissed when workflow stops rendering it without interaction on the dialog itself

If a workflow renders a dialog with a listener on the buttons that result in the dialog no longer being rendered, everything is fine.

However, if the workflow suddenly stops rendering the dialog without the user having clicked on the dialog buttons, the dialog is not dismissed.

Repro steps:

  1. Render a dialog with AlertContainerScreen.
  2. After a timeout, stop rendering it. Dialog will still be around.

Repro project: https://github.com/square/droidcon-nyc-2019-workflow-game (win the game, then enter time travel mode and scrub past the "game over" dialog)

Extension library proposal: Stepped workflows

Abstract

This is a proposal for a mini-library built on top of Workflows for creating "stepped" flows – a DAG of screens that work together to accomplish some task, driven by a single parent workflow. E.g. a realistic login flow that has multiple authentication methods, password reset, 2-factor, etc.

This proposal is expressed in Kotlin, but a similar design could be done for Swift.

Design

Steps

Each "step" is a workflow. Each step does its own bookkeeping to know when it's "complete" and the next screen should be shown. The step workflows rendering type includes a number of things:

  • The UI model for that screen (e.g. BackStackScreen).
  • If the step is "complete",
    • some arbitrary value to pass up to the parent workflow.
    • an event handler (ala onEvent) to invoke when the user navigates back to the workflow. Should reset it's "completion" state to not-complete.

Step rendering values are of the following type:

sealed class StepRendering<out UiT, out ResultT> {
  abstract val uiModel: UiT

  data class InProgress<UiT>(override val uiModel: UiT): StepRendering<UiT, Nothing>()
  data class Complete<UiT, ResultT>(
    override val uiModel: UiT,
    val result: ResultT,
    val onBack: () -> Unit
  ): StepRendering<UiT, ResultT>()
}

A step workflow is of the following type:

interface WorkflowStep<I, O : Any, UiT, ResultT> : Workflow<I, O, StepRendering<UiT, ResultT>>

Step workflows must conform their rendering (or output, see below) type to the special type, but other than that are free to do anything a workflow can do, including receive input, emit output, handle events, run workers, and compose other workflows (even other stepped sub-flows).

Output-based Steps

WorkflowStep requires steps to track their own completion state. Many steps may not need much state tracking however, and are simply "complete" once a "Next" button has been clicked. For those steps, we can provide a helper to effectively move the indication of "complete" from the rendering to the output:

sealed class StepOutput<out O : Any, out ResultT> {
  data class Output<O : Any>(val output: O): StepOutput<O, Nothing>()
  data class Complete<O : Any, ResultT>(
    val result: ResultT,
    val output: O? = null
  ): StepOutput<O, ResultT>()
}

interface SimpleWorkflowStep<I, O : Any, UiT, ResultT> : WorkflowStep<I, StepOutput<O, ResultT>, UiT>

A workflow of type SimpleWorkflowStep can be converted to a WorkflowStep by wrapping it in a workflow that simply keeps track of whether a Complete output has been emitted or the onBack event has been invoked, and returns the appropriate rendering case.

Uniqueness

Note that all wrappers would have the same type, so would appear to be the same "session" to the RenderContext. They would need to be distinguished with keys, which could be derived from the wrapped workflows' class names. However this is kind of hacky, and we could add explicit support for "wrapped" workflows to get around this (e.g. a WrapperWorkflow interface that exposes a val wrapped: Workflow<*, *, *> which is included in the comparison).

Parent Workflow

Given a set of StepWorkflows, it should be apparent how to manually write a workflow that can drive them:

  1. Determine the first step, and render it (forwarding its output).
  2. If the rendering is InProgress, return its UiT – done.
  3. If the rendering is Complete, use the ResultT to determine the next step, and return to 1. The previous step's UiT may either be ignored or composed with the subsequent step's UI, (e.g. building up a BackStackScreen).

Since the parent workflow is just a Workflow, it can be composed with other workflows, or even implement StepWorkflow itself.

Declarative DAG

However, writing all this wiring code manually every time would get verbose, and bury the actual business logic in boilerplate. We can do better. The code we'd like to write would just render a step, get its result, and then continue rendering the subsequent steps, without worrying about Taking inspiration from Haskell, if we think of StepRendering as IO, we can build our own version of do.

Given the RenderContext from the parent, we can define our own wrapper around it that defines its own renderStep method that implicitly builds up the BackStackScreen from the UiTs from each step, and only returns ResultTs to the parent. renderStep could also take an optional function to determine how to fold the new UiT into the previous one (e.g. copying in a body-layer screen under a dialog). The business logic for the parent is written as if all steps were complete, and is about as close to a pure graph definition as we can get. Under the hood, renderStep can stop the render after the first InProgress step by throwing and catching a special exception instead of returning (which it doesn't have enough information to do anyway, since InProgress doesn't contain a ResultT).

Since it's provided as a simple helper function, this glue helper can be invoked as expression body for the parent's render method in simple cases, or the parent can do other rendering work in addition to rendering its steps.

Add lint check for creating views too frequently in LayoutRunner

When writing a LayoutRunner, it's easy to make the mistake of inflating or creating views in showRendering on every update. In general, views should only be inflated/created in the initializer, and updated in showRendering. We can probably write a simple lint check that catches most of these cases.

Pass props to WorkflowActions

It's come up a couple of times, at least in Kotlin, that WorkflowActions get the state of the current workflow but not its props. Since we consider props to be part of the workflow's overall "state", it seems unintuitive that we only pass part of that to actions.

The reason for not doing this was, initially, to discourage side effects from being performed in actions. However, we realized that is too restrictive (see this Slack discussion).

Note for Swift this means passing the Workflow value itself into the action, since there's no separate props type.

Kotlin version of square/workflow-swift#139.

[kotlin] WorkflowViewStub breaks view back handling

If a LayoutRunner that is being used to render a view inside a WorkflowViewStub calls backPressedHandler = on the root view (the view passed to the LayoutRunner's constructor), that view is actually the parent view of the stub, so the back handler will be set on the stub's container. If another view doesn't replace the back handler in the stub's container, that handler will live beyond the lifetime of that screen.

To repro, see square/workflow#873.

Resolve render(workflow) inconsistencies

// This is the fundamental API. 
// rendered method here requires output type to be Action<...>
let result = MyWorkflow()
    .mapOutput {  }
    .rendered(with: context, key: “key”)

// Convenience for the above,  and probably /maybe the 90% use case
let result = MyWorkflow()
    .rendered(
        with: context,
        key: “key”,
        onOutput: {  }
    )

Unclear, though, what impact the mapOutput call has / should have on workflow equivalence.
While sorting this out, keep in mind the intimately related conversation in square/workflow#961. It would be pretty odd to land in different places.

Also slightly related to square/workflow#544?

This change should also be applied to Workers. See #611.

Detect infinite render loops in unit tests

If you have a worker that is incorrectly implemented, in that it always returns false from doesSameWorkAs and its Flow completes immediately, your workflow will enter an infinite render loop.

This might seem like a weird thing to do, but if your worker is a Mockito mock and you forget to override doesSameWorkAs, it's exactly what will happen. We can detect this situation in the WorkflowTester API by counting render passes, and if you hit some extraordinarily high number of passes before even running the test block, fail the test with some message about this case.

cc @afollestad

Kotlin samples should experiment with DI via PropsT

@zach-klippenstein @bencochran and I just independently had the same thought: what if the DI-esque Kotlin samples were changed s.t. all workflows were object, and all injection happened via Props, using the factory pattern where necessary.

Could be perfectly pleasant, and much easier guidance to give ("Workflows are always stateless objects"). Also very consistent with Swift.

Ensure unit test coverage for awaitFailure and exception thrown from render method

Consider this simple stateful workflow that throws TODO() on render.

class FooWorkflow : StatefulWorkflow<Unit, State, Unit, Unit>() {

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

  override fun render(
    props: Unit,
    state: State,
    context: RenderContext<State, Unit>
  ) {
    TODO("implement me")
  }

  override fun snapshotState(state: State): Snapshot = Snapshot.EMPTY

  object State
}

This is what my unit test looks like to test that the workflow indeed throws (realistically I wanted to throw in a very certain, not yet implemented case and I added a unit test for that ala TDD):

class FooWorkflowTest {
  private val fooWorkflow = FooWorkflow()

  @Test
  fun verifyThrowsException() {
    fooWorkflow.testFromStart {
      assertThat(awaitFailure()).isInstanceOf<NotImplementedError>()
    }
  }
}

While test passes, it still throws right after that:

Exception in thread "main @coroutine#3" kotlin.NotImplementedError: An operation is not implemented: implement
	at com.squareup.scales.FooWorkflow.render(FooWorkflow.kt:20)
	at com.squareup.scales.FooWorkflow.render(FooWorkflow.kt:8)
	at com.squareup.workflow.internal.WorkflowNode.renderWithStateType(WorkflowNode.kt:178)
	at com.squareup.workflow.internal.WorkflowNode.render(WorkflowNode.kt:95)
	at com.squareup.workflow.internal.RealWorkflowLoop$runWorkflowLoop$2.invokeSuspend(WorkflowLoop.kt:79)
	(Coroutine boundary)
	at com.squareup.workflow.LaunchWorkflowKt$launchWorkflowImpl$workflowJob$1.invokeSuspend(LaunchWorkflow.kt:174)
Caused by: kotlin.NotImplementedError: An operation is not implemented: implement
	at com.squareup.scales.FooWorkflow.render(FooWorkflow.kt:20)
	at com.squareup.scales.FooWorkflow.render(FooWorkflow.kt:8)
	at com.squareup.workflow.internal.WorkflowNode.renderWithStateType(WorkflowNode.kt:178)
	at com.squareup.workflow.internal.WorkflowNode.render(WorkflowNode.kt:95)
	at com.squareup.workflow.internal.RealWorkflowLoop$runWorkflowLoop$2.invokeSuspend(WorkflowLoop.kt:79)
	at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:33)
	at kotlinx.coroutines.DispatchedTask.run(Dispatched.kt:241)
	at kotlinx.coroutines.EventLoop.processUnconfinedEvent(EventLoop.common.kt:68)
	at kotlinx.coroutines.DispatchedKt.resumeCancellable(Dispatched.kt:464)
	at kotlinx.coroutines.intrinsics.CancellableKt.startCoroutineCancellable(Cancellable.kt:26)
	at kotlinx.coroutines.CoroutineStart.invoke(CoroutineStart.kt:109)
	at kotlinx.coroutines.AbstractCoroutine.start(AbstractCoroutine.kt:154)
	at kotlinx.coroutines.BuildersKt__Builders_commonKt.launch(Builders.common.kt:54)
	at kotlinx.coroutines.BuildersKt.launch(Unknown Source)
	at kotlinx.coroutines.BuildersKt__Builders_commonKt.launch$default(Builders.common.kt:47)
	at kotlinx.coroutines.BuildersKt.launch$default(Unknown Source)
	at com.squareup.workflow.LaunchWorkflowKt.launchWorkflowImpl(LaunchWorkflow.kt:166)
	at com.squareup.workflow.LaunchWorkflowKt.launchWorkflowIn(LaunchWorkflow.kt:109)
	at com.squareup.workflow.testing.WorkflowTesterKt.testFromStart(WorkflowTester.kt:230)
	at com.squareup.workflow.testing.WorkflowTesterKt.testFromStart(WorkflowTester.kt:253)
	at com.squareup.workflow.testing.WorkflowTesterKt.testFromStart$default(WorkflowTester.kt:251)
	at com.squareup.scales.FooWorkflowTest.verifyThrowsException(FooWorkflowTest.kt:13)
	at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
	at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
	at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
	at java.lang.reflect.Method.invoke(Method.java:498)
	at org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:59)
	at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:12)
	at org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:56)
	at org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.java:17)
	at org.junit.runners.ParentRunner$3.evaluate(ParentRunner.java:305)
	at org.junit.runners.BlockJUnit4ClassRunner$1.evaluate(BlockJUnit4ClassRunner.java:100)
	at org.junit.runners.ParentRunner.runLeaf(ParentRunner.java:365)
	at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:103)
	at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:63)
	at org.junit.runners.ParentRunner$4.run(ParentRunner.java:330)
	at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:78)
	at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:328)
	at org.junit.runners.ParentRunner.access$100(ParentRunner.java:65)
	at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:292)
	at org.junit.runners.ParentRunner$3.evaluate(ParentRunner.java:305)
	at org.junit.runners.ParentRunner.run(ParentRunner.java:412)
	at org.junit.runner.JUnitCore.run(JUnitCore.java:137)
	at com.intellij.junit4.JUnit4IdeaTestRunner.startRunnerWithArgs(JUnit4IdeaTestRunner.java:68)
	at com.intellij.rt.execution.junit.IdeaTestRunner$Repeater.startRunnerWithArgs(IdeaTestRunner.java:47)
	at com.intellij.rt.execution.junit.JUnitStarter.prepareStreamsAndStart(JUnitStarter.java:242)
	at com.intellij.rt.execution.junit.JUnitStarter.main(JUnitStarter.java:70)
	at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
	at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
	at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
	at java.lang.reflect.Method.invoke(Method.java:498)
	at com.intellij.rt.execution.CommandLineWrapper.main(CommandLineWrapper.java:66)
Exception in thread "main @coroutine#1" kotlin.NotImplementedError: An operation is not implemented: implement
	at com.squareup.scales.FooWorkflow.render(FooWorkflow.kt:20)
	at com.squareup.scales.FooWorkflow.render(FooWorkflow.kt:8)
	at com.squareup.workflow.internal.WorkflowNode.renderWithStateType(WorkflowNode.kt:178)
	at com.squareup.workflow.internal.WorkflowNode.render(WorkflowNode.kt:95)
	at com.squareup.workflow.internal.RealWorkflowLoop$runWorkflowLoop$2.invokeSuspend(WorkflowLoop.kt:79)
	(Coroutine boundary)
	at com.squareup.workflow.LaunchWorkflowKt$launchWorkflowImpl$workflowJob$1.invokeSuspend(LaunchWorkflow.kt:174)
Caused by: kotlin.NotImplementedError: An operation is not implemented: implement
	at com.squareup.scales.FooWorkflow.render(FooWorkflow.kt:20)
	at com.squareup.scales.FooWorkflow.render(FooWorkflow.kt:8)
	at com.squareup.workflow.internal.WorkflowNode.renderWithStateType(WorkflowNode.kt:178)
	at com.squareup.workflow.internal.WorkflowNode.render(WorkflowNode.kt:95)
	at com.squareup.workflow.internal.RealWorkflowLoop$runWorkflowLoop$2.invokeSuspend(WorkflowLoop.kt:79)
	at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:33)
	at kotlinx.coroutines.DispatchedTask.run(Dispatched.kt:241)
	at kotlinx.coroutines.EventLoop.processUnconfinedEvent(EventLoop.common.kt:68)
	at kotlinx.coroutines.DispatchedKt.resumeCancellable(Dispatched.kt:464)
	at kotlinx.coroutines.intrinsics.CancellableKt.startCoroutineCancellable(Cancellable.kt:26)
	at kotlinx.coroutines.CoroutineStart.invoke(CoroutineStart.kt:109)
	at kotlinx.coroutines.AbstractCoroutine.start(AbstractCoroutine.kt:154)
	at kotlinx.coroutines.BuildersKt__Builders_commonKt.launch(Builders.common.kt:54)
	at kotlinx.coroutines.BuildersKt.launch(Unknown Source)
	at kotlinx.coroutines.BuildersKt__Builders_commonKt.launch$default(Builders.common.kt:47)
	at kotlinx.coroutines.BuildersKt.launch$default(Unknown Source)
	at com.squareup.workflow.LaunchWorkflowKt.launchWorkflowImpl(LaunchWorkflow.kt:166)
	at com.squareup.workflow.LaunchWorkflowKt.launchWorkflowIn(LaunchWorkflow.kt:109)
	at com.squareup.workflow.testing.WorkflowTesterKt.testFromStart(WorkflowTester.kt:230)
	at com.squareup.workflow.testing.WorkflowTesterKt.testFromStart(WorkflowTester.kt:253)
	at com.squareup.workflow.testing.WorkflowTesterKt.testFromStart$default(WorkflowTester.kt:251)
	at com.squareup.scales.FooWorkflowTest.verifyThrowsException(FooWorkflowTest.kt:13)
	at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
	at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
	at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
	at java.lang.reflect.Method.invoke(Method.java:498)
	at org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:59)
	at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:12)
	at org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:56)
	at org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.java:17)
	at org.junit.runners.ParentRunner$3.evaluate(ParentRunner.java:305)
	at org.junit.runners.BlockJUnit4ClassRunner$1.evaluate(BlockJUnit4ClassRunner.java:100)
	at org.junit.runners.ParentRunner.runLeaf(ParentRunner.java:365)
	at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:103)
	at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:63)
	at org.junit.runners.ParentRunner$4.run(ParentRunner.java:330)
	at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:78)
	at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:328)
	at org.junit.runners.ParentRunner.access$100(ParentRunner.java:65)
	at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:292)
	at org.junit.runners.ParentRunner$3.evaluate(ParentRunner.java:305)
	at org.junit.runners.ParentRunner.run(ParentRunner.java:412)
	at org.junit.runner.JUnitCore.run(JUnitCore.java:137)
	at com.intellij.junit4.JUnit4IdeaTestRunner.startRunnerWithArgs(JUnit4IdeaTestRunner.java:68)
	at com.intellij.rt.execution.junit.IdeaTestRunner$Repeater.startRunnerWithArgs(IdeaTestRunner.java:47)
	at com.intellij.rt.execution.junit.JUnitStarter.prepareStreamsAndStart(JUnitStarter.java:242)
	at com.intellij.rt.execution.junit.JUnitStarter.main(JUnitStarter.java:70)
	at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
	at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
	at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
	at java.lang.reflect.Method.invoke(Method.java:498)
	at com.intellij.rt.execution.CommandLineWrapper.main(CommandLineWrapper.java:66)
Exception in thread "main @coroutine#2" kotlin.NotImplementedError: An operation is not implemented: implement
	at com.squareup.scales.FooWorkflow.render(FooWorkflow.kt:20)
	at com.squareup.scales.FooWorkflow.render(FooWorkflow.kt:8)
	at com.squareup.workflow.internal.WorkflowNode.renderWithStateType(WorkflowNode.kt:178)
	at com.squareup.workflow.internal.WorkflowNode.render(WorkflowNode.kt:95)
	at com.squareup.workflow.internal.RealWorkflowLoop$runWorkflowLoop$2.invokeSuspend(WorkflowLoop.kt:79)
	(Coroutine boundary)
	at com.squareup.workflow.LaunchWorkflowKt$launchWorkflowImpl$workflowJob$1.invokeSuspend(LaunchWorkflow.kt:174)
Caused by: kotlin.NotImplementedError: An operation is not implemented: implement
	at com.squareup.scales.FooWorkflow.render(FooWorkflow.kt:20)
	at com.squareup.scales.FooWorkflow.render(FooWorkflow.kt:8)
	at com.squareup.workflow.internal.WorkflowNode.renderWithStateType(WorkflowNode.kt:178)
	at com.squareup.workflow.internal.WorkflowNode.render(WorkflowNode.kt:95)
	at com.squareup.workflow.internal.RealWorkflowLoop$runWorkflowLoop$2.invokeSuspend(WorkflowLoop.kt:79)
	at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:33)
	at kotlinx.coroutines.DispatchedTask.run(Dispatched.kt:241)
	at kotlinx.coroutines.EventLoop.processUnconfinedEvent(EventLoop.common.kt:68)
	at kotlinx.coroutines.DispatchedKt.resumeCancellable(Dispatched.kt:464)
	at kotlinx.coroutines.intrinsics.CancellableKt.startCoroutineCancellable(Cancellable.kt:26)
	at kotlinx.coroutines.CoroutineStart.invoke(CoroutineStart.kt:109)
	at kotlinx.coroutines.AbstractCoroutine.start(AbstractCoroutine.kt:154)
	at kotlinx.coroutines.BuildersKt__Builders_commonKt.launch(Builders.common.kt:54)
	at kotlinx.coroutines.BuildersKt.launch(Unknown Source)
	at kotlinx.coroutines.BuildersKt__Builders_commonKt.launch$default(Builders.common.kt:47)
	at kotlinx.coroutines.BuildersKt.launch$default(Unknown Source)
	at com.squareup.workflow.LaunchWorkflowKt.launchWorkflowImpl(LaunchWorkflow.kt:166)
	at com.squareup.workflow.LaunchWorkflowKt.launchWorkflowIn(LaunchWorkflow.kt:109)
	at com.squareup.workflow.testing.WorkflowTesterKt.testFromStart(WorkflowTester.kt:230)
	at com.squareup.workflow.testing.WorkflowTesterKt.testFromStart(WorkflowTester.kt:253)
	at com.squareup.workflow.testing.WorkflowTesterKt.testFromStart$default(WorkflowTester.kt:251)
	at com.squareup.scales.FooWorkflowTest.verifyThrowsException(FooWorkflowTest.kt:13)
	at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
	at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
	at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
	at java.lang.reflect.Method.invoke(Method.java:498)
	at org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:59)
	at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:12)
	at org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:56)
	at org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.java:17)
	at org.junit.runners.ParentRunner$3.evaluate(ParentRunner.java:305)
	at org.junit.runners.BlockJUnit4ClassRunner$1.evaluate(BlockJUnit4ClassRunner.java:100)
	at org.junit.runners.ParentRunner.runLeaf(ParentRunner.java:365)
	at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:103)
	at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:63)
	at org.junit.runners.ParentRunner$4.run(ParentRunner.java:330)
	at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:78)
	at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:328)
	at org.junit.runners.ParentRunner.access$100(ParentRunner.java:65)
	at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:292)
	at org.junit.runners.ParentRunner$3.evaluate(ParentRunner.java:305)
	at org.junit.runners.ParentRunner.run(ParentRunner.java:412)
	at org.junit.runner.JUnitCore.run(JUnitCore.java:137)
	at com.intellij.junit4.JUnit4IdeaTestRunner.startRunnerWithArgs(JUnit4IdeaTestRunner.java:68)
	at com.intellij.rt.execution.junit.IdeaTestRunner$Repeater.startRunnerWithArgs(IdeaTestRunner.java:47)
	at com.intellij.rt.execution.junit.JUnitStarter.prepareStreamsAndStart(JUnitStarter.java:242)
	at com.intellij.rt.execution.junit.JUnitStarter.main(JUnitStarter.java:70)
	at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
	at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
	at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
	at java.lang.reflect.Method.invoke(Method.java:498)
	at com.intellij.rt.execution.CommandLineWrapper.main(CommandLineWrapper.java:66)

Demonstrate nested modal views

AC:

  • Sample shows off modal flow over modal flow visual effect
  • Views of inner-most flow should re-use same dialog / frame as first modal flow, just use visual effects to imply stacking.

Depends on #784

Kotlin CLI sample crashes when space is typed

In Android Studio, right-click on Main.kt and choose Run.
Press the spacebar
Kaboom:

Exception in thread "main" java.lang.StringIndexOutOfBoundsException: String index out of range: 13
	at java.lang.String.substring(String.java:1963)
	at com.squareup.sample.hellotodo.EditTextWorkflowKt.insertCharAt(EditTextWorkflow.kt:99)
	at com.squareup.sample.hellotodo.EditTextWorkflowKt.access$insertCharAt(EditTextWorkflow.kt:1)
	at com.squareup.sample.hellotodo.EditTextWorkflow$onKeystroke$1.invoke(EditTextWorkflow.kt:71)
	at com.squareup.sample.hellotodo.EditTextWorkflow$onKeystroke$1.invoke(EditTextWorkflow.kt:16)
	at com.squareup.workflow.StatefulWorkflowKt$action$2.apply(StatefulWorkflow.kt:265)
	at com.squareup.workflow.WorkflowActionKt.applyTo(WorkflowAction.kt:182)
	at com.squareup.workflow.internal.WorkflowNode$tick$1.invoke(WorkflowNode.kt:174)
	at com.squareup.workflow.internal.WorkflowNode$tick$$inlined$forEach$lambda$1.invokeSuspend(WorkflowNode.kt:196)
	at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:33)
	at kotlinx.coroutines.DispatchedTask.run(Dispatched.kt:241)
	at kotlinx.coroutines.EventLoopImplBase.processNextEvent(EventLoop.common.kt:270)
	at kotlinx.coroutines.BlockingCoroutine.joinBlocking(Builders.kt:79)
	at kotlinx.coroutines.BuildersKt__BuildersKt.runBlocking(Builders.kt:54)
	at kotlinx.coroutines.BuildersKt.runBlocking(Unknown Source)
	at kotlinx.coroutines.BuildersKt__BuildersKt.runBlocking$default(Builders.kt:36)
	at kotlinx.coroutines.BuildersKt.runBlocking$default(Unknown Source)
	at com.squareup.sample.helloterminal.terminalworkflow.TerminalWorkflowRunner.run(TerminalWorkflowRunner.kt:72)
	at com.squareup.sample.hellotodo.MainKt.main(Main.kt:23)
	at com.squareup.sample.hellotodo.MainKt.main(Main.kt)

Resolve running(Worker) inconsistencies

MyWorker()
  .running(context, "key")

This requires turning Worker in Kotlin into an abstract class and making Worker.run protected, to reduce confusion.

We need to something similar for Workflow, see square/workflow#996.

[kotlin] Experiment with simplified queueing architecture

Queue-per-worker is actually more complicated and probably less performant than necessary.

A simpler implementation could involve a single, global (to the instance of the workflow runtime) Channel of values that look something like:

data class PendingUpdate<O : Any>(
  val isDisposed: () -> Boolean,
  val applyUpdate: () -> O?
)

When a Worker is run for the first time, you subscribe to it by enqueing one of these structs:

scope.launch {
  val job = coroutineContext[Job]!!
  workerFlow.collect { value ->
    val pendingUpdate = PendingUpdate(
      isDisposed = job::isCancelled,
      applyUpdate = { workflowNode.applyActionForWorkerValue(value) }
    )
    channel.send(pendingUpdate)
  }
}

Where applyActionForWorkerValue is a function that gets the latest action handler for this worker, calculates the action, and applies it to the node's current state, returning either null or the root workflow's output.

This means all workers are only subscribed once, the first time they are ran, and the node just needs to keep track of all those jobs so it can dispose them when the workflow is torn down. The channel could have capacity of 0, so all workers experience backpressure immediately on contention, but I think we could also buffer pending updates without negative effect (since the consumer will check for disposed events before applying anyway).

Rendering/UI Events

There are a couple options for support non-blocking rendering events. Launch-per-event is a simpler, more elegant solution.

Secondary queue

Since UI events can't handle backpressure, an additional, unbounded channel would be created at the runtime level that would be used to pump rendering events into (i.e. back the RenderContext.actionSink). A coroutine would be launched for the lifetime of the workflow that would just forward events from this channel into the main one. Or the consumer could select over them both (the former is slightly more fair, I think).

Launch-per-event

An alternative solution is to launch a new coroutine in the workflow node's CoroutineScope every time an event is sent. No secondary queue would be required, and the coroutine runtime would take care of clearing cancelled workflows' events from the queue. It's important to process UI events in the order in which they were sent – fortunately, channels are "fair", so if rendering events start coming in too fast, they will be processed in FIFO order.

Consuming PendingUpdates

In WorkflowLoop, after the render pass finishes, you receive the next PendingUpdate from the queue. If isDisposed returns true, it means that struct represents an update for a worker/rendering-event that is now stale, so dequeue another and repeat until you find an undisposed update. Once isDisposed returns false, just call applyUpdate, emit the top-level output if present, and then do another render pass.

Originally posted by @zach-klippenstein in square/workflow#907 (comment)

Root runWorkflowLoop has inconsistent output behavior compared to its WorkflowNode children

In RealWorkflowLoop#runWorkflowLoop, the order of operations after an output for the root workflow's runWorkflowLoop is:

  1. output = select { .. }
  2. doRender
  3. output?.let { onOutput }

However, child workflows have their outputs immediately executed when the WorkflowAction is run.

  • From WorkflowNode#tick we call WorkflowNode#applyAction, but notice that we call output?.let(emitOutputToParent) immediately.

This difference in behavior manifests in the root workflow performing an extra render pass before the root workflow runner can receive an output and potentially end the workflow.

This extra render pass can cause a crash if the workflow does not expect to have render called after emitting an output (because the presumption is that the output stop the workflow from running).


To work around this, I can wrap my root workflow R in another workflow P that has 2 states: Running and CancelledAfterOutput. Initially, P will be in the Running state, and call renderChild on R. But after 1 output from R, P transitions to the CancelledAfterOutput state, which doesn't call renderChild on R. This "fixes" the errant extra render pass.

Add a renderTester method for expecting typed workers

Eg expectTypedWorker. It's technically a bit weird because it's basically leaking an implementation detail of a private type, but it's been asked for internally, I think it's probably a super common use case, and the fact that that behavior is a private implementation detail is weird to begin with.

I think it will also become less weird after we get implement GUWT.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.