Skip to content
This repository has been archived by the owner on Dec 30, 2020. It is now read-only.

Commit

Permalink
Dsl improvements (#19)
Browse files Browse the repository at this point in the history
  • Loading branch information
Mikolaj Leszczynski authored and Matthew Dolan committed Oct 23, 2019
1 parent d998588 commit 0a181c2
Show file tree
Hide file tree
Showing 22 changed files with 473 additions and 369 deletions.
2 changes: 2 additions & 0 deletions .idea/codeStyles/Project.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

45 changes: 40 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,10 @@ that RxJava gives you out of the box.
We drew inspiration from [Managing State with RxJava by Jake Wharton](https://www.reddit.com/r/androiddev/comments/656ter/managing_state_with_rxjava_by_jake_wharton/),
[RxFeedback](https://github.com/NoTests/RxFeedback.kt) and [Mosby MVI](https://github.com/sockeqwe/mosby).

For more details about MVI and our implementation, read our [overview](docs/overview.md).
For more details about MVI and our implementation, please read

1. [MVI overview](docs/overview.md).
1. [Creating flows in Orbit](docs/orbits.md).

## Getting started

Expand All @@ -41,16 +44,48 @@ data class State(val total: Int = 0)

data class AddAction(val number: Int)

class CalculatorMiddleware: Middleware<State, Unit> by middleware(State(), {
sealed class SideEffect {
data class Toast(val text: String) : SideEffect()
}

class CalculatorViewModel: OrbitViewModel<State, SideEffect> (State(), {

perform("addition")
.on<AddAction>()
.withReducer { state, action ->
state.copy(state.total + action.number)
}
.postSideEffect { SideEffect.Toast("Adding ${action.number}") }
.withReducer { state.copy(currentState.total + event.number) }

...
})
```

And then in your activity / fragment

``` kotlin
private val actions by lazy {
Observable.merge(
listOf(
addButton.clicks().map { AddAction }
...
)
)
}

override fun onStart() {
viewModel.connect(this, actions, ::handleState, ::handleSideEffect)
}

private fun handleState(state: State) {
...
}

private fun handleSideEffect(sideEffect: SideEffect) {
when(sideEffect) {
is SideEffect.Toast -> toast(sideEffect.text)
}
}
```

Read more about what makes an [orbit](docs/orbits.md).

## Contributing
Expand Down
110 changes: 79 additions & 31 deletions docs/orbits.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,7 @@ the glue between small, distinct functions.
perform("add random number")
.on<AddRandomNumberButtonPressed>()
.transform { this.compose(getRandomNumberUseCase) }
.withReducer { state, useCaseEvent ->
state.copy(state.total + useCaseEvent.number)
}
.withReducer { state.copy(currentState.total + event.number) }
```

We can break an orbit into its constituent parts to be able to understand it
Expand Down Expand Up @@ -38,7 +36,7 @@ actions.
## Transformers

``` kotlin
.transform { this.compose(getRandomNumberUseCase) }
.transform { this.compose(getRandomNumberUseCase) }
```

Next we apply transformations to the action observable within the lambda here.
Expand All @@ -56,9 +54,9 @@ observable. There are three possible reactions:
### Reducers

``` kotlin
.withReducer { state, useCaseEvent ->
state.copy(state.total + useCaseEvent.number)
}
.withReducer {
state.copy(currentState.total + event.number)
}
```

We can apply the `withReducer` function to a transformed observable in order to
Expand All @@ -68,24 +66,18 @@ Reducers can also be applied directly to an action observable, without any
transformations beforehand:

``` kotlin
orbits {
perform("addition")
.on<AddAction>()
.withReducer { state, action ->
state.copy(state.total + action.number)
}
}
perform("addition")
.on<AddAction>()
.withReducer { state.copy(currentState.total + event.number) }
```

### Ignored events

``` kotlin
orbits {
perform("add random number")
.on<AddRandomNumberButtonPressed>()
.transform { this.doOnNext(…) }
.ignoringEvents()
}
perform("add random number")
.on<AddRandomNumberButtonPressed>()
.transform { this.map{ … } }
.ignoringEvents()
```

If we have an orbit that mainly invokes side effects in response to an action
Expand All @@ -94,20 +86,76 @@ and does not need to produce a new state, we can ignore it.
### Loopbacks

``` kotlin
orbits {
perform("add random number")
.on<AddAction>()
.transform { this.compose(getRandomNumberUseCase) }
.loopBack { it }
perform("add random number")
.on<AddAction>()
.transform { this.compose(getRandomNumberUseCase) }
.loopBack { event }

perform("reduce add random number")
.on<GetRandomNumberUseCaseEvent>()
.withReducer { state, event ->
state.copy(state.total + event.number)
}
}
perform("reduce add random number")
.on<GetRandomNumberUseCaseEvent>()
.withReducer { state.copy(currentState.total + event.number) }
```

Loopbacks allow you to create feedback loops where events coming from one orbit
can create new actions that feed into the system. These are useful to represent
a cascade of events.

### Side effects

We cannot run away from the fact that working with Android will
inherently have some side effects. We've made side effects a first class
citizen in Orbit as we believe that it's better to have a full, clear
view of what side effects are possible in a particular view model.

This functionality is commonly used for things like truly one-off events,
navigation, logging, analytics etc.

It comes in two flavors:

1. `sideEffect` lets us perform side effects that are not intended for
consumption outside the Orbit container.
1. `postSideEffect` sends the value returned from the closure to a relay
that can be subscribed when connecting to the view model. Use this for
view-related side effects like Toasts, Navigation, etc.

``` kotlin
sealed class SideEffect {
data class Toast(val text: String) : SideEffect()
data class Navigate(val screen: Screen) : SideEffect()
}

OrbitViewModel<State, SideEffect>(State(), {

perform("side effect straight on the incoming action")
.on<SomeAction>()
.sideEffect { state, event ->
Timber.log(inputState)
Timber.log(action)
}
.ignoringEvents()

perform("side effect after transformation")
.on<OtherAction>()
.transform { this.compose(getRandomNumberUseCase) }
.sideEffect { Timber.log(event) }
.ignoringEvents()

perform("add random number")
.on<YetAnotherAction>()
.transform { this.compose(getRandomNumberUseCase) }
.postSideEffect { SideEffect.Toast(event.toString()) }
.postSideEffect { SideEffect.Navigate(Screen.Home) }
.ignoringEvents()

perform("post side effect straight on the incoming action")
.on<NthAction>()
.postSideEffect { SideEffect.Toast(inputState.toString()) }
.postSideEffect { SideEffect.Toast(action.toString()) }
.postSideEffect { SideEffect.Navigate(Screen.Home) }
.ignoringEvents()
})
```

The `OrbitContainer` hosted in the `OrbitViewModel` provides a relay that
you can subscribe through the `connect` method on `OrbitViewModel` in order
to receive view-related side effects.
7 changes: 7 additions & 0 deletions docs/overview.md
Original file line number Diff line number Diff line change
Expand Up @@ -80,3 +80,10 @@ A `OrbitViewModel` uses an `AndroidOrbitContainer` internally, which modifies
the output of the default one to listen in on the UI thread. This means that
from the UI perspective you do not need to worry about which thread to send or
receive events from.

### Error handling

It is good practice to handle your errors at the transformer level. Currently
Orbit has no error handling in place which means that any error will break the
Orbit cycle and result in a defunct Orbit container. We are looking into
ways to improve this.
Original file line number Diff line number Diff line change
Expand Up @@ -19,17 +19,17 @@ package com.babylon.orbit
import io.reactivex.Observable
import io.reactivex.android.schedulers.AndroidSchedulers

class AndroidOrbitContainer<STATE : Any, EVENT : Any> private constructor(
private val delegate: BaseOrbitContainer<STATE, EVENT>
) : OrbitContainer<STATE, EVENT> by delegate {
class AndroidOrbitContainer<STATE : Any, SIDE_EFFECT : Any> private constructor(
private val delegate: BaseOrbitContainer<STATE, SIDE_EFFECT>
) : OrbitContainer<STATE, SIDE_EFFECT> by delegate {

constructor(middleware: Middleware<STATE, EVENT>) : this(BaseOrbitContainer(middleware))
constructor(middleware: Middleware<STATE, SIDE_EFFECT>) : this(BaseOrbitContainer(middleware))

val state: STATE
get() = delegate.state.blockingGet()

override val orbit: Observable<STATE> = delegate.orbit.observeOn(AndroidSchedulers.mainThread())

override val sideEffect: Observable<EVENT> =
override val sideEffect: Observable<SIDE_EFFECT> =
delegate.sideEffect.observeOn(AndroidSchedulers.mainThread())
}
29 changes: 17 additions & 12 deletions orbit-android/src/main/java/com/babylon/orbit/OrbitViewModel.kt
Original file line number Diff line number Diff line change
Expand Up @@ -16,16 +16,21 @@

package com.babylon.orbit

import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.ViewModel
import com.uber.autodispose.android.lifecycle.AndroidLifecycleScopeProvider
import com.uber.autodispose.autoDispose
import com.uber.autodispose.android.lifecycle.autoDispose
import io.reactivex.Observable

abstract class OrbitViewModel<STATE : Any, EVENT : Any>(
middleware: Middleware<STATE, EVENT>
abstract class OrbitViewModel<STATE : Any, SIDE_EFFECT : Any>(
middleware: Middleware<STATE, SIDE_EFFECT>
) : ViewModel() {

private val container: AndroidOrbitContainer<STATE, EVENT> = AndroidOrbitContainer(middleware)
constructor(
initialState: STATE,
init: OrbitsBuilder<STATE, SIDE_EFFECT>.() -> Unit
) : this(middleware(initialState, init))

private val container: AndroidOrbitContainer<STATE, SIDE_EFFECT> = AndroidOrbitContainer(middleware)

val state: STATE
get() = container.state
Expand All @@ -37,22 +42,22 @@ abstract class OrbitViewModel<STATE : Any, EVENT : Any>(
* For example onStart -> onStop, onResume -> onPause, onCreate -> onDestroy.
*/
fun connect(
scoper: AndroidLifecycleScopeProvider,
lifecycleOwner: LifecycleOwner,
actions: Observable<out Any>,
stateConsumer: (STATE) -> Unit,
eventConsumer: (EVENT) -> Unit = {}
sideEffectConsumer: (SIDE_EFFECT) -> Unit = {}
) {

container.orbit
.autoDispose(scoper)
.autoDispose(lifecycleOwner)
.subscribe(stateConsumer)

actions.autoDispose(scoper)
.subscribe(container.inputRelay::accept)
actions.autoDispose(lifecycleOwner)
.subscribe(container.inputRelay::onNext)

container.sideEffect
.autoDispose(scoper)
.subscribe(eventConsumer)
.autoDispose(lifecycleOwner)
.subscribe(sideEffectConsumer)
}

override fun onCleared() {
Expand Down
4 changes: 2 additions & 2 deletions orbit/src/main/java/com/babylon/orbit/ActionState.kt
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,10 @@

package com.babylon.orbit

import com.jakewharton.rxrelay2.PublishRelay
import io.reactivex.Observable
import io.reactivex.Scheduler
import io.reactivex.schedulers.Schedulers
import io.reactivex.subjects.PublishSubject
import java.util.concurrent.Executors

data class ActionState<out STATE : Any, out ACTION : Any>(
Expand All @@ -29,7 +29,7 @@ data class ActionState<out STATE : Any, out ACTION : Any>(

fun <STATE : Any, EVENT : Any> Observable<ActionState<STATE, *>>.buildOrbit(
middleware: Middleware<STATE, EVENT>,
inputRelay: PublishRelay<Any>
inputRelay: PublishSubject<Any>
): Observable<STATE> {
val scheduler = createSingleScheduler()
return this
Expand Down
12 changes: 6 additions & 6 deletions orbit/src/main/java/com/babylon/orbit/BaseOrbitContainer.kt
Original file line number Diff line number Diff line change
Expand Up @@ -16,22 +16,22 @@

package com.babylon.orbit

import com.jakewharton.rxrelay2.PublishRelay
import io.reactivex.Observable
import io.reactivex.Single
import io.reactivex.disposables.CompositeDisposable
import io.reactivex.observables.ConnectableObservable
import io.reactivex.rxkotlin.plusAssign
import io.reactivex.subjects.PublishSubject

class BaseOrbitContainer<STATE : Any, EVENT : Any>(
middleware: Middleware<STATE, EVENT>
) : OrbitContainer<STATE, EVENT> {
class BaseOrbitContainer<STATE : Any, SIDE_EFFECT : Any>(
middleware: Middleware<STATE, SIDE_EFFECT>
) : OrbitContainer<STATE, SIDE_EFFECT> {
var state: Single<STATE>
private set

override val inputRelay: PublishRelay<Any> = PublishRelay.create()
override val inputRelay: PublishSubject<Any> = PublishSubject.create()
override val orbit: ConnectableObservable<STATE>
override val sideEffect: Observable<EVENT> = middleware.sideEffect
override val sideEffect: Observable<SIDE_EFFECT> = middleware.sideEffect

private val disposables = CompositeDisposable()

Expand Down
Loading

0 comments on commit 0a181c2

Please sign in to comment.