State Machine in Android (Part 2): Android Integration and More

In the last article, we talked about a state machine and how it could help our Android Development. We will keep diving deeper into how it could help us more by adding logic into the Auto Retry Mechanism, implement side effects, and integrating it into an Android application.

The state machine in Android series:

Part1: Why and How?

Part2: Android integration, and more? (this.article)

Implement side effects

In the previous article, we only defined the side effects and triggered them along with specific events. We need to implement the behavior of those side effects.

First, we need to listen to the side effects by adding this code to the end of the state machine class:

....class UserDataStateMachine() {
val stateMachine = StateMachine.create<
UserDataState,
UserDataEvent,
UserDataSideEffect> {
initialState(UserDataState.DataUnloaded)
....
onTransition {
val validTransition = it as? StateMachine.Transition.Valid ?: return@onTransition
when (validTransition.sideEffect) {
UserDataSideEffect.StartLoader -> {
// Starting loader code go here
}
UserDataSideEffect.DisplayError -> {
// Display error code go here
}
UserDataSideEffect.DisplaySuccessData -> {
// Display success data code go here
}
UserDataSideEffect.TriggerAutoRetryChecker -> {
// Trigger auto retry checker code go here
}
}
}
}
}

Other components, like ViewModel or Presenter, can control the state machine. To support integrating to Android application, we need to make the state machine notify other components when side effect happens.

Let’s create a component that does the notification. Its name is UserDataSideEffectHandler

class UserDataSideEffectHandler {

fun startLoader() {

}
fun requestData() {

}
fun retry() {

}
fun displayError() {

}

fun displaySuccessData() {

}

fun triggerAutoRetryChecker() {

}
}

Here is how we integrate the new class into the state machine class:

