Ecosyste.ms: Awesome
An open API service indexing awesome lists of open source software.
https://github.com/adrielcafe/hal
🔴 A non-deterministic finite-state machine for Android & JVM that won't let you down
https://github.com/adrielcafe/hal
android android-library coroutines coroutines-flow finite-state-machine hal kotlin kotlin-android kotlin-library livedata machine non-deterministic-finite-automaton state state-machine
Last synced: about 2 months ago
JSON representation
🔴 A non-deterministic finite-state machine for Android & JVM that won't let you down
- Host: GitHub
- URL: https://github.com/adrielcafe/hal
- Owner: adrielcafe
- License: mit
- Created: 2019-06-20T22:10:00.000Z (over 5 years ago)
- Default Branch: master
- Last Pushed: 2021-08-14T01:30:18.000Z (over 3 years ago)
- Last Synced: 2024-08-04T08:07:36.081Z (5 months ago)
- Topics: android, android-library, coroutines, coroutines-flow, finite-state-machine, hal, kotlin, kotlin-android, kotlin-library, livedata, machine, non-deterministic-finite-automaton, state, state-machine
- Language: Kotlin
- Homepage:
- Size: 278 KB
- Stars: 79
- Watchers: 6
- Forks: 0
- Open Issues: 2
-
Metadata Files:
- Readme: README.md
- Funding: .github/FUNDING.yml
- License: LICENSE
Awesome Lists containing this project
README
[![JitPack](https://img.shields.io/jitpack/v/github/adrielcafe/hal.svg?style=for-the-badge)](https://jitpack.io/#adrielcafe/hal)
[![Android API](https://img.shields.io/badge/api-16%2B-brightgreen.svg?style=for-the-badge)](https://android-arsenal.com/api?level=16)
[![Bitrise](https://img.shields.io/bitrise/29bfee3f365ee4b9/master.svg?style=for-the-badge&token=AWE1QrlM0cgnpevpS1Tmrw)](https://app.bitrise.io/app/29bfee3f365ee4b9)
[![Codacy](https://img.shields.io/codacy/grade/590119aba1d14ea38908d6c1c8c11f07.svg?style=for-the-badge)](https://www.codacy.com/app/adriel_cafe/hal)
[![Codecov](https://img.shields.io/codecov/c/github/adrielcafe/hal/master.svg?style=for-the-badge)](https://codecov.io/gh/adrielcafe/hal)
[![kotlin](https://img.shields.io/github/languages/top/adrielcafe/hal.svg?style=for-the-badge)](https://kotlinlang.org/)
[![ktlint](https://img.shields.io/badge/code%20style-%E2%9D%A4-FF4081.svg?style=for-the-badge)](https://ktlint.github.io/)
[![License MIT](https://img.shields.io/github/license/adrielcafe/hal.svg?style=for-the-badge&color=yellow)](https://opensource.org/licenses/MIT)
### **HAL** is a non-deterministic [finite-state machine](https://en.wikipedia.org/wiki/Finite-state_machine) for Android & JVM built with [Coroutines StateFlow](https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.flow/-state-flow/index.html) and [LiveData](https://developer.android.com/topic/libraries/architecture/livedata).
#### Why non-deterministic?
Because in a [non-deterministic](https://www.tutorialspoint.com/automata_theory/non_deterministic_finite_automaton.htm) 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`[![turnstile diagram](https://github.com/adrielcafe/hal/blob/master/turnstile-diagram.jpg?raw=true)](https://www.smashingmagazine.com/2018/01/rise-state-machines/)
#### Why HAL?It's a tribute to [HAL 9000](https://en.wikipedia.org/wiki/HAL_9000) (**H**euristically programmed **AL**gorithmic computer), the sentient computer that controls the systems of the [Discovery One](https://en.wikipedia.org/wiki/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!
## Usage
First, declare your `Action`s and `State`s. They *must* implement `HAL.Action` and `HAL.State` respectively.```kotlin
sealed class MyAction : HAL.Action {object LoadPosts : MyAction()
data class AddPost(val post: Post) : MyAction()
}sealed class MyState : HAL.State {
object Init : MyState()
object Loading : MyState()
data class PostsLoaded(val posts: List) : MyState()
data class Error(val message: String) : MyState()
}
```Next, implement the `HAL.StateMachine` interface in your `ViewModel`, `Presenter`, `Controller` or similar.
The `HAL` class receives the following parameters:
* The initial state
* A [`CoroutineScope`](https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-coroutine-scope/) (tip: use the [built in viewModelScope](https://developer.android.com/topic/libraries/architecture/coroutines#viewmodelscope))
* An *optional* [CoroutineDispatcher](https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-coroutine-dispatcher/index.html) to run the reducer function (default is [Dispatcher.DEFAULT](https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-dispatchers/-default.html))
* 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 machineYou 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).
```kotlin
class MyViewModel(private val postRepository: PostRepository) : ViewModel(), HAL.StateMachine {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.
```kotlin
class MyActivity : AppCompatActivity() {private val viewModel by viewModels()
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](https://github.com/adrielcafe/HAL/blob/master/hal-livedata/src/main/kotlin/cafe/adriel/hal/livedata/observer/LiveDataStateObserver.kt), just pass your `LifecycleOwner` to `observeState()`, otherwise HAL will use the default [**Flow**-based state observer](https://github.com/adrielcafe/HAL/blob/master/hal-core/src/main/kotlin/cafe/adriel/hal/observer/FlowStateObserver.kt).
```kotlin
// 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](https://guide.elm-lang.org/architecture/) or the Store in [Redux](https://redux.js.org/introduction/three-principles)? 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:
```kotlin
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 = emptyList(),
val loading: Boolean = false,
val error: String? = null
) : HAL.State
```Now, when handling the emitted actions use `state.copy()` to change your state:
```kotlin
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:
```kotlin
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` interface:```kotlin
class MyCustomStateObserver(
private val myAwesomeParam: MyAwesomeClass
) : HAL.StateObserver {override fun observe(stateFlow: Flow) {
// Handle the incoming states
}
}
```And to use, just create an instance of it and pass to `observeState()` function:
```kotlin
viewModel.observeState(MyCustomStateObserver(myAwesomeParam))
```## Import to your project
1. Add the JitPack repository in your root build.gradle at the end of repositories:
```gradle
allprojects {
repositories {
maven { url 'https://jitpack.io' }
}
}
```2. Next, add the desired dependencies to your module:
```gradle
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: [![JitPack](https://img.shields.io/jitpack/v/github/adrielcafe/hal.svg?style=flat-square)](https://jitpack.io/#adrielcafe/hal)### Platform compatibility
| | `hal-core` | `hal-livedata` |
|---------|------------|----------------|
| Android | ✓ | ✓ |
| JVM | ✓ | |