Last active
August 4, 2017 09:23
-
-
Save brescia123/4b25ca6d16a3ffd7cd9a35b5f02277e8 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import io.reactivex.Observable | |
import io.reactivex.disposables.CompositeDisposable | |
import io.reactivex.disposables.Disposable | |
import io.reactivex.observers.DisposableObserver | |
import io.reactivex.subjects.BehaviorSubject | |
import io.reactivex.subjects.PublishSubject | |
interface ViewState | |
interface Action | |
typealias Reducer<VS, A> = (VS, A) -> VS | |
interface NavigationAction: Action | |
typealias Navigator<NA, V> = (NA) -> (V) -> Unit | |
/** Interface that defines a View that has a single method to render a ViewState on the UI */ | |
interface MVIView<in VS> : View { | |
fun render(viewState: VS) | |
} | |
interface ViewIntent<T> { | |
fun observe(): Observable<T> | |
fun trigger(value: T) | |
} | |
/** Type alias that defines a function that from a View produces an Observable emitting an ViewIntent */ | |
private typealias ViewIntentBinder<V, T> = (V) -> Observable<T> | |
/** | |
* Type alias that defines a the function that the View (V) will use to render the ViewState (VS). | |
* It returns a Boolean to force its usage in conjunction with when statement and its sealed class | |
* case coverage feature. | |
*/ | |
private typealias ViewStateRender<V, VS> = (V, VS) -> Boolean | |
/** Helper class that maintains a connection between a [ViewIntentBinder] and its [PublishSubject] */ | |
private class SubjectViewIntentBinder<in V : View, T : Any>(val pair: Pair<PublishSubject<T>, ViewIntentBinder<V, T>>) | |
/** | |
* Base presenter to use when implementing an MVI paradigm. It should survive during View lifecycle. | |
* The subclasses should override [bindIntentsToViewState] returning an Observable that should be the merging of | |
* various View's intent Observables produced using [intent]. | |
*/ | |
abstract class MVIPresenter<V : MVIView<VS>, VS : ViewState> : Presenter<V>() { | |
/** The subclasses should override this value to provide a first state to be rendered by the view */ | |
protected abstract val firstState: VS | |
/** Keep track of the current ViewState */ | |
private lateinit var currentState: VS | |
/** Method that should return an Observable producing ViewState */ | |
abstract fun bindIntentsToViewState(): Observable<VS> | |
/** Keep track of the first time the Presenter is attached to the View */ | |
private var firstAttach = true | |
/** BehaviourSubject subscribed to the Observable producing the ViewStates (that comes from | |
* bindIntentsToViewState()) that calls the view render method when a new ViewState is emitted | |
* It remains subscribed to the ViewState Observable and when a View is attached we will subscribe to | |
* it calling the view render onNext | |
*/ | |
private val viewStateSubject: BehaviorSubject<VS> = BehaviorSubject.create<VS>() | |
private var viewStateDisposable: Disposable? = null | |
/** | |
* List of SubjectViewIntentBinder. OnAttach we will subscribe to each of the subjects to continue | |
* to receive intents form the view | |
*/ | |
private val subjectViewIntentBinderList = mutableListOf<SubjectViewIntentBinder<V, *>>() | |
private val intentsCompositeDisposable: CompositeDisposable = CompositeDisposable() | |
private var pendingNavigationEvent: ((V) -> Unit)? = null | |
private val attachIntentSubject = PublishSubject.create<Unit>() | |
private val detachIntentSubject = PublishSubject.create<Unit>() | |
/** | |
* Couple of Observable that offers to subclasses the ability to treat attach and detach events as | |
* intents and mix them with view ones. | |
*/ | |
protected val attachIntent: Observable<Unit> = attachIntentSubject | |
protected val detachIntent: Observable<Unit> = detachIntentSubject | |
override fun onAttach(v: V) { | |
super.onAttach(v) | |
if (firstAttach) { | |
bindViewState(bindIntentsToViewState() | |
.startWith(firstState) | |
.doOnNext { currentState = it }) | |
} | |
// Subscribe to all viewIntent subjects | |
subjectViewIntentBinderList.forEach { intentsCompositeDisposable.add(bind(v, it)) } | |
// Subscribe to viewState subject | |
viewStateDisposable = viewStateSubject.subscribe({ v.render(it) }) | |
firstAttach = false | |
consumePendingNavigationEvent(with = v) | |
attachIntentSubject.onNext(Unit) | |
} | |
override fun onDetach() { | |
super.onDetach() | |
detachIntentSubject.onNext(Unit) | |
intentsCompositeDisposable.clear() | |
viewStateDisposable?.dispose() | |
} | |
protected fun getCurrentState() = currentState | |
/** Register an intent */ | |
protected fun <T : Any> intent(binder: ViewIntentBinder<V, T>): Observable<T> = PublishSubject.create<T>() | |
.apply { subjectViewIntentBinderList.add(SubjectViewIntentBinder<V, T>(this to binder)) } | |
/** Register a navigation callback and ask the View to consume it */ | |
protected fun navigate(callback: (V) -> Unit) { | |
val view = view() | |
if (view != null) callback(view) else pendingNavigationEvent = callback | |
} | |
/** Use a [Navigator] to generate a navigation callback and register it */ | |
inline protected fun <A : Action, reified NA: NavigationAction> A.navigateWith(navigator: Navigator<NA, V>) { | |
val navigationCallback = if (this is NA) { | |
navigator(this) | |
} else return | |
navigate(navigationCallback) | |
} | |
/** Subscribe a PublishSubject to the Observable emitting view Intents */ | |
private fun <T : Any> bind(view: V, subjectViewIntentBinder: SubjectViewIntentBinder<V, T>): Disposable { | |
val (subject, binder) = subjectViewIntentBinder.pair | |
return binder(view).subscribeWith(object : DisposableObserver<T>() { | |
override fun onComplete() = subject.onComplete() | |
override fun onError(e: Throwable) { | |
throw IllegalStateException("The flow of Intents should never end with an error", e) | |
} | |
override fun onNext(t: T) = subject.onNext(t) | |
}) | |
} | |
/** Subscribe the viewStateSubject to the Observable emitting ViewState (producer) */ | |
private fun bindViewState(producer: Observable<VS>) { | |
producer.subscribeWith(object : DisposableObserver<VS>() { | |
override fun onComplete() {} | |
override fun onError(e: Throwable) { | |
throw IllegalStateException("The flow of ViewStates should never end with an error", e) | |
} | |
override fun onNext(t: VS) = viewStateSubject.onNext(t) | |
}) | |
} | |
/** If there is a pending navigation event consumes it and set it to null */ | |
private fun consumePendingNavigationEvent(with: V) { | |
pendingNavigationEvent?.invoke(with) | |
pendingNavigationEvent = null | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment