You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
### **HAL** is a non-deterministic [finite-state machine](https://en.wikipedia.org/wiki/Finite-state_machine) for Android & JVM built with [Coroutines](https://kotlinlang.org/docs/reference/coroutines-overview.html) and [LiveData](https://developer.android.com/topic/libraries/architecture/livedata).
14
+
### **HAL** is a non-deterministic [finite-state machine](https://en.wikipedia.org/wiki/Finite-state_machine) for Android & JVM built with [Coroutines Flow](https://kotlinlang.org/docs/reference/coroutines/flow.html) and [LiveData](https://developer.android.com/topic/libraries/architecture/livedata).
15
15
16
16
#### Why non-deterministic?
17
17
@@ -37,7 +37,6 @@ It's a tribute to [HAL 9000](https://en.wikipedia.org/wiki/HAL_9000) (**H**euris
37
37
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!
38
38
39
39
## Usage
40
-
41
40
First, declare your `Action`s and `State`s. They *must* implement `HAL.Action` and `HAL.State` respectively.
42
41
43
42
```kotlin
@@ -63,32 +62,31 @@ sealed class MyState : HAL.State {
63
62
Next, implement the `HAL.StateMachine<YourAction, YourState>` interface in your `ViewModel`, `Presenter`, `Controller` or similar.
64
63
65
64
The `HAL` class receives the following parameters:
65
+
* The initial state
66
66
* 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))
67
-
* An initial state
68
-
* An *optional* capacity to specify the [Channel buffer size](https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.channels/-channel/index.html) (default is [Channel.UNLIMITED](https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.channels/-channel/-u-n-l-i-m-i-t-e-d.html))
69
67
* 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))
70
-
* A reducer function, `suspend (action: A, transitionTo: (S) -> Unit) -> Unit`, where:
68
+
* A reducer function, `suspend (action: A, state: S) -> Unit`, where:
71
69
-`suspend`: the reducer runs inside a `CoroutineScope`, so you can run IO and other complex tasks without worrying about block the Main Thread
72
70
-`action: A`: the action emitted to the state machine
73
-
-`transitionTo: (S) -> Unit`: the function responsible for changing the state
71
+
-`state: S`: the current state of the state machine
74
72
75
-
You should handle all actions inside the reducer function. Call `transitionTo()` whenever you need to change the state (it can be called multiple times).
73
+
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).
overridevalhal by HAL(viewModelScope, MyState.Init) { action, transitionTo->
78
+
overridevalstateMachine by HAL(MyState.Init, viewModelScope) { action, state->
81
79
when (action) {
82
80
isMyAction.LoadPosts-> {
83
-
transitionTo(MyState.Loading)
81
+
+MyState.Loading
84
82
85
83
try {
86
84
// You can run suspend functions without blocking the Main Thread
87
85
val posts = postRepository.getPosts()
88
86
// And emit multiple states per action
89
-
transitionTo(MyState.PostsLoaded(posts))
87
+
+MyState.PostsLoaded(posts)
90
88
} catch(e:Exception) {
91
-
transitionTo(MyState.Error("Ops, something went wrong."))
89
+
+MyState.Error("Ops, something went wrong.")
92
90
}
93
91
}
94
92
@@ -100,9 +98,7 @@ class MyViewModel(private val postRepository: PostRepository) : ViewModel(), HAL
100
98
}
101
99
```
102
100
103
-
Finally, choose a class to emit actions to your state machine and observe state changes, it can be an `Activity`, `Fragment` or any other class.
104
-
105
-
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) (highly recommended if you're on Android), just pass your `LifecycleOwner` to `observeState()`, otherwise HAL will use a default [Callback-based state observer](https://github.com/adrielcafe/HAL/blob/master/hal-core/src/main/kotlin/cafe/adriel/hal/observer/CallbackStateObserver.kt) (which is best suited for JVM-only applications).
101
+
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.
106
102
107
103
```kotlin
108
104
classMyActivity : AppCompatActivity() {
@@ -112,12 +108,13 @@ class MyActivity : AppCompatActivity() {
112
108
overridefunonCreate(savedInstanceState:Bundle?) {
113
109
114
110
// Easily emit actions to your State Machine
111
+
// You can all use: viewModel.emit(MyAction.LoadPosts)
115
112
loadPostsBt.setOnClickListener {
116
-
viewModel +MyAction.LoadPosts
113
+
viewModel +=MyAction.LoadPosts
117
114
}
118
115
119
-
// Observe and handle state changes backed by a LiveData
120
-
viewModel.observeState(lifecycleOwner =this) { state ->
116
+
// Observe and handle state changes
117
+
viewModel.observeState(lifecycleScope) { state ->
121
118
when (state) {
122
119
isMyState.Init-> showWelcomeMessage()
123
120
@@ -132,29 +129,84 @@ class MyActivity : AppCompatActivity() {
132
129
}
133
130
```
134
131
135
-
### Custom StateObserver
132
+
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).
133
+
134
+
```kotlin
135
+
// Observe and handle state changes backed by LiveData
136
+
viewModel.observeState(lifecycleOwner) { state ->
137
+
// Handle state
138
+
}
139
+
```
140
+
141
+
### TEA/Redux like approach
142
+
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 Store in [Redux](https://redux.js.org/introduction/three-principles)? I have good news: you can do the same with HAL!
136
143
137
-
You can easily create your custom state observer by implementing the `StateObserver<State>` interface:
144
+
Instead of use a sealed class with multiple states just create a single data class to represent your entire state:
145
+
146
+
```kotlin
147
+
sealedclassMyAction : HAL.Action {
148
+
// Declare your actions as usual
149
+
}
150
+
151
+
// Tip: use default parameters to represent your initial state
152
+
data classMyState(
153
+
valposts:List<Post> = emptyList(),
154
+
valloading:Boolean = false,
155
+
valerror:String? = null
156
+
) : HAL.State
157
+
```
158
+
159
+
Now, when handling the emitted actions use `state.copy()` to change your state:
160
+
161
+
```kotlin
162
+
overrideval stateMachine by HAL(MyState(), viewModelScope) { action, state ->
163
+
when (action) {
164
+
isNetworkAction.LoadPosts-> {
165
+
+state.copy(loading =true)
166
+
167
+
try {
168
+
val posts = postRepository.getPosts()
169
+
+state.copy(posts = posts)
170
+
} catch (e:Throwable) {
171
+
+state.copy(error ="Ops, something went wrong.")
172
+
}
173
+
}
174
+
175
+
isMyAction.AddPost-> {
176
+
/* Handle action */
177
+
}
178
+
}
179
+
}
180
+
```
181
+
182
+
And finally you can handle the state as a single source of truth:
183
+
184
+
```kotlin
185
+
viewModel.observeState(lifecycleScope) { state ->
186
+
showPosts(state.posts)
187
+
setLoading(state.loading)
188
+
state.error?.let(::showError)
189
+
}
190
+
```
191
+
192
+
### Custom StateObserver
193
+
If needed, you can also create your custom state observer by implementing the `StateObserver<S>` interface:
138
194
139
195
```kotlin
140
196
classMyCustomStateObserver<S:HAL.State>(
141
-
privatevalmyAwesomeParam:MyAwesomeClass,
142
-
overridevalobserver: (S) ->Unit
143
-
) : StateObserver<S> {
197
+
privatevalmyAwesomeParam:MyAwesomeClass
198
+
) : HAL.StateObserver<S> {
144
199
145
-
overridefuntransitionTo(newState:S) {
146
-
// Do any kind of operation here and call `observer(newState)` in the end
147
-
// IMPORTANT: this method runs on the Main Thread!
200
+
overridefunobserve(receiver:ReceiveChannel<S>) {
201
+
// You should consume the channel and handle the incoming states
148
202
}
149
203
}
150
204
```
151
205
152
206
And to use, just create an instance of it and pass to `observeState()` function:
153
207
154
208
```kotlin
155
-
viewModel.observeState(MyCustomStateObserver(myAwesomeParam) { state ->
0 commit comments