Skip to content

Commit 262ec77

Browse files
committed
feat: flow support and complete refactor
2 parents 1acbffa + 63e6314 commit 262ec77

33 files changed

+665
-439
lines changed

README.md

Lines changed: 81 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
<img width="200px" height="200px" src="https://github.com/adrielcafe/hal/blob/master/hal-logo.png?raw=true">
1212
</p>
1313

14-
### **HAL** is a non-deterministic [finite-state machine](https://en.wikipedia.org/wiki/Finite-state_machine) for Android &amp; 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 &amp; JVM built with [Coroutines Flow](https://kotlinlang.org/docs/reference/coroutines/flow.html) and [LiveData](https://developer.android.com/topic/libraries/architecture/livedata).
1515

1616
#### Why non-deterministic?
1717

@@ -37,7 +37,6 @@ It's a tribute to [HAL 9000](https://en.wikipedia.org/wiki/HAL_9000) (**H**euris
3737
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!
3838

3939
## Usage
40-
4140
First, declare your `Action`s and `State`s. They *must* implement `HAL.Action` and `HAL.State` respectively.
4241

4342
```kotlin
@@ -63,32 +62,31 @@ sealed class MyState : HAL.State {
6362
Next, implement the `HAL.StateMachine<YourAction, YourState>` interface in your `ViewModel`, `Presenter`, `Controller` or similar.
6463

6564
The `HAL` class receives the following parameters:
65+
* The initial state
6666
* 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))
6967
* 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:
7169
- `suspend`: the reducer runs inside a `CoroutineScope`, so you can run IO and other complex tasks without worrying about block the Main Thread
7270
- `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
7472

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).
7674

7775
```kotlin
7876
class MyViewModel(private val postRepository: PostRepository) : ViewModel(), HAL.StateMachine<MyAction, MyState> {
7977

80-
override val hal by HAL(viewModelScope, MyState.Init) { action, transitionTo ->
78+
override val stateMachine by HAL(MyState.Init, viewModelScope) { action, state ->
8179
when (action) {
8280
is MyAction.LoadPosts -> {
83-
transitionTo(MyState.Loading)
81+
+MyState.Loading
8482

8583
try {
8684
// You can run suspend functions without blocking the Main Thread
8785
val posts = postRepository.getPosts()
8886
// And emit multiple states per action
89-
transitionTo(MyState.PostsLoaded(posts))
87+
+MyState.PostsLoaded(posts)
9088
} catch(e: Exception) {
91-
transitionTo(MyState.Error("Ops, something went wrong."))
89+
+MyState.Error("Ops, something went wrong.")
9290
}
9391
}
9492

@@ -100,9 +98,7 @@ class MyViewModel(private val postRepository: PostRepository) : ViewModel(), HAL
10098
}
10199
```
102100

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.
106102

