State Management is a very regular thing we, as Android developers, have to do every day. From displaying a loading dialog when your user hits a button to managing network error with complex conditioning layers. The party is usually under control when we only have a couple of states to manage, such as:
- Is loading dialog being displayed?
- Is loading dialog dismissed, and the user’s data is ready to be shown?
Is the code above similar to you?
That looks very easy to manage. But when we invite more guests to the party, drunk guys could cause some problems.
- Should we display a loading dialog when the user comes back to the Fragment/Activity?
- Should we keep the loaded data when the user returns to the app, but we still allow them to refresh manually?
- We have the data, but now internet connect is dropped, what should we do?
- Internet connection is back, but when we should kick off the retry? Automatically or Manually by the user?
The state machine in Android series:
Part1: Why and How? (this.article
)
The list goes on and on and on….
Let take a step back and map them all out. If we look closer to the problems, they present some kind states:
- Data unloaded state: when the user arrives at the screen or start the function.
- Data loading: when the user triggers a data loading event (by a pull to refresh or the screen/function start on opening)
- Data Loaded successfully: When the data source (API, Cloud, Local database…) returns the needed data to the screen/function.
- Data error: When the data source returns an error (Network error, device error, API error…)
- Auto retry: When the code can automatically retry if the user’s device/machine satisfies some conditions (Wifi is on, the internet is accessible, the device has ) after the Data Error state happens.
- User retry: When the user hit the retry button or pull to refresh.
Present all states in UML Finite State Machine:
A finite-state machine (FSM) or finite-state automaton (FSA, plural: automata), finite automaton, or simply a state machine, is a mathematical model of computation. It is an abstract machine that can be in exactly one of a finite number of states at any given time. The FSM can change from one state to another in response to some inputs. (Wikipedia)
A Finite state machine includes:
- States: States of process/object within its lifecycle.
- Events: The trigger which moves the object from state to state. They could be triggered internally and externally.
- Side Effect: The result/side effect might happen when an event is triggered.
Let categorize our states by the above definition.
- States: Data Unloaded, Data Loading, Data loaded Successfully, Data Error, Auto Retrying, App gets backgrounded.
- Events: Start loading data, Returns success data, return an error, trigger auto-retry, user Userretry, move app to the background.
- Side Effects (Actions): Start loader, Display success data, display error, trigger auto-retry checker, start retrying, move app to the background.
Taking one more step further to put them into a diagram:
Now your processes are clearer that helps your coworkers follow your logic easier and greatly support your maintenance.
Transforming state chart to code:
There are several options to go from here. We could either write our own state machine library or using a third-party one. In this article, I pick the State Machine library from Tinder’s team, which is great work from the team and suits all of the needs to drive our logic.
Firstly, Let’s transform our states, events, and side effects into code:
And put our new classes into the new state machine class:
class UserDataStateMachine() {
val stateMachine = StateMachine.create<
UserDataState,
UserDataEvent,
UserDataSideEffect> {
}
}
We need to indicate an initial state that helps the library know which state the state machine needs to be assigned whenever it is created.
val stateMachine = StateMachine.create<
UserDataState,
UserDataEvent,
UserDataSideEffect> {
initialState(UserDataState.DataUnloaded)
}
Next, we will define the event for every state and also the side effect of each event (if it has any).
Let’s start with the DataUnloaded
state first.
...
initialState(UserDataState.DataUnloaded)
// DataUnloaded state
state<UserDataState.DataUnloaded> {
on<UserDataEvent.StartLoadingData> {
transitionTo(
state = UserDataState.DataLoading,
sideEffect = UserDataSideEffect.StartLoader
)
}
on<UserDataEvent.AppGetBackgrounded> {
transitionTo(UserDataState.DataUnloaded)
}
}
...
The above code defines that when the state machine current state is DataUnloaded
and it gets:
- the
StartLoadingData
event, the state machine will be moved toDataLoading
state and also execute theStartLoadingData
side effect. - the
AppGetBackgrounded
event, the state machine will be moved toDatUnloaded
state.
Apply the same mechanism to the other states, and we will have:
With that, we finish creating a state machine from the statechart.
While working on this article, I found out that the chart does not allow the user to manually retry when the final error happens. So let modify the chart and the code a little bit to add the RetryByUser
event to the DataError
state:
...
// Data error
state<UserDataState.DataError> {
on<UserDataEvent.AppGetBackgrounded> {
transitionTo(state = UserDataState.DataUnloaded)
}
on<UserDataEvent.RetryByUser> {
transitionTo(
state = UserDataState.DataLoading,
sideEffect = UserDataSideEffect.StartLoader
)
}
}
...
Now, the user can execute a try action when the process show error or success.
Here is the unit test to verify our state machine with some cases:
With support from the state machine, our code gains robustness easily by the ability to unit test the underline logic and a clear vision of the statechart. If the state machine state is DataLoading
, it can’t go to DataError
because it does not support the ReturnFinalError
event even the code call the transition. That way, we always make sure the code will follow the defined logic without exception.
Conclusion
That is not all the benefit of the state machine could bring us. In the next article, I will show you how we can define the logic for the Auto Trigger Mechanism to achieve:
- The mechanism can tell when to start retrying or stop and send out the final error.
- The mechanism also can do some pre-retry logic, such as WIFI and Internet check.
In the next article, We will also integrate the state machine into an Android application to give us a deeper understanding of how it could fit into the application.
See you in the next article. Thank you very much for reading. This concept is not new but has not been very popular, so I hope this article could gain you some knowledge and joy.