How to build an event-driven state machine in Android: a real-life example from SoftTeco
While Android apps are mostly built by MVP (Model-Viewer-Presenter) or MVVM (Model-View-ViewModel) architectural patterns, state machines are something not many developers actually think of. However, the state machine design pattern brings far more benefits and make the application more predictable, easier to debug, and much more maintainable.
The implementation of a state machine in the Android application is actually easier than many may think. But in the end, it will provide you with a stable and high-performing application with great usability and easy maintenance.
What exactly is a state machine?
If we address the definition provided by Wikipedia and similar resources, we will learn that a state machine is a mathematical model of computation. It originates from computer science and its main purpose is reading a series of inputs and switching to different states after reading a new input.
A state machine consists of the following components:
State: basically a status of the machine. While being in a state, a machine waits for the event that will trigger a transition to a different state.
Event: the actual inputs that the machine reads. If we speak about a mobile application as an example, an event would be an action performed by the user.
Transition: the movement of the machine from one state to another after a certain event occurs.
We can describe the work of a state machine with the following pattern: a state machine is in a certain state, then it receives input and transitions to another state. Usually, the number of states is finite and this is what makes the machine predictable.
A good example of a state machine in a mobile application is a ride-sharing app. When the user launches the app, he sees a home screen with a map - this is the first state of the application. Once the user selects the destination, the app shows available ride options - this is another state. The app switched to it after receiving an input in the form of choosing and entering a destination.
The benefits of using state machine pattern in your mobile application
The deployment of a state machine allows developers to focus only on the configuration of states and transitions without digging into the “how” aspect of the process. In simpler words, once a developer configures the needed states and the conditions for the transitions, the work is done and the machine will function in accordance with the set conditions.
Such well-defined work ensures a high level of predictability and eliminates the possibility of any malfunctions since every action is predefined. As well, it allows developers to visualize the work and makes the whole system more transparent.
Other benefits of using state machines in mobile application development include:
Elimination of hard coding conditions in the code,
Easy tracking of the processes due to the finite number of states and machine’s predictability,
An option to isolate certain pieces of code due to the independency of every action,
High stability of the system.
However, it won’t be correct to list down the benefits and ignore the possible flaws of having a state machine in your application. Here are some of the disadvantages of this pattern:
Not very suitable for asynchronous execution,
Possibility to have complex code if several transitions are executed from one state,
Limited support from the community,
Tricky data management and storage,
A possibility to obtain your own persistence layer in order to load balance properly.
Building a state machine for Android app: the breakdown of the process
In order to clearly explain the step-by-step process of building a state machine-powered Android app, we will have a look at the most simplistic made-up example of an app with 3 screens only.
Let’s say that we want to build a planner where a user can see the upcoming events. The screens will be as following:
Home screen with a list of the upcoming events
A screen where a user can choose a new event from the existing templates
A screen where a user can name the chosen event and submit it, thus, adding to the list on the home screen
Judging from the screens, a user will be able to take the following actions:
Click on “Add event” and switch from the home screen to the screen with the event templates
Choose a template and switch to a screen where the user can name the event and submit it
Name the event and submit it in order to get back to the home screen and see a renewed list of events
In order to make the whole machine work, here are the steps to follow.
Step 1: Define the states
Every screen of the application will have a State and we will have three states as a total: EventsList for the home screen, AddEvent for the screen with event selection and NameEvent for the screen where the user can name and submit the event.
Step 2: Define the actions
The next step is defining the actions that the users will take within the app. In this way, we will set up a certain pattern of behavior for the users which will contribute to the machine’s predictability and stability.
When on the home screen, the only action that the user can take is AddEventClicked Action. On the next screen, the user can select an event from the available templates so it will be EventSelected. Finally, the user can submit the new event and thus the action would be SubmitEventClicked.
Note: in state machine, an Action is not only the action by the user but it can also be a notification or a similar external event that can trigger the transition to another state.
Step 3: Connect the events with actions
Once you have a list of events and actions ready, it’s time to set up the equations and define the relations between them. At this stage, you define the user behavior patterns and give directions to the machine. Within our scenario, we will have the following equations:
EventList + AddEventClicked = AddEvent
AddEvent + EventSelected = NameEvent
NameEvent + SubmitEventClicked = EventList
In this way, we have built a framework for the state machine - now it is time to do the actual coding.
Step 4: Write a bit of code
For the sake of saving the space, we will not write the actual code but will outline the needed actions:
Create an interface for EventState in the state machine. Once this is done, every state with this interface will supply a new state after an action happens.
Model every Action as a sealed class. This is needed so we can use Kotlin when statement later.
Step 5: Implement the states and build the activity
For every state, you will need to create a class that implements the above-mentioned EventState. At this stage, you will specify what kind of action happens during each state, what the user sees at the screen (i.e. a list of events) and how the machine proceeds to the next state.
Be careful when writing logic for every state as the machine will work exactly as you define it. Once you define every state, you can connect the state machine to the Activity so it can independently carry out the needed tasks.
If you have a Model-View-Presenter architecture, you will need to write a contract in order for the View and Presenter to interact with each other. A contract specifies the way the two interfaces communicate and describes what kind of actions cause transitions to different states.
The trickiest part of designing a state machine is clearly defining all the intended states and events that will cause the machine’s transition. Otherwise, the actual implementation of this pattern is not very challenging but if done right, it will result in a very user-friendly and high-performing application that will also save you some time of coding.
SoftTeco’s real-life experience: building a state machine for an Android project
An example mentioned above describes a very abstract and simplistic state machine. In reality, a state machine for a mobile application is a complex and high-profile solution that demands careful maintenance and management.
We created a state machine for one of our projects which is a ride-sharing solution. While in the example above there are only three states and three actions, in reality, we got a complex tree of states that consists of more than dozens of states.
There is a base BaseState and we have several parent states such as IdleState or PickUpState. Each of these parent states corresponds to a screen a user sees upon performing a certain action. Next, every parent state has several child states that can be described as interstitial states between the primary transitions. For example, when choosing a destination, a user can check out the demand for the cars - and that would be a separate child state under the parent DestinationState (as an example).
The state tree is basically a state hierarchy: it allows us to easily switch between the states, see which state is the parent and which is the child one, and enables easy state and event monitoring and tracking. For every state, we write a set of events that can occur and we can also list down the restricted events.
Data storage in states and events
While some developers recommend decoupling the data storage from the state machine, our experience proves that this is the wrong decision that often leads to numerous issues. In fact, we prefer storing the data in states and events and here is why.
There must be one data source in order to ensure data veracity and stability. So if you isolate the data and store it elsewhere, this will result in confusion and possible bugs and errors as well as asynchronous operations.
All data fields are stored in the BaseState and descend by inheritance. All parent states have a certain data set that remains the same for all the child states of a parent state. In this way, one state stores only the data that is needed for this specific state and this, in turn, facilitates data management and storage.
Unit testing for state machines
One of the greatest advantages of the state machines that we noticed throughout our work is the possibility to conduct automated unit testing of all the states and events (events and states are tested separately). For the states, we test the set parameters and whether they are maintained and for the events, we test whether the states support them and whether they are received or not.
With the state machine, we get 95%-100% test coverage which is really great. This is achieved due to the stable and clear architecture of a state machine - while many software projects with poor architecture and badly written code cannot be tested automatically and require manual testing. When everything is in one place and written in clumsy code, it becomes hard to test the product’s performance and dependencies.
Among other advantages of the state machine is its actual nature. A state machine is the data, not the data display - and that means, a developer can rewrite the state machine in another programming language in no time.
Speaking about the data, nothing except for events can change the data in the state. So when the event arrives in the event handler, we can easily monitor that and instantly perform any debugging or testing.
One more interesting thing that we have in our state machine is state history. When a mistake occurs (i.e. there are no available rides at the moment), a state history takes the user back to the latest state which is recovered with all the data saved.
And last but not least - we use annotations for code generation. That means, when we define how the events are supported in a state, we get automatically generated method stubs for events processing and method stubs for unit testing. This helps developers to ensure that they implemented all the needed features and did not leave anything behind.