State management is usually the most complex part of a UI application. At Instacart, we have been using RxJava to manage state for a few years now. As our Android team grew, we realized that there was a steep learning curve for new developers to get started. We started to experiment with how we could reduce complexity. After a few iterations, we are excited to announce Formula — an open-source state management library built in Kotlin. Formula provides a simple, declarative, and composable API for managing your app complexity. The goal is to express the essence of your program without much ceremony.
Probably the easiest way to show you how Formula works is to build a simple stopwatch application. It will show how long the stopwatch has been running and allow the user to start/stop or reset it.
When working with Formula, the recommended way to start is by defining what the UI needs to display and what actions the user will be able to take. We model this with Kotlin data classes and call this concept a
RenderModel. We model user actions as event listeners on the render model.
Render model is an immutable representation of your view. Any time we want to update UI, we will create a new instance and pass it to the
RenderView. Render view is responsible for taking a render model and applying it to Android views.
With rendering logic out of the way, let’s see how we actually create the render model. Render model creation is the responsibility of the
Formula interface. Let’s create a
StopwatchFormula class that extends it.
Formula interface takes three generic parameters: Input, State and Render Model (we will not use input in this example). In Formula, we keep all our dynamic properties within a single Kotlin data class which we call
State. For the time being, we will use an empty State object as a placeholder. The
evaluate function takes the current
state and is responsible for creating the render model.
Our current implementation always returns the same render model. Before we make it dynamic, let’s connect this implementation to our render view. There is an extension function
start that creates an RxJava observable from the
For simplicity sake, I’ve placed this logic directly in the activity. In a real application, it should be placed within a surface that survives configuration changes such as AndroidX ViewModel or
Now that we are observing render model changes, let’s start making the UI dynamic. For example, when the user clicks the “Start” button, we want to reflect this change and update the button to show “Stop”. We need a dynamic property to keep track if the stopwatch was started. Let’s update our
State to include
isRunning. We initially set
false. If you have worked with Redux or MVI, this should look familiar to you.
Let’s update the start/stop button render model creation.
Any time user clicks this button, we want to toggle
isRunning and create a new render model. We will use
FormulaContext to accomplish this.
FormulaContext is a special object passed by the runtime that allows us to create callbacks and define transitions to a new state. Any time
onSelected is called we transition to a state where
isRunning is inverted (if it was
true it becomes
false and vice-versa). Instead of mutating the state, we use the data class
copy method to create a new instance. A transition will cause
evaluate to be called again with the new state, new render model will be created, the
Observable will emit the new value and our UI will be updated.
Now that we have a functioning button, we actually need to run the stopwatch and update the UI as time passes. Let’s add a new property to our
The actual implementation to create a display value from milliseconds is a bit complex, so let’s just append
ms to time passed.
timePassedInMillis we will use the RxJava interval operator.
When the stopwatch is running, we want to listen to this observable and update
timePassedInMillis on each event. To add this behavior we need to update
evaluate function. As part of each state change, Formula can decide what services it wants to run and listen to. We update our
Evaluation and use conditional logic within
context.updates block to declare when we want our observable to run.
Starting the stopwatch now updates the time passed label. The logic here is very simple: when
true, listen to
incrementTimePassedEvents and increment
timePassedInMillis. The updates mechanism is agnostic to RxJava so you need to wrap it using
RxStream. The event callback is very similar to UI event callbacks except it’s already scoped so you don’t need to use
We still need to handle the reset button clicks. This is the same as implementing the start/stop button. There are no new concepts to introduce here. Any time reset button is selected we transition to a new state where
timePassedInMillis is set to
isRunning is set to
And we are done. You can find the source code here.
There are still a lot of things that we didn’t go over such as composition, testing or side-effects. If you want to learn more, take a look at the documentation, samples or the Github repository. If you have any questions or feedback you can find me on Twitter.