Reaktor is a framework for a reactive and unidirectional Kotlin application architecture.
It is a Kotlin port of the ReaktorKit Swift concept.
allprojects {
repositories {
jcenter()
}
}
dependencies {
/**
* Core. Kotlin only dependency.
*/
implementation "at.florianschuster.reaktor:reaktor-core:$version"
/**
* Android (AAC) Extensions for Reactor. See: Reaktor-Android
*/
implementation "at.florianschuster.reaktor:reaktor-android:$version"
/**
* Android (AAC) Koin Extensions for Reactor. See: Reaktor-Android-Koin
*/
implementation "at.florianschuster.reaktor:reaktor-android-koin:$version"
}
- Kotlin
- RxJava2
- MVI Architecture Pattern
As seen in the previous graphic, there are two actors that interact with each other: the view and the reactor.
A view displays data. In Android terms, a view could be an Activity
, a Fragment
, a ViewGroup
or a View
. A view should not have any business related logic but rather delegate all user actions to the reactor and render its state.
A view must implement the ReactorView
interface:
class ExampleFragment : Fragment(), ReactorView<ExampleReactor> {
override val reactor = ExampleReactor()
override val disposables = CompositeDisposable()
override fun bind(reactor: ExampleReactor) {
// bind actions (view --> reactor)
buttonSetValue.clicks()
.map { ExampleReactor.Action.SetValue(2) }
.bind(to = reactor.action)
.let(disposables::add)
// render state (reactor --> view)
reactor.state.changesFrom { it.value }
.map { "$it" }
.bind(to = textViewValue::setText)
.let(disposables::add)
}
}
A reactor is an UI-independent class that manages the state of the view and handles business logic or redirects to services that handle business logic. The reactor has no dependency on the view, which makes it easily testable.
A Reactor must implement the Reactor
interface:
class ExampleReactor() : Reactor<ExampleReactor.Action, ExampleReactor.Mutation, ExampleReactor.State> {
sealed class Action {
data class SetValue(val value: Int) : Action()
}
sealed class Mutation {
data class SetMutatedValue(val value: Int) : Mutation()
}
data class State(
val value: Int
)
override val initialState = State(value = 0)
}
fun mutate(Action)
receives an Action and returns anObservable<Mutation>
. All asynchronous side effects are executed herefun reduce(State, Mutation)
takes the previous State, receives a Mutation and returns the newly generated State synchronouslyfun transform(Mutation)
can be used to transform a global state, such as for example a user session into a Mutation
A view can only emit actions and a reactor can only emit states, thus unidirectional observable stream paradigm is abided.
For more in depth info, you should hit up the ReactorKit Repo Readme. It is very extensive and since Swift 4 and Kotlin are much alike you will feel right at home! They also have nice graphics.
The DefaultReactor
is a default implementation for a reactor that handles creation for all variables but not the clearing of the CompositeDisposable
. Do not forget to clear the CompositeDisposable
in the reactor after you are done with it.
The DefaultReactor
also catches and ignores all errors emitted in fun mutate()
and fun reduce()
to keep the state
stream going. You can change this behavior (e.g. for debug builds) and also attach an error handler with Reaktor.attachErrorHandler(...)
.
When binding the reactor to an Activity
or a Fragment
, their life cycles have to be taken into account.
All Android views have to be laid out before the bind happens, so you should not call fun bind(Reactor)
before:
- Activity: after
setContentView(Int)
infun onCreate(Bundle)
- Fragment:
fun onViewCreated(View, Bundle)
Also do not forget to dispose the view's CompositeDisposable
. I propose to do this in:
- Activity:
fun onDestroy()
- Fragment:
fun onDestroyView()
The ViewModelReactor
is a default implementation for a reactor that uses the Android Architecture ViewModel and thus handles the clearing of the CompositeDisposable
in fun onCleared()
.
When binding a state
stream to a view, the fun bind(...)
extension function can come in handy since it observes on the AndroidSchedulers.mainThread()
and also logs errors.
This module also contains an extension function that exposes the reactor state
as a LiveData<State>
object:
reactor.liveDataState().observe(this, Observer { state ->
...
}
Koin is a lightweight dependency injection framework for Kotlin.
The androidkoin
module contains simple extension functions to inject a ViewModelReactor
. They are just renamed Koin extension functions that can be used for more clarity when developing with the framework.
- Counter: Most Basic Counter Example. It uses
ViewModelReactor
for an Activity. - Github Search: Github Repository Search. It uses
ViewModelReactor
for a Fragment. - Saved State Example: Like the Counter. Basic but it uses
onSaveInstanceState
to preserve theReactor
's state on process death. - Watchables: A Movie and TV Show Watchlist Application. It uses Koin as DI Framework to inject dependencies into a
Reactor
.
Visit my Website.
- Update Versions in
build.gradle
,info_strings.xml
&README.md
. - Run
fastlane deploy
. - Tag, commit & push.
Copyright 2019 Florian Schuster.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.