107103
```kotlin
108104
class MyActivity : AppCompatActivity() {
@@ -112,12 +108,13 @@ class MyActivity : AppCompatActivity() {
112108
override fun onCreate(savedInstanceState: Bundle?) {
113109

114110
// Easily emit actions to your State Machine
111+
// You can all use: viewModel.emit(MyAction.LoadPosts)
115112
loadPostsBt.setOnClickListener {
116-
viewModel + MyAction.LoadPosts
113+
viewModel += MyAction.LoadPosts
117114
}
118115

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 ->
121118
when (state) {
122119
is MyState.Init -> showWelcomeMessage()
123120

@@ -132,29 +129,84 @@ class MyActivity : AppCompatActivity() {
132129
}
133130
```
134131

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!
136143

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+
sealed class MyAction : HAL.Action {
148+
// Declare your actions as usual
149+
}
150+
151+
// Tip: use default parameters to represent your initial state
152+
data class MyState(
153+
val posts: List<Post> = emptyList(),
154+
val loading: Boolean = false,
155+
val error: 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+
override val stateMachine by HAL(MyState(), viewModelScope) { action, state ->
163+
when (action) {
164+
is NetworkAction.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+
is MyAction.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:
138194

139195
```kotlin
140196
class MyCustomStateObserver<S : HAL.State>(
141-
private val myAwesomeParam: MyAwesomeClass,
142-
override val observer: (S) -> Unit
143-
) : StateObserver<S> {
197+
private val myAwesomeParam: MyAwesomeClass
198+
) : HAL.StateObserver<S> {
144199

145-
override fun transitionTo(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+
override fun observe(receiver: ReceiveChannel<S>) {
201+
// You should consume the channel and handle the incoming states
148202
}
149203
}
150204
```
151205

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

154208
```kotlin
155-
viewModel.observeState(MyCustomStateObserver(myAwesomeParam) { state ->
156-
// Handle state
157-
})
209+
viewModel.observeState(MyCustomStateObserver(myAwesomeParam))
158210
```
159211

160212
## Import to your project
@@ -170,7 +222,7 @@ allprojects {
170222
2. Next, add the desired dependencies to your module:
171223
```gradle
172224
dependencies {
173-
// Core with callback state observer
225+
// Core with Flow state observer
174226
implementation "com.github.adrielcafe.hal:hal-core:$currentVersion"
175227
176228
// LiveData state observer only
Lines changed: 61 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -1,63 +1,61 @@
1-
@file:Suppress("Unused", "MayBeConstant", "MemberVisibilityCanBePrivate")
2-
3-
internal object Version {
4-
const val GRADLE_ANDROID = "3.4.1"
5-
const val GRADLE_DETEKT = "1.0.0-RC15"
6-
const val GRADLE_KTLINT = "8.1.0"
7-
const val GRADLE_JACOCO = "0.15.0"
8-
const val GRADLE_VERSIONS = "0.21.0"
9-
const val GRADLE_MAVEN = "2.1"
10-
11-
const val KOTLIN = "1.3.40"
12-
const val COROUTINES = "1.2.1"
13-
const val LIVEDATA = "2.0.0"
14-
15-
// Sample app only
16-
const val APP_COMPAT = "1.1.0-beta01"
17-
const val LIFECYCLE = "2.2.0-alpha01"
18-
const val VIEWMODEL_KTX = "2.2.0-alpha01"
19-
const val ACTIVITY_KTX = "1.0.0-beta01"
20-
const val FUEL = "2.1.0"
21-
22-
const val TEST_JUNIT = "4.12"
23-
const val TEST_STRIKT = "0.21.1"
24-
const val TEST_MOCKK = "1.9.3"
25-
}
26-
27-
object ProjectLib {
28-
const val ANDROID = "com.android.tools.build:gradle:${Version.GRADLE_ANDROID}"
29-
const val KOTLIN = "org.jetbrains.kotlin:kotlin-gradle-plugin:${Version.KOTLIN}"
30-
const val DETEKT = "io.gitlab.arturbosch.detekt:detekt-gradle-plugin:${Version.GRADLE_DETEKT}"
31-
const val KTLINT = "org.jlleitschuh.gradle:ktlint-gradle:${Version.GRADLE_KTLINT}"
32-
const val JACOCO = "com.vanniktech:gradle-android-junit-jacoco-plugin:${Version.GRADLE_JACOCO}"
33-
const val VERSIONS = "com.github.ben-manes:gradle-versions-plugin:${Version.GRADLE_VERSIONS}"
34-
const val MAVEN = "com.github.dcendents:android-maven-gradle-plugin:${Version.GRADLE_MAVEN}"
35-
36-
val all = setOf(ANDROID, KOTLIN, DETEKT, KTLINT, JACOCO, VERSIONS, MAVEN)
37-
}
38-
39-
object ModuleLib {
40-
const val KOTLIN = "org.jetbrains.kotlin:kotlin-stdlib-jdk8:${Version.KOTLIN}"
41-
const val COROUTINES_CORE = "org.jetbrains.kotlinx:kotlinx-coroutines-core:${Version.COROUTINES}"
42-
const val COROUTINES_ANDROID = "org.jetbrains.kotlinx:kotlinx-coroutines-android:${Version.COROUTINES}"
43-
const val LIVEDATA = "androidx.lifecycle:lifecycle-livedata:${Version.LIVEDATA}"
44-
45-
const val APP_COMPAT = "androidx.appcompat:appcompat:${Version.APP_COMPAT}"
46-
const val LIFECYCLE = "androidx.lifecycle:lifecycle-extensions:${Version.LIFECYCLE}"
47-
const val VIEWMODEL_KTX = "androidx.lifecycle:lifecycle-viewmodel-ktx:${Version.VIEWMODEL_KTX}"
48-
const val ACTIVITY_KTX = "androidx.activity:activity-ktx:${Version.ACTIVITY_KTX}"
49-
const val FUEL_CORE = "com.github.kittinunf.fuel:fuel:${Version.FUEL}"
50-
const val FUEL_COROUTINES = "com.github.kittinunf.fuel:fuel-coroutines:${Version.FUEL}"
51-
52-
val sample = setOf(KOTLIN, COROUTINES_ANDROID, APP_COMPAT, LIFECYCLE, VIEWMODEL_KTX,
53-
ACTIVITY_KTX, FUEL_CORE, FUEL_COROUTINES)
54-
}
55-
56-
object TestLib {
57-
const val JUNIT = "junit:junit:${Version.TEST_JUNIT}"
58-
const val STRIKT = "io.strikt:strikt-core:${Version.TEST_STRIKT}"
59-
const val MOCKK = "io.mockk:mockk:${Version.TEST_MOCKK}"
60-
const val COROUTINES = "org.jetbrains.kotlinx:kotlinx-coroutines-test:${Version.COROUTINES}"
61-
62-
val all = setOf(JUNIT, STRIKT, MOCKK, COROUTINES)
63-
}
1+
@file:Suppress("Unused", "MayBeConstant", "MemberVisibilityCanBePrivate")
2+
3+
internal object Version {
4+
const val GRADLE_ANDROID = "3.6.2"
5+
const val GRADLE_DETEKT = "1.7.4"
6+
const val GRADLE_KTLINT = "9.2.1"
7+
const val GRADLE_JACOCO = "0.16.0"
8+
const val GRADLE_VERSIONS = "0.28.0"
9+
10+
const val KOTLIN = "1.3.72"
11+
const val COROUTINES = "1.3.5"
12+
const val LIFECYCLE = "2.2.0"
13+
14+
// Sample app only
15+
const val KTOR = "1.3.2"
16+
const val SERIALIZATION = "0.20.0"
17+
const val APP_COMPAT = "1.1.0"
18+
const val ACTIVITY = "1.1.0"
19+
20+
const val TEST_JUNIT = "1.1.1"
21+
const val TEST_STRIKT = "0.25.0"
22+
const val TEST_MOCKK = "1.9.3"
23+
}
24+
25+
object ProjectLib {
26+
const val ANDROID = "com.android.tools.build:gradle:${Version.GRADLE_ANDROID}"
27+
const val KOTLIN = "org.jetbrains.kotlin:kotlin-gradle-plugin:${Version.KOTLIN}"
28+
const val SERIALIZATION = "org.jetbrains.kotlin:kotlin-serialization:${Version.KOTLIN}"
29+
const val DETEKT = "io.gitlab.arturbosch.detekt:detekt-gradle-plugin:${Version.GRADLE_DETEKT}"
30+
const val KTLINT = "org.jlleitschuh.gradle:ktlint-gradle:${Version.GRADLE_KTLINT}"
31+
const val JACOCO = "com.vanniktech:gradle-android-junit-jacoco-plugin:${Version.GRADLE_JACOCO}"
32+
const val VERSIONS = "com.github.ben-manes:gradle-versions-plugin:${Version.GRADLE_VERSIONS}"
33+
34+
val all = setOf(ANDROID, KOTLIN, SERIALIZATION, DETEKT, KTLINT, JACOCO, VERSIONS)
35+
}
36+
37+
object ModuleLib {
38+
const val KOTLIN = "org.jetbrains.kotlin:kotlin-stdlib-jdk8:${Version.KOTLIN}"
39+
const val COROUTINES_CORE = "org.jetbrains.kotlinx:kotlinx-coroutines-core:${Version.COROUTINES}"
40+
const val COROUTINES_ANDROID = "org.jetbrains.kotlinx:kotlinx-coroutines-android:${Version.COROUTINES}"
41+
const val LIVEDATA = "androidx.lifecycle:lifecycle-livedata-ktx:${Version.LIFECYCLE}"
42+
43+
// Sample app only
44+
const val KTOR = "io.ktor:ktor-client-android:${Version.KTOR}"
45+
const val KTOR_SERIALIZATION = "io.ktor:ktor-client-serialization-jvm:${Version.KTOR}"
46+
const val SERIALIZATION = "org.jetbrains.kotlinx:kotlinx-serialization-runtime:${Version.SERIALIZATION}"
47+
const val APP_COMPAT = "androidx.appcompat:appcompat:${Version.APP_COMPAT}"
48+
const val ACTIVITY = "androidx.activity:activity-ktx:${Version.ACTIVITY}"
49+
const val VIEWMODEL = "androidx.lifecycle:lifecycle-viewmodel-ktx:${Version.LIFECYCLE}"
50+
51+
val sample = setOf(KOTLIN, COROUTINES_ANDROID, KTOR, KTOR_SERIALIZATION, SERIALIZATION, APP_COMPAT, ACTIVITY, VIEWMODEL)
52+
}
53+
54+
object TestLib {
55+
const val JUNIT = "androidx.test.ext:junit-ktx:${Version.TEST_JUNIT}"
56+
const val STRIKT = "io.strikt:strikt-core:${Version.TEST_STRIKT}"
57+
const val MOCKK = "io.mockk:mockk:${Version.TEST_MOCKK}"
58+
const val COROUTINES = "org.jetbrains.kotlinx:kotlinx-coroutines-test:${Version.COROUTINES}"
59+
60+
val all = setOf(JUNIT, STRIKT, MOCKK, COROUTINES)
61+
}

buildSrc/src/main/kotlin/Maven.kt

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1-
@file:Suppress("Unused", "MayBeConstant", "MemberVisibilityCanBePrivate")
2-
3-
object Maven {
4-
5-
const val GROUP = "com.github.adrielcafe.hal"
6-
}
1+
@file:Suppress("Unused", "MayBeConstant", "MemberVisibilityCanBePrivate")
2+
3+
object Maven {
4+
5+
const val GROUP = "com.github.adrielcafe.hal"
6+
}

0 commit comments

Comments
 (0)