What the Formula? Managing state on Android

Laimonas Turauskas

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.

Note: we could refactor out a reusable ButtonRenderView.

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 Formula interface.

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 formula-android module.

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 isRunning to 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 state class.

The actual implementation to create a display value from milliseconds is a bit complex, so let’s just append ms to time passed.

To update 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.

Note that we don’t manually subscribe to the Observable. Formula runtime handles the subscriptions for us.

Starting the stopwatch now updates the time passed label. The logic here is very simple: when isRunning is 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 context.callback.

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 0 and isRunning is set to false.

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.

Source link


Please enter your comment!
Please enter your name here