Skip to content

Commit 373b33e

Browse files
New Object Interface (#1)
1 parent 2244567 commit 373b33e

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

47 files changed

+2186
-1205
lines changed

.editorconfig

+9
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
[*.{kt,kts}]
2+
# possible values: number (e.g. 2), "unset" (makes ktlint ignore indentation completely)
3+
indent_size=4
4+
# true (recommended) / false
5+
insert_final_newline=true
6+
# possible values: number (e.g. 120) (package name, imports & comments are ignored), "off"
7+
# it's automatically set to 100 on `ktlint --android ...` (per Android Kotlin Style Guide)
8+
max_line_length=off
9+
disabled_rules=import-ordering

README.md

+46-61
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
# Kelm
22

3-
Kelm eases the pain when dealing with complex app state and asynchronous tasks.
3+
Kelm simplifies management of complex app states and asynchronous tasks.
4+
45
Kelm is a Kotlin library based on the [Elm Architecture](https://guide.elm-lang.org/architecture) and [RxJava](http://reactivex.io/).
56

67
## Introduction
@@ -17,31 +18,39 @@ The **Model** can only be updated in the **Update** function. **Messages** are t
1718

1819
**Messages** can be UI events, responses from APIs, or the result of computations.
1920

20-
A good strategy when designing the initial version of a **Model** and **Messages** is to write down every event the UI can generate, and everything it can render.
21-
2221
### A simple example
2322

2423
Below is a simple Kelm app. Try to read and guess what it does:
2524

2625
```kotlin
27-
data class Model(val count: Int)
26+
object CounterElement : Kelm.Sandbox<Model, Msg>() {
27+
sealed class Msg {
28+
object MinusClick : Msg()
29+
object PlusClick : Msg()
30+
object ResetClick : Msg()
31+
}
2832

29-
sealed class Msg {
30-
object Increment : Msg()
31-
object Decrement : Msg()
33+
data class Model(val count: Int) {
34+
val resetBtEnabled = count > 0
35+
val minusBtEnabled = resetBtEnabled
36+
}
37+
38+
fun initModel() = Model(count = 0)
39+
40+
override fun updateSimple(model: Model, msg: Msg): Model? =
41+
when (msg) {
42+
is Msg.MinusClick -> model.copy(count = model.count - 1)
43+
is Msg.PlusClick -> model.copy(count = model.count + 1)
44+
is Msg.ResetClick -> model.copy(count = 0)
45+
}
3246
}
3347

3448
val msgSubj = PublishSubject.create<Msg>()
3549

36-
Kelm.build<Model, Msg, Nothing>(
37-
msgObserver = msgSubj,
38-
initModel = Model(0)
39-
) { model, msg ->
40-
when (msg) {
41-
is Increment -> Model(model.count + 1)
42-
is Decrement -> Model(model.count - 1)
43-
}
44-
}.subscribe { model ->
50+
CounterElement.start(
51+
initModel = CounterElement.initModel(),
52+
msgInput = msgSubj
53+
).subscribe { model ->
4554
println("The total count is ${model.count}")
4655
}
4756

@@ -51,13 +60,17 @@ msgSubj.onNext(Msg.Decrement)
5160
```
5261

5362
The above example shows how the main flow of a Kelm app works.
54-
We first declare our **Model** and our **Messages**, we then build the main **Update** function.
63+
We first declare a `Kelm.Sandbox` object†, the contract for our **Model** and our **Messages**,
64+
and how they interact with the **update** function.
65+
66+
† Your **Sandbox** implementation should be an **object** with no properties.
5567

5668
The **Update** is a *pure function* that takes the current **Model** and a **Message** and returns a new **Model**.
5769

58-
The return of the `Kelm::build` is of type `Observable<Model>`. The **View** (a Android View, for example) can subscribe to this `Observable` and render it when it changes.
70+
The return of the `Element::start` is of type `Observable<Model>`.
71+
The **View** (an Android View, for example) can subscribe to this `Observable` and render it when it changes.
5972

60-
See the [Counter Sample](sample-andorid/src/main/java/kelm/sample/CounterSampleActivity.kt) for a working implementation.
73+
See the [Counter Sample](sample-android/src/main/java/kelm/sample/CounterSampleActivity.kt).
6174

6275
#### Rules for the **Update** function:
6376

@@ -68,65 +81,37 @@ See the [Counter Sample](sample-andorid/src/main/java/kelm/sample/CounterSampleA
6881

6982
### Commands
7083

71-
**Commands** are asynchronous tasks that finish with *one* **Message**.
84+
**Commands** are asynchronous tasks that finish with *at most one* **Message**.
7285
This **Message** indicates the result of a task, be it a successful result
7386
or an error.
7487

75-
Here's an example using commands to fetch data from an API:
88+
* All side-effects and expensive computations must be done with **Commands**.
7689

77-
```kotlin
78-
fun fetchFromApi(): Single<Response> = ...
90+
To work with **Commands** implement an ``Kelm::Element`` instead of a ``Kelm::Sandbox``.
7991

80-
sealed class Model {
81-
object Loading : Model()
82-
data class LoadedContent(val response: Response) : Model()
83-
}
92+
Then create a `cmdToMaybe` function that takes a **Command** and transforms it into a `Maybe<Msg>`.
93+
This `Maybe<Msg>` is the action of a **Command** and *it should never emit any error in its error channel*.
8494

85-
sealed class Msg {
86-
object FetchClick : Msg()
87-
data class ContentFetched(val response: Response) : Msg()
88-
}
95+
The **Update** function has some special implicit functions like the `runCmd` function. `runCmd` adds the **Command** to be executed.
8996

90-
sealed class Cmd : kelm.Cmd() {
91-
object FetchFromApi : Cmd()
92-
}
97+
See the [Fox Service Sample](sample-android/src/main/java/kelm/sample/FoxServiceSampleActivity.kt).
9398

94-
val msgSubj = PublishSubject.create<Msg>()
99+
### Subscriptions
95100

96-
Kelm.build<Model, Msg, Cmd>(
97-
msgObserver = msgSubj,
98-
initModel = Model.Loading,
99-
cmdToSingle = { cmd ->
100-
when (cmd) {
101-
is Cmd.FetchFromApi ->
102-
fetchFromApi().map { Msg.ContentFetched(it) }
103-
}
104-
},
105-
update = { model, msg ->
106-
when (msg) {
107-
is FetchClick -> Model.Loading.also {
108-
runCmd(Cmd.FetchFromApi)
109-
}
110-
is ContentFetched -> Model.LoadedContent(msg.response)
111-
}
112-
}
113-
)
101+
```
102+
TODO
114103
```
115104

116-
To work with **Commands** we first declare their type.
117-
Then we create a `cmdToSingle` function that takes a **Command** and transforms it into a `Single<Msg>`.
118-
This `Single<Msg>` is the action of a **Command** and *it should never emit any error in its error channel*.
119-
120-
The **Update** function has some special implicit functions like the `runCmd` function. `runCmd` adds the **Command** to be executed.
121-
122-
See the [Fox Service Sample](sample-andorid/src/main/java/kelm/sample/FoxServiceSampleActivity.kt) for a working implementation.
105+
See the [Clock Sample](sample-android/src/main/java/kelm/sample/ClockSampleActivity.kt).
123106

124-
### Subscriptions
107+
### Dealing with complex projects
125108

126109
```
127110
TODO
128111
```
129112

113+
See the [Advanced Sample](sample-android/src/main/java/kelm/sample/signUp).
114+
130115
### FAQ
131116

132117
```

build.gradle

+7-8
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
// Top-level build file where you can add configuration options common to all sub-projects/modules.
22

33
buildscript {
4-
ext.kotlin_version = '1.3.50'
54
repositories {
65
google()
76
jcenter()
@@ -10,18 +9,18 @@ buildscript {
109
}
1110
}
1211
dependencies {
13-
classpath 'com.android.tools.build:gradle:3.5.0'
14-
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
15-
classpath 'org.jlleitschuh.gradle:ktlint-gradle:8.0.0'
16-
// NOTE: Do not place your application dependencies here; they belong
17-
// in the individual module build.gradle files
12+
classpath 'com.android.tools.build:gradle:3.5.1'
13+
classpath Dep.plugins.kotlin
14+
classpath Dep.plugins.ktlint
15+
classpath Dep.plugins.gradleVersions
1816
}
1917
}
2018

2119
apply plugin: "org.jlleitschuh.gradle.ktlint"
20+
apply plugin: "com.github.ben-manes.versions"
2221

2322
subprojects {
24-
apply plugin: "org.jlleitschuh.gradle.ktlint" // Version should be inherited from parent
23+
apply plugin: "org.jlleitschuh.gradle.ktlint"
2524

2625
ktlint {
2726
}
@@ -36,4 +35,4 @@ allprojects {
3635

3736
task clean(type: Delete) {
3837
delete rootProject.buildDir
39-
}
38+
}

buildSrc/build.gradle.kts

+7
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
plugins {
2+
`kotlin-dsl`
3+
}
4+
5+
repositories {
6+
jcenter()
7+
}

buildSrc/settings.gradle.kts

Whitespace-only changes.
+41
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
private object Versions {
2+
const val kotlin = "1.3.50"
3+
const val spek = "2.0.8"
4+
}
5+
6+
object Dep {
7+
val plugins = Plugins
8+
val kotlin = Kotlin
9+
val android = Android
10+
11+
object Plugins {
12+
const val kotlin = "org.jetbrains.kotlin:kotlin-gradle-plugin:${Versions.kotlin}"
13+
const val ktlint = "org.jlleitschuh.gradle:ktlint-gradle:9.0.0"
14+
const val gradleVersions = "com.github.ben-manes:gradle-versions-plugin:0.27.0"
15+
}
16+
17+
object Kotlin {
18+
private val v = Versions
19+
20+
const val std = "org.jetbrains.kotlin:kotlin-stdlib:${v.kotlin}"
21+
const val kotlinTest = "org.jetbrains.kotlin:kotlin-test:${v.kotlin}"
22+
const val reflect = "org.jetbrains.kotlin:kotlin-reflect:${v.kotlin}"
23+
const val kotlinAssertions = "io.kotlintest:kotlintest-assertions:3.4.2"
24+
const val junit = "junit:junit:4.12"
25+
const val rxJava = "io.reactivex.rxjava2:rxjava:2.2.13"
26+
const val spekJvm = "org.spekframework.spek2:spek-dsl-jvm:${v.spek}"
27+
const val spekRuntime = "org.spekframework.spek2:spek-runner-junit5:${v.spek}"
28+
}
29+
30+
object Android {
31+
private val v = Versions
32+
33+
const val appCompat = "androidx.appcompat:appcompat:1.1.0"
34+
const val coreKtx = "androidx.core:core-ktx:1.2.0-beta01"
35+
const val lifecycleExt = "androidx.lifecycle:lifecycle-extensions:2.2.0-rc01"
36+
const val rxAndroid = "io.reactivex.rxjava2:rxandroid:2.1.1"
37+
const val constraintLayout = "androidx.constraintlayout:constraintlayout:2.0.0-beta3"
38+
const val picasso = "com.squareup.picasso:picasso:2.71828"
39+
const val loadingButton = "br.com.simplepass:loading-button-android:2.1.5"
40+
}
41+
}

docs/kelm_adv_sample_elements.png

9.28 KB
Loading

docs/kelm_adv_sample_flow.png

58.4 KB
Loading

kelm-core/build.gradle

+10-12
Original file line numberDiff line numberDiff line change
@@ -7,19 +7,15 @@ repositories {
77
}
88

99
dependencies {
10-
def spek_version = "2.0.5"
11-
implementation fileTree(dir: 'libs', include: ['*.jar'])
12-
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
13-
implementation "io.reactivex.rxjava2:rxjava:2.2.8"
10+
implementation Dep.kotlin.std
11+
implementation Dep.kotlin.rxJava
1412

15-
testImplementation 'junit:junit:4.12'
16-
testImplementation "org.jetbrains.kotlin:kotlin-test:$kotlin_version"
17-
testImplementation "io.kotlintest:kotlintest-assertions:3.4.0"
18-
19-
//spek2
20-
testImplementation "org.spekframework.spek2:spek-dsl-jvm:$spek_version"
21-
testRuntimeOnly "org.spekframework.spek2:spek-runner-junit5:$spek_version"
22-
testImplementation "org.jetbrains.kotlin:kotlin-reflect:$kotlin_version"
13+
testImplementation Dep.kotlin.junit
14+
testImplementation Dep.kotlin.kotlinTest
15+
testImplementation Dep.kotlin.kotlinAssertions
16+
testImplementation Dep.kotlin.spekJvm
17+
testRuntimeOnly Dep.kotlin.spekRuntime
18+
testImplementation Dep.kotlin.reflect
2319
}
2420

2521
test {
@@ -30,6 +26,8 @@ test {
3026
testLogging {
3127
events("passed", "skipped", "failed")
3228
}
29+
30+
systemProperty 'SPEK_TIMEOUT', 0
3331
}
3432

3533
group = 'com.github.AllanHasegawa.kelm'
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
package kelm
2+
3+
class CmdFactoryNotImplementedException :
4+
RuntimeException("Cmd factory not implemented")
5+
6+
class SubFactoryNotImplementedException :
7+
RuntimeException("Subscription factory not implemented")
8+
9+
sealed class ExternalException(message: String, cause: Throwable) :
10+
RuntimeException(message, cause)
11+
12+
data class SubscriptionException(val subscription: Any, override val cause: Throwable) :
13+
ExternalException("The subscription [$subscription] threw an error", cause)
14+
15+
data class CmdException(val cmd: Any, override val cause: Throwable) :
16+
ExternalException("The command [$cmd] threw an error", cause)

0 commit comments

Comments
 (0)