...
class UserDataStateMachine(val sideEffectHandler: UserDataSideEffectHandler) {
....
onTransition {
val validTransition = it as? StateMachine.Transition.Valid ?: return@onTransition
when (validTransition.sideEffect) {
UserDataSideEffect.StartLoader -> {
sideEffectHandler.startLoader()
}
UserDataSideEffect.DisplayError -> {
sideEffectHandler.displayError()
}
UserDataSideEffect.DisplaySuccessData -> {
sideEffectHandler.displaySuccessData()
}
UserDataSideEffect.TriggerAutoRetryChecker -> {
sideEffectHandler.triggerAutoRetryChecker()
}
}
}
...

Now, external components need to listen from the side effect handler whenever a side effect happens.

To achieve that, we use the kotlin delegate observable pattern:

private var eventDelegate: UserDataUiEvent by Delegates.observable(UserDataUiEvent.UNKNOWN) { _, _, newValue ->
onEvent?.invoke(newValue)
}

var onEvent: ((event: UserDataUiEvent) -> Unit)? = null

enum class UserDataUiEvent {
enum class UserDataUiEvent {
START_LOADER,
DISPLAY_ERROR,
DISPLAY_SUCCESS_DATA,
REQUEST_DATA,
RETRY,
UNKNOWN
}

And set the corresponding event to the delegate from the side effects methods:

fun startLoader() {
eventDelegate = UserDataUIEvent(UserDataUiEventType.START_LOADER, retryCount)
}

fun requestData() {
eventDelegate = UserDataUIEvent(UserDataUiEventType.REQUEST_DATA)
}

fun retry() {
eventDelegate = UserDataUIEvent(UserDataUiEventType.RETRY)
}

fun displayError() {
eventDelegate = UserDataUIEvent(UserDataUiEventType.DISPLAY_ERROR)
}

fun displaySuccessData() {
eventDelegate = UserDataUIEvent(UserDataUiEventType.DISPLAY_SUCCESS_DATA)
}

With that setup, whoever wants to listen for the side effect of the state machine can initiate the onEvent method.

Implement logic for the auto-retry mechanism:

We can add a 3 times retry checker for the auto-retry process to decide whether the process should be retried.

private var retryCount : Int = AUTO_RETRY_DEFAULT_INIT
...
fun displayError() {
// Reset the retry count
retryCount = AUTO_RETRY_DEFAULT_INIT
eventDelegate = UserDataUIEvent(UserDataUiEventType.DISPLAY_ERROR)
}

fun displaySuccessData() {
// Reset the retry count
retryCount = AUTO_RETRY_DEFAULT_INIT
eventDelegate = UserDataUIEvent(UserDataUiEventType.DISPLAY_SUCCESS_DATA)
}

fun triggerAutoRetryChecker() {
if (retryCount >= AUTO_RETRY_MAX) {
// STOP the retry and move the state mach1ine to final error
stateMachine?.stateMachine?.transition(UserDataEvent.ReturnFinalError)
return
}

retryCount++
// Do the retry
stateMachine?.stateMachine?.transition(UserDataEvent.AutoRetry)
}
...
private const val AUTO_RETRY_MAX = 3
private const val AUTO_RETRY_DEFAULT_INIT = 1

We need to transition the state machine to the final error when the retry count is greater than the max value. We can keep the state machine instance in the handler class:

var stateMachine: UserDataStateMachine? = null

and transition to the ReturnFinalError state

if (retryCount > AUTO_RETRY_MAX) {
// STOP the retry and move the state machine to final error
stateMachine?.stateMachine?.transition(UserDataEvent.ReturnFinalError)
return
}

Integrate into an Android application

Let’s create a simple Android application with a button that starts a process that might randomly fail.

// TODO Screenshot here

In the application’s activity, we need to create a state machine instance. In this example, we will do everything in the activity class to low the scope that only focuses on integrating with the state machine.

private val userDataSideEffectHandler : UserDataSideEffectHandler = UserDataSideEffectHandler()
private val userDataStateMachine : UserDataStateMachine = UserDataStateMachine(userDataSideEffectHandler)

and define the onEvent callback method to listen for the state machine event:

userDataSideEffectHandler.onEvent = { event ->
runOnUiThread {
when (event.eventType) {
UserDataSideEffectHandler.UserDataUiEventType.START_LOADER -> {
progress_circular.visibility = View.VISIBLE
val retryCount : Int = event.data as Int
if (retryCount > UserDataSideEffectHandler.AUTO_RETRY_DEFAULT_INIT) {
tv_status.text = "Loading, Current retry count $retryCount"
} else {
tv_status.text = "Loading"
}
}

UserDataSideEffectHandler.UserDataUiEventType.DISPLAY_ERROR -> {
progress_circular.visibility = View.GONE
tv_status.text = "Failed"
btn_click_here.text = "Retry Process"
}
UserDataSideEffectHandler.UserDataUiEventType.DISPLAY_SUCCESS_DATA -> {
progress_circular.visibility = View.GONE
tv_status.text = "Success"
btn_click_here.text = "Retry Process"
}

UserDataSideEffectHandler.UserDataUiEventType.REQUEST_DATA,
UserDataSideEffectHandler.UserDataUiEventType.RETRY -> {
startProcess()
}

else -> {
// DO NOTHING
}
}
}

}

The above code defines actions to take for every UI event from the handler.

On the START_LOADER We show the loading progress bar and change the UI process status based on the current retry count.

On the DISPLAY_ERROR We dismiss the loading process bar and also change the status to “Failed.” The button text is also changed to “Retry Process” to hint the user to retry manually.

On the DISPLAY_SUCCESS_DATA We dismiss the loading progress bar and also change the status to “SUCCESS”. The button text is also changed to “Retry Process” to hint the user to retry manually.

On the REQUEST_DATA or RETRY, we kick off the process

Finally, we need to create a randomly fail process:

....
private val process: RandomFailProcess = RandomFailProcess()
....
class RandomFailProcess {
private val mutex : Mutex = Mutex()
suspend fun start() {
mutex.withLock {
if (!process()) {
throw Exception("Process failed")
}
}
}

private suspend fun process(): Boolean {
delay(2000)
val randomValue = Random.nextInt(0, 10)
return randomValue > 7
}
}

The method to start it also handles the state machine when the process is successful or fails.

private fun startProcess() {
coroutineScope.launch {
try {
process.start()
userDataStateMachine.stateMachine.transition(
event = UserDataEvent.ReturnSuccessData
)
} catch (ex : Exception) {
userDataStateMachine.stateMachine.transition(
event = UserDataEvent.ReturnAnError
)
}
}
}

Here is the demo of the application:

Check out the full source code on Github.

Conclusion

With the integration finish, we demonstrated the ability to handle complex state flows of the state machine for the Android application. That also proves the logic's robustness and easy maintenance because it is separate from the Android component code. It is highly reusable when you only need to set up the listeners on wherever you want event it is not Android component such as activity, fragment, or ViewModel.

Dad, Husband, Android Developer, Manga/Video Game Lover, Unity3d learner.