HAL

General

Category
Free
Tag
FSM
License
MIT License
Min SDK
16 (Android 4.1 Jelly Bean)
Registered
Jul 1, 2019
Favorites
2
Link
https://github.com/adrielcafe/HAL
See also
Engine
Kaskade
Ken-Ken-Pa
EasyFlow
Dalek

Additional

Language
Kotlin
Version
1.0.1 (May 13, 2020)
Created
Jun 21, 2019
Updated
May 13, 2020
Owner
Adriel Café (adrielcafe)
Contributor
Adriel Café (adrielcafe)
1
Activity
Badge
Generate
Download
Source code
APK file

Advertisement

HAL is a non-deterministic finite-state machine for Android & JVM built with Coroutines StateFlow and LiveData.

Why non-deterministic?

Because in a non-deterministic finite-state machine, an action can lead to one, more than one, or no transition for a given state. That way we have more flexibility to handle any kind of scenario.

Use cases:

  • InsertCoin transition to Unlocked
  • LoadPosts transition to Loading then transition to Success or Error
  • LogMessage don't transition

Why HAL?

It's a tribute to HAL 9000 (Heuristically programmed ALgorithmic computer), the sentient computer that controls the systems of the Discovery One spacecraft.

"I'm sorry, Dave. I'm afraid I can't do that." (HAL 9000)

This project started as a library module in one of my personal projects, but I decided to open source it and add more features for general use. Hope you like!


Next, implement the HAL.StateMachine<YourAction, YourState> interface in your ViewModel, Presenter, Controller or similar.

The HAL class receives the following parameters:

  • The initial state
  • A CoroutineScope (tip: use the built in viewModelScope)
  • An optional CoroutineDispatcher to run the reducer function (default is Dispatcher.DEFAULT)
  • A reducer function, suspend (action: A, state: S) -> Unit, where:
    • suspend: the reducer runs inside a CoroutineScope, so you can run IO and other complex tasks without worrying about block the Main Thread
    • action: A: the action emitted to the state machine
    • state: S: the current state of the state machine

You should handle all actions inside the reducer function. Call transitionTo(newState) or simply +newState whenever you need to change the state (it can be called multiple times).

class MyViewModel(private val postRepository: PostRepository) : ViewModel(), HAL.StateMachine<MyAction, MyState> {

    override val stateMachine by HAL(MyState.Init, viewModelScope) { action, state ->
        when (action) {
            is MyAction.LoadPosts -> {
                +MyState.Loading
                
                try {
                    // You can run suspend functions without blocking the Main Thread
                    val posts = postRepository.getPosts()
                    // And emit multiple states per action
                    +MyState.PostsLoaded(posts)
                } catch(e: Exception) {
                    +MyState.Error("Ops, something went wrong.")
                }
            }
            
            is MyAction.AddPost -> {
                /* Handle action */
            }
        }
    }
}

Finally, choose a class to emit actions to your state machine and observe state changes, it can be an Activity, Fragment, View or any other class.

class MyActivity : AppCompatActivity() {

    private val viewModel by viewModels<MyViewModel>()

    override fun onCreate(savedInstanceState: Bundle?) {
    
        // Easily emit actions to your State Machine
        // You can all use: viewModel.emit(MyAction.LoadPosts)
        loadPostsBt.setOnClickListener {
            viewModel += MyAction.LoadPosts
        }
        
        // Observe and handle state changes
        viewModel.observeState(lifecycleScope) { state ->
            when (state) {
                is MyState.Init -> showWelcomeMessage()
                
                is MyState.Loading -> showLoading()
                
                is MyState.PostsLoaded -> showPosts(state.posts)
                
                is MyState.Error -> showError(state.message)
            }
        }
    }
}

If you want to use a LiveData-based state observer, just pass your LifecycleOwner to observeState(), otherwise HAL will use the default Flow-based state observer.

// Observe and handle state changes backed by LiveData
viewModel.observeState(lifecycleOwner) { state ->
    // Handle state
}

Single source of truth

Do you like the idea of have a single source of truth, like the Model in The Elm Architecture or the Store in Redux? I have good news: you can do the same with HAL!

Instead of use a sealed class with multiple states just create a single data class to represent your entire state:

sealed class MyAction : HAL.Action {
    // Declare your actions as usual
}

// Tip: use default parameters to represent your initial state
data class MyState(
    val posts: List<Post> = emptyList(),
    val loading: Boolean = false,
    val error: String? = null
) : HAL.State

Now, when handling the emitted actions use state.copy() to change your state:

override val stateMachine by HAL(MyState(), viewModelScope) { action, state ->
    when (action) {
        is NetworkAction.LoadPosts -> {
            +state.copy(loading = true)

            try {
                val posts = postRepository.getPosts()
                +state.copy(posts = posts)
            } catch (e: Throwable) {
                +state.copy(error = "Ops, something went wrong.")
            }
        }
        
        is MyAction.AddPost -> {
            /* Handle action */
        }
    }
}

And finally you can handle the state as a single source of truth:

viewModel.observeState(lifecycleScope) { state ->
    showPosts(state.posts)
    setLoading(state.loading)
    state.error?.let(::showError)
}

Custom StateObserver

If needed, you can also create your custom state observer by implementing the StateObserver<S> interface:

class MyCustomStateObserver<S : HAL.State>(
    private val myAwesomeParam: MyAwesomeClass
) : HAL.StateObserver<S> {

    override fun observe(stateFlow: Flow<S>) {
        // Handle the incoming states
    }
}

And to use, just create an instance of it and pass to observeState() function:

viewModel.observeState(MyCustomStateObserver(myAwesomeParam))

Import to your project

  1. Add the JitPack repository in your root build.gradle at the end of repositories:
allprojects {
    repositories {
        maven { url 'https://jitpack.io' }
    }
}
  1. Next, add the desired dependencies to your module:
dependencies {
    // Core with Flow state observer
    implementation "com.github.adrielcafe.hal:hal-core:$currentVersion"

    // LiveData state observer only
    implementation "com.github.adrielcafe.hal:hal-livedata:$currentVersion"
}

Current version:

Platform compatibility

hal-core hal-livedata
Android
JVM