{"id":13822062,"url":"https://github.com/adrielcafe/hal","last_synced_at":"2025-04-23T16:34:05.842Z","repository":{"id":52409342,"uuid":"192993166","full_name":"adrielcafe/hal","owner":"adrielcafe","description":"🔴 A non-deterministic finite-state machine for Android \u0026 JVM that won't let you down","archived":false,"fork":false,"pushed_at":"2021-08-14T01:30:18.000Z","size":285,"stargazers_count":79,"open_issues_count":2,"forks_count":0,"subscribers_count":6,"default_branch":"master","last_synced_at":"2024-08-04T08:07:36.081Z","etag":null,"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"],"latest_commit_sha":null,"homepage":"","language":"Kotlin","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/adrielcafe.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":".github/FUNDING.yml","license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null},"funding":{"ko_fi":"adrielcafe"}},"created_at":"2019-06-20T22:10:00.000Z","updated_at":"2024-06-27T13:01:03.000Z","dependencies_parsed_at":"2022-09-06T05:21:43.663Z","dependency_job_id":null,"html_url":"https://github.com/adrielcafe/hal","commit_stats":null,"previous_names":[],"tags_count":6,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/adrielcafe%2Fhal","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/adrielcafe%2Fhal/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/adrielcafe%2Fhal/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/adrielcafe%2Fhal/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/adrielcafe","download_url":"https://codeload.github.com/adrielcafe/hal/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":223930300,"owners_count":17227046,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["android","android-library","coroutines","coroutines-flow","finite-state-machine","hal","kotlin","kotlin-android","kotlin-library","livedata","machine","non-deterministic-finite-automaton","state","state-machine"],"created_at":"2024-08-04T08:01:40.927Z","updated_at":"2024-11-10T08:27:07.567Z","avatar_url":"https://github.com/adrielcafe.png","language":"Kotlin","readme":"[![JitPack](https://img.shields.io/jitpack/v/github/adrielcafe/hal.svg?style=for-the-badge)](https://jitpack.io/#adrielcafe/hal) \n[![Android API](https://img.shields.io/badge/api-16%2B-brightgreen.svg?style=for-the-badge)](https://android-arsenal.com/api?level=16) \n[![Bitrise](https://img.shields.io/bitrise/29bfee3f365ee4b9/master.svg?style=for-the-badge\u0026token=AWE1QrlM0cgnpevpS1Tmrw)](https://app.bitrise.io/app/29bfee3f365ee4b9) \n[![Codacy](https://img.shields.io/codacy/grade/590119aba1d14ea38908d6c1c8c11f07.svg?style=for-the-badge)](https://www.codacy.com/app/adriel_cafe/hal) \n[![Codecov](https://img.shields.io/codecov/c/github/adrielcafe/hal/master.svg?style=for-the-badge)](https://codecov.io/gh/adrielcafe/hal) \n[![kotlin](https://img.shields.io/github/languages/top/adrielcafe/hal.svg?style=for-the-badge)](https://kotlinlang.org/) \n[![ktlint](https://img.shields.io/badge/code%20style-%E2%9D%A4-FF4081.svg?style=for-the-badge)](https://ktlint.github.io/) \n[![License MIT](https://img.shields.io/github/license/adrielcafe/hal.svg?style=for-the-badge\u0026color=yellow)](https://opensource.org/licenses/MIT) \n\n\u003cp align=\"center\"\u003e\n    \u003cimg width=\"200px\" height=\"200px\" src=\"https://github.com/adrielcafe/hal/blob/master/hal-logo.png?raw=true\"\u003e\n\u003c/p\u003e\n\n### **HAL** is a non-deterministic [finite-state machine](https://en.wikipedia.org/wiki/Finite-state_machine) for Android \u0026amp; 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).\n\n#### Why non-deterministic?\n\nBecause 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.\n\nUse cases:\n* InsertCoin `transition to` Unlocked\n* LoadPosts `transition to` Loading then `transition to` Success or Error\n* LogMessage `don't transition` \n\n[![turnstile diagram](https://github.com/adrielcafe/hal/blob/master/turnstile-diagram.jpg?raw=true)](https://www.smashingmagazine.com/2018/01/rise-state-machines/)\n    \n#### Why HAL?\n\nIt'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. \n\n\u003cp align=\"center\"\u003e\n    \u003ci\u003e\"I'm sorry, Dave. I'm afraid I can't do that.\" (HAL 9000)\u003c/i\u003e\n\u003c/p\u003e\n\n---\n\nThis 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!\n\n## Usage\nFirst, declare your `Action`s and `State`s. They *must* implement `HAL.Action` and `HAL.State` respectively.\n\n```kotlin\nsealed class MyAction : HAL.Action {\n\n    object LoadPosts : MyAction()\n    \n    data class AddPost(val post: Post) : MyAction()\n}\n\nsealed class MyState : HAL.State {\n\n    object Init : MyState()\n    \n    object Loading : MyState()\n    \n    data class PostsLoaded(val posts: List\u003cPost\u003e) : MyState()\n    \n    data class Error(val message: String) : MyState()\n}\n```\n\nNext, implement the `HAL.StateMachine\u003cYourAction, YourState\u003e` interface in your `ViewModel`, `Presenter`, `Controller` or similar.\n\nThe `HAL` class receives the following parameters:\n* The initial state\n* 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))\n* 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))\n* A reducer function, `suspend (action: A, state: S) -\u003e Unit`, where:\n    - `suspend`: the reducer runs inside a `CoroutineScope`, so you can run IO and other complex tasks without worrying about block the Main Thread\n    - `action: A`: the action emitted to the state machine \n    - `state: S`: the current state of the state machine\n\nYou 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).\n\n```kotlin\nclass MyViewModel(private val postRepository: PostRepository) : ViewModel(), HAL.StateMachine\u003cMyAction, MyState\u003e {\n\n    override val stateMachine by HAL(MyState.Init, viewModelScope) { action, state -\u003e\n        when (action) {\n            is MyAction.LoadPosts -\u003e {\n                +MyState.Loading\n                \n                try {\n                    // You can run suspend functions without blocking the Main Thread\n                    val posts = postRepository.getPosts()\n                    // And emit multiple states per action\n                    +MyState.PostsLoaded(posts)\n                } catch(e: Exception) {\n                    +MyState.Error(\"Ops, something went wrong.\")\n                }\n            }\n            \n            is MyAction.AddPost -\u003e {\n                /* Handle action */\n            }\n        }\n    }\n}\n```\n\nFinally, 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.\n\n```kotlin\nclass MyActivity : AppCompatActivity() {\n\n    private val viewModel by viewModels\u003cMyViewModel\u003e()\n\n    override fun onCreate(savedInstanceState: Bundle?) {\n    \n        // Easily emit actions to your State Machine\n        // You can all use: viewModel.emit(MyAction.LoadPosts)\n        loadPostsBt.setOnClickListener {\n            viewModel += MyAction.LoadPosts\n        }\n        \n        // Observe and handle state changes\n        viewModel.observeState(lifecycleScope) { state -\u003e\n            when (state) {\n                is MyState.Init -\u003e showWelcomeMessage()\n                \n                is MyState.Loading -\u003e showLoading()\n                \n                is MyState.PostsLoaded -\u003e showPosts(state.posts)\n                \n                is MyState.Error -\u003e showError(state.message)\n            }\n        }\n    }\n}\n```\n\nIf 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).\n\n```kotlin\n// Observe and handle state changes backed by LiveData\nviewModel.observeState(lifecycleOwner) { state -\u003e\n    // Handle state\n}\n```\n\n### Single source of truth\nDo 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!\n\nInstead of use a sealed class with multiple states just create a single data class to represent your entire state:\n\n```kotlin\nsealed class MyAction : HAL.Action {\n    // Declare your actions as usual\n}\n\n// Tip: use default parameters to represent your initial state\ndata class MyState(\n    val posts: List\u003cPost\u003e = emptyList(),\n    val loading: Boolean = false,\n    val error: String? = null\n) : HAL.State\n```\n\nNow, when handling the emitted actions use `state.copy()` to change your state:\n\n```kotlin\noverride val stateMachine by HAL(MyState(), viewModelScope) { action, state -\u003e\n    when (action) {\n        is NetworkAction.LoadPosts -\u003e {\n            +state.copy(loading = true)\n\n            try {\n                val posts = postRepository.getPosts()\n                +state.copy(posts = posts)\n            } catch (e: Throwable) {\n                +state.copy(error = \"Ops, something went wrong.\")\n            }\n        }\n        \n        is MyAction.AddPost -\u003e {\n            /* Handle action */\n        }\n    }\n}\n```\n\nAnd finally you can handle the state as a single source of truth:\n\n```kotlin\nviewModel.observeState(lifecycleScope) { state -\u003e\n    showPosts(state.posts)\n    setLoading(state.loading)\n    state.error?.let(::showError)\n}\n```\n\n### Custom StateObserver\nIf needed, you can also create your custom state observer by implementing the `StateObserver\u003cS\u003e` interface:\n\n```kotlin\nclass MyCustomStateObserver\u003cS : HAL.State\u003e(\n    private val myAwesomeParam: MyAwesomeClass\n) : HAL.StateObserver\u003cS\u003e {\n\n    override fun observe(stateFlow: Flow\u003cS\u003e) {\n        // Handle the incoming states\n    }\n}\n``` \n\nAnd to use, just create an instance of it and pass to `observeState()` function: \n\n```kotlin\nviewModel.observeState(MyCustomStateObserver(myAwesomeParam))\n``` \n\n## Import to your project\n1. Add the JitPack repository in your root build.gradle at the end of repositories:\n```gradle\nallprojects {\n    repositories {\n        maven { url 'https://jitpack.io' }\n    }\n}\n```\n\n2. Next, add the desired dependencies to your module:\n```gradle\ndependencies {\n    // Core with Flow state observer\n    implementation \"com.github.adrielcafe.hal:hal-core:$currentVersion\"\n\n    // LiveData state observer only\n    implementation \"com.github.adrielcafe.hal:hal-livedata:$currentVersion\"\n}\n```\nCurrent version: [![JitPack](https://img.shields.io/jitpack/v/github/adrielcafe/hal.svg?style=flat-square)](https://jitpack.io/#adrielcafe/hal)\n\n### Platform compatibility\n\n|         | `hal-core` | `hal-livedata` |\n|---------|------------|----------------|\n| Android | ✓          | ✓              |\n| JVM     | ✓          |                |\n","funding_links":["https://ko-fi.com/adrielcafe"],"categories":["Kotlin"],"sub_categories":[],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fadrielcafe%2Fhal","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fadrielcafe%2Fhal","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fadrielcafe%2Fhal/lists"}