Skip to content

Instantly share code, notes, and snippets.

@brescia123
Last active August 4, 2017 09:23
Show Gist options
  • Save brescia123/4b25ca6d16a3ffd7cd9a35b5f02277e8 to your computer and use it in GitHub Desktop.
Save brescia123/4b25ca6d16a3ffd7cd9a35b5f02277e8 to your computer and use it in GitHub Desktop.
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