Fluent is a lightweight framework that helps you to create an entirely Android Application following SOLID pattern with a unidirectional flow using reactive concepts or not.
The main concepts in this architecture are: State
, View
, Store
, Job
and Hub
You have to add Fluent maven repository to your project
allprojects {
repositories {
google()
jcenter()
maven { url 'http://dl.bintray.com/fluentio/maven' }
}
}
Then you'll be able to download Fluent dependencies
implementation 'io.fluent:library:0.2.0'
implementation 'io.fluent:rx-library:0.2.0'
The View
class is the entry point of your Fluent flow. Basically, you need to expose all the events (or streams) that the Hub
class needs to connect with some Job
.
You need to create your own specialized View
class for each view of your application.
A regular implementation of your View
:
interface LoginView : View<LoginState> {
fun doLoginClicks(): UserCredentials
}
Note that the View
class is the reference to your screen itself and you should attach your screen State
as well.
Your Activity
, Fragment
or Custom View
needs to implement your View
class:
class LoginActivity : AppCompatActivity(), LoginView {
override fun bind(newState: LoginState) { ... }
}
For convenience, the Fluent framework already provides the
bind()
function. It is just a helper function to reduce the boilerplate.See how to implement with rx and without rx
The State
represents some state of your screen. In order to reduce inconsistency from your View
, the State
is an object who represents the entire state which you view should react.
Note that your view's state should be a data class
, which means that your View
should not have more than one single State
.
It is a good practice that each of your screens has your own State
class.
Fluent already provides the type()
function so you can easily retrieve the type of your view's state.
By default, every State
has a StateType
which represents the type of your view state.
The StateType
is class that you can create the different states of your view.
The Fluent framework already provides four default StateType, which is: Initial
, Loading
, Success
and Error
.
You can easily create your own StateType inheriting the StateType class:
class LoginStateType : StateType() {
object Refreshing : StateType()
class Error(val message: String) : StateType()
}
Responsible for handle all the new States. It is the only way to update the View state.
It receives all the possible states of a View
so you don't need to worry, because we take care about statefulness and performance.
See how to implement with rx and without rx
A job is a little piece of work which might look like an atomic operation. It can be network calls, database operations, third-party libraries or even a view-lifecycle handling.
It's the only place where you can generate new states and push them to the Store
.
See how to implement with rx and without rx
The Hub
is maybe the most important and disruptive layer of this framework.
For each new View
events, your Hub
should connect with some Job
.
It is responsible to connect()
the View
and the Job
's layers.
You can combine and/or filter actions before performing the Job
itself. It acts kind like a bridge binding user actions with the use cases.
See how to implement with rx and without rx
//TODO
Too be filled
//TODO
Too be filled
//TODO
Too be filled
//TODO
Too be filled
Besides Fluent can be used with callbacks, Fluent can be more charming with Reactive Extensions, following the Reactive Manifesto
Stay aware to think in a way to declare all the user's actions and screen's actions related as Observable
.
interface LoginView : View<LoginState> {
fun doLoginClicks(): Observable<Unit>
fun activityResults(): Observable<Pair<Int, Intent>>
}
RxStore
is an implementation ofStore
that works with Reactive Extensions
It requires an initial State
. You can use the stateChanges()
to start receiving new states:
RxStore(SomeState()).stateChanges()
.observeOn(mainThread)
.subscribe { newState -> bind(newState) }
The stream created by it can be handled inside the View
implementation.
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_login)
store.stateChanges()
.observeOn(AndroidSchedulers.mainThread())
.subscribe { bind(it) }
}
override fun bind(newState: LoginState) {
when (newState.type) {
StateType.Success -> success()
StateType.Error -> error()
StateType.Loading -> loading()
}
}
RxJob
is an implementation ofJob
that works with Reactive Extensions
The RxJob have the bind()
function, which returns a Completable
class. Since Fluent is a unidirectional flow, your Job
should complete or not.
You just need to inherit your use case class from RxJob
(specifying the type of the input parameter) and override the bind(input: T):Completable
method with the properly work.
You must handle all the possible edge cases into your job. You should not allow your job to throw an exception back to the
Hub
.
class DoGoogleLoginJob @Inject constructor(
private val store: RxStore<LoginState>,
private val firebase: GoogleFirebaseAuth) : RxJob<Intent>() {
override fun bind(input: Intent): Completable {
return firebase.firebaseAuthWith(intent = input)
.doOnSubscribe { store.update { setType(StateType.Loading) } }
.doOnSuccess { store.update { setType(StateType.Success) } }
.doOnError { store.update { setType(StateType.Error) } }
.toCompletable()
.onErrorComplete()
}
}
In this example we are injecting the local parameters
store
andfirebase
with dagger we strong recommend that.
RxHub
is an implementation ofHub
that works with Reactive Extensions
The RxHub
contains functions to easily connects your View
events source to your RxJob
's.
You connect the View
and the RxJob
's layers through the operator bind(job: RxJob<T>)
(that's why all of your View
's methods needs to return an Observable
).
After that just need to inherit the class RxHub<T>
(specifying the View
it will connect with) and override the connect(view: T)
method with the binds to RxJobs
you need.
We have already implemented the disconnect()
method to release resources by disposing all the bound jobs.
class LoginHub @Inject constructor(
private val doGoogleLoginJob: RxJob<Intent>,
private val requestGoogleLoginJob: RxJob<Unit>) : RxHub<LoginView>() {
override fun connect(view: LoginView) {
view.doLoginClicks()
.bind(requestGoogleLoginJob)
view.activityResults()
.filter { it.first == GoogleLogin.GOOGLE_REQUEST }
.map { it.second }
.bind(doGoogleLoginJob)
}
}
And finally that's how you connect and disconnect your View
to the Hub
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_login)
...
hub.connect(this)
}
override fun onDestroy() {
super.onDestroy()
hub.disconnect()
}
We are looking forward to improving this framework. If you have some feedback, comments, questions or if you want to contribute, join us at our Slack group
Copyright 2018 fluentio Team
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.