Skip to content

Common Patterns

There are a lot associated/generic types in workflow code – that doesn’t mean you always need to use all of them. Here are some common configurations we’ve seen.

Stateless Workflows

Remember that workflow state is made up of public and private parts. When a workflow’s state consists entirely of public state (i.e. it’s initializer arguments in Swift or PropsT in Kotlin), it can ignore all the machinery for private state. In Swift, theState type can be Void, and in Kotlin it can be Unit – such workflows are often referred to as “stateless”, since they have no state of their own.

Props-less Workflows

Some workflows manage all of their state internally, and have no public state (aka props). In Swift, this just means the workflow implementation has no parameters (although this is rare, see Injecting Dependencies below). In Kotlin, the PropsT type can be Unit. RenderContext has convenience overloads of most of its functions to implicitly pass Unit for these workflows.

Outputless Workflows

Workflows that only talk to their parent via their Rendering, and never emit any output, are encouraged to indicate that by using the bottom type as their Output type. In addition to documenting the fact that the workflow will never output, using the bottom type also lets the compiler enforce it – code that tries to emit outputs will not compile. In Swift, the Output type is specified as Never. In Kotlin, use Nothing.

Composite Workflows

Composition is a powerful tool for working with Workflows. A workflow can often accomplish a lot simply by rendering various children. It may just combine the renderings of multiple children, or use its props to determine which of a set of children to render. Such workflows can often be stateless.

One-and-done Workflows (RenderingT v. OutputT)

A common question is “why can’t I emit output from initialState,” or “what if my Workflow realizes it doesn’t actually need to run? The most efficient, and most expressive, way to handle this is to use an optional or conditional Rendering type, and an Output of Never/Nothing.

Imagine a PromptForPermissionMaybeWorkflow, that renders a UI to get a passcode, but only if that permission has not already been granted. If you make its RenderingT nullable (e.g. Screen?), it can return null to indicate that its job is done. Its callers will be synchronously informed that the coast is clear, and can immediately render what they actually care about.

Another variation of this pattern is to use a sealed class / enum type for Rendering, with a Working type that implements Screen, and a unviewable Finished type that carries the work product.

A good rule of thumb for choosing between using Rendering or Output is to remember that Output is event-like, and is always asynchronous. A parent waiting for an output must be given something to render in the meantime. Using Rendering is a great idiom for a one-and-done workflow tasked with providing a single product, especially one that might be available instantly.

Props values v. Injected Dependencies

Dependency injection is a technique for making code less coupled and more testable. In short, it’s better for classes/structs to accept their dependencies when they’re created instead of hard-coding them. Workflows typically have dependencies like specific Workers they need to perform some tasks, child workflows to delegate rendering to, or helpers for things like network requests, formatting and logging.

Swift

A Swift workflow typically receives its dependencies as initializer arguments, just like its input values, and is normally instantiated anew by its parent in each call to the parent’s render method. The factory pattern can be employed to keep knowledge of children’s implementation details from leaking into their parents.

Kotlin

Kotlin workflows make a more formal distinction between dependencies and props, via the PropsT parameter type on the Kotlin Workflow interface. Dependencies (e.g. a network service) are typically provided as constructor parameters, while props values (e.g. a record locator) are provided by the parent as an argument to the RenderContext.renderChild method. This works seamlessly with DI libraries like Dagger.

The careful reader will note that this is technically storing “state” in the workflow instance – something that is generally discouraged. However, since this “state” is never changed, we can make an exception for this case. If a workflow has properties, they should only be used to store injected dependencies or dependencies derived from injected ones (e.g. Workers created from Observables).

Info

This difference between Swift and Kotlin practices is a side effect of Kotlin’s lack of a parallel to Swift’s Self type. Kotlin has no practical way to provide a method like Swift’s Workflow.workflowDidChange, which accepts a strongly typed reference to the instance from the previous run of a parent’s Render method. Kotlin’s alternative, StatefulWorkflow.onPropsChanged, requires the extra PropsT type parameter.