Permalink
Join GitHub today
GitHub is home to over 28 million developers working together to host and review code, manage projects, and build software together.
Sign up| package com.airbnb.mvrx | |
| import android.arch.lifecycle.LifecycleOwner | |
| import android.arch.lifecycle.ViewModel | |
| import android.support.annotation.CallSuper | |
| import android.support.annotation.RestrictTo | |
| import android.util.Log | |
| import io.reactivex.Observable | |
| import io.reactivex.Single | |
| import io.reactivex.android.schedulers.AndroidSchedulers | |
| import io.reactivex.disposables.CompositeDisposable | |
| import io.reactivex.disposables.Disposable | |
| import io.reactivex.functions.Consumer | |
| import io.reactivex.schedulers.Schedulers | |
| import kotlin.reflect.KProperty1 | |
| import kotlin.reflect.KVisibility | |
| /** | |
| * To use MvRx, create your own base MvRxViewModel that extends this one and sets debugMode. | |
| * | |
| * All subsequent ViewModels in your app should use that one. | |
| */ | |
| abstract class BaseMvRxViewModel<S : MvRxState>( | |
| initialState: S, | |
| private val debugMode: Boolean = false, | |
| private val stateStore: MvRxStateStore<S> = RealMvRxStateStore(initialState) | |
| ) : ViewModel() { | |
| private val tag by lazy { javaClass.simpleName } | |
| private val disposables = CompositeDisposable() | |
| private lateinit var mutableStateChecker: MutableStateChecker<S> | |
| init { | |
| if (debugMode) { | |
| mutableStateChecker = MutableStateChecker(initialState) | |
| Observable.fromCallable { validateState(initialState) } | |
| .subscribeOn(Schedulers.computation()).subscribe() | |
| } | |
| } | |
| internal val state: S | |
| get() = stateStore.state | |
| /** | |
| * Override this to provide the initial state. | |
| */ | |
| @CallSuper | |
| override fun onCleared() { | |
| super.onCleared() | |
| disposables.dispose() | |
| } | |
| /** | |
| * Call this to mutate the current state. | |
| * A few important notes about the state reducer. | |
| * 1) It will not be called synchronously or on the same thread. This is for performance and accuracy reasons. | |
| * 2) Similar to the execute lambda above, the current state is the state receiver so the `count` in `count + 1` is actually the count | |
| * property of the state at the time that the lambda is called | |
| * 3) In development, MvRx will do checks to make sure that your setState is pure by calling in multiple times. As a result, DO NOT use | |
| * mutable variables or properties from outside the lambda or else it may crash. | |
| */ | |
| protected fun setState(reducer: S.() -> S) { | |
| if (debugMode) { | |
| // Must use `set` to ensure the validated state is the same as the actual state used in reducer | |
| // Do not use `get` since `getState` queue has lower priority and the validated state would be the state after reduced | |
| stateStore.set { | |
| val firstState = this.reducer() | |
| val secondState = this.reducer() | |
| if (firstState != secondState) throw IllegalArgumentException("Your reducer must be pure!") | |
| mutableStateChecker.onStateChanged(firstState) | |
| firstState | |
| } | |
| } else { | |
| stateStore.set(reducer) | |
| } | |
| } | |
| /** | |
| * Access the current ViewModel state. Takes a block of code that will be run after all current pending state | |
| * updates are processed. The `this` inside of the block is the state. | |
| */ | |
| protected fun withState(block: (state: S) -> Unit) { | |
| stateStore.get(block) | |
| } | |
| /** | |
| * Validates a number of properties on the state class. This cannot be called from the main thread because it does | |
| * a fair amount of reflection. | |
| */ | |
| private fun validateState(initialState: S) { | |
| if (state::class.visibility != KVisibility.PUBLIC) { | |
| throw IllegalStateException("Your state class ${state::class.qualifiedName} must be public.") | |
| } | |
| state::class.assertImmutability() | |
| val bundle = state.persistState(assertCollectionPersistability = true) | |
| bundle.restorePersistedState(initialState) | |
| } | |
| /** | |
| * Helper to map an Single to an Async property on the state object. | |
| */ | |
| fun <T> Single<T>.execute( | |
| stateReducer: S.(Async<T>) -> S | |
| ) = toObservable().execute({ it }, null, stateReducer) | |
| /** | |
| * Helper to map an Single to an Async property on the state object. | |
| * @param mapper A map converting the observable type to the desired AsyncData type. | |
| * @param stateReducer A reducer that is applied to the current state and should return the | |
| * new state. Because the state is the receiver and it likely a data | |
| * class, an implementation may look like: `{ copy(response = it) }`. | |
| */ | |
| fun <T, V> Single<T>.execute( | |
| mapper: (T) -> V, | |
| stateReducer: S.(Async<V>) -> S | |
| ) = toObservable().execute(mapper, null, stateReducer) | |
| /** | |
| * Helper to map an observable to an Async property on the state object. | |
| */ | |
| fun <T> Observable<T>.execute( | |
| stateReducer: S.(Async<T>) -> S | |
| ) = execute({ it }, null, stateReducer) | |
| /** | |
| * Execute an observable and wrap its progression with AsyncData reduced to the global state. | |
| * | |
| * @param mapper A map converting the observable type to the desired AsyncData type. | |
| * @param successMetaData A map that provides metadata to set on the Success result. | |
| * It allows data about the original Observable to be kept and accessed later. For example, | |
| * your mapper could map a network request to just the data your UI needs, but your base layers could | |
| * keep metadata about the request, like timing, for logging. | |
| * @param stateReducer A reducer that is applied to the current state and should return the | |
| * new state. Because the state is the receiver and it likely a data | |
| * class, an implementation may look like: `{ copy(response = it) }`. | |
| * | |
| * @see Success.metadata | |
| */ | |
| fun <T, V> Observable<T>.execute( | |
| mapper: (T) -> V, | |
| successMetaData: ((T) -> Any)? = null, | |
| stateReducer: S.(Async<V>) -> S | |
| ): Disposable { | |
| setState { stateReducer(Loading()) } | |
| return map { | |
| val success = Success(mapper(it)) | |
| success.metadata = successMetaData?.invoke(it) | |
| success as Async<V> | |
| } | |
| .onErrorReturn { Fail(it) } | |
| .subscribe { asyncData -> setState { stateReducer(asyncData) } } | |
| .disposeOnClear() | |
| } | |
| /** | |
| * Output all state changes to logcat. | |
| */ | |
| fun logStateChanges() { | |
| if (!debugMode) return | |
| subscribe { Log.d(tag, "New State: $it") } | |
| } | |
| /** | |
| * For ViewModels that want to subscribe to itself. | |
| */ | |
| protected fun subscribe(subscriber: (S) -> Unit) = | |
| stateStore.observable.subscribeLifecycle(null, subscriber) | |
| @RestrictTo(RestrictTo.Scope.LIBRARY) | |
| fun subscribe(owner: LifecycleOwner, subscriber: (S) -> Unit) = | |
| stateStore.observable.subscribeLifecycle(owner, subscriber) | |
| /** | |
| * Subscribe to state changes for only a single property. | |
| */ | |
| protected fun <A> selectSubscribe( | |
| prop1: KProperty1<S, A>, | |
| subscriber: (A) -> Unit | |
| ) = selectSubscribeInternal(null, prop1, subscriber) | |
| @RestrictTo(RestrictTo.Scope.LIBRARY) | |
| fun <A> selectSubscribe( | |
| owner: LifecycleOwner, | |
| prop1: KProperty1<S, A>, | |
| subscriber: (A) -> Unit | |
| ) = selectSubscribeInternal(owner, prop1, subscriber) | |
| private fun <A> selectSubscribeInternal( | |
| owner: LifecycleOwner?, | |
| prop1: KProperty1<S, A>, | |
| subscriber: (A) -> Unit | |
| ) = stateStore.observable | |
| .map { MvRxTuple1(prop1.get(it)) } | |
| .distinctUntilChanged() | |
| .subscribeLifecycle(owner) { (a) -> subscriber(a) } | |
| /** | |
| * Subscribe to changes in an async property. There are optional parameters for onSuccess | |
| * and onFail which automatically unwrap the value or error. | |
| */ | |
| protected fun <T> asyncSubscribe( | |
| asyncProp: KProperty1<S, Async<T>>, | |
| onFail: ((Throwable) -> Unit)? = null, | |
| onSuccess: ((T) -> Unit)? = null | |
| ) = asyncSubscribeInternal(null, asyncProp, onFail, onSuccess) | |
| @RestrictTo(RestrictTo.Scope.LIBRARY) | |
| fun <T> asyncSubscribe( | |
| owner: LifecycleOwner, | |
| asyncProp: KProperty1<S, Async<T>>, | |
| onFail: ((Throwable) -> Unit)? = null, | |
| onSuccess: ((T) -> Unit)? = null | |
| ) = asyncSubscribeInternal(owner, asyncProp, onFail, onSuccess) | |
| private fun <T> asyncSubscribeInternal( | |
| owner: LifecycleOwner?, | |
| asyncProp: KProperty1<S, Async<T>>, | |
| onFail: ((Throwable) -> Unit)? = null, | |
| onSuccess: ((T) -> Unit)? = null | |
| ) = selectSubscribeInternal(owner, asyncProp) { | |
| if (onSuccess != null && it is Success) { | |
| onSuccess(it()) | |
| } else if (onFail != null && it is Fail) { | |
| onFail(it.error) | |
| } | |
| } | |
| /** | |
| * Subscribe to state changes for two properties. | |
| */ | |
| protected fun <A, B> selectSubscribe( | |
| prop1: KProperty1<S, A>, | |
| prop2: KProperty1<S, B>, | |
| subscriber: (A, B) -> Unit | |
| ) = selectSubscribeInternal(null, prop1, prop2, subscriber) | |
| @RestrictTo(RestrictTo.Scope.LIBRARY) | |
| fun <A, B> selectSubscribe( | |
| owner: LifecycleOwner, | |
| prop1: KProperty1<S, A>, | |
| prop2: KProperty1<S, B>, | |
| subscriber: (A, B) -> Unit | |
| ) = selectSubscribeInternal(owner, prop1, prop2, subscriber) | |
| private fun <A, B> selectSubscribeInternal( | |
| owner: LifecycleOwner?, | |
| prop1: KProperty1<S, A>, | |
| prop2: KProperty1<S, B>, | |
| subscriber: (A, B) -> Unit | |
| ) = stateStore.observable | |
| .map { MvRxTuple2(prop1.get(it), prop2.get(it)) } | |
| .distinctUntilChanged() | |
| .subscribeLifecycle(owner) { (a, b) -> subscriber(a, b) } | |
| /** | |
| * Subscribe to state changes for three properties. | |
| */ | |
| protected fun <A, B, C> selectSubscribe( | |
| prop1: KProperty1<S, A>, | |
| prop2: KProperty1<S, B>, | |
| prop3: KProperty1<S, C>, | |
| subscriber: (A, B, C) -> Unit | |
| ) = selectSubscribeInternal(null, prop1, prop2, prop3, subscriber) | |
| @RestrictTo(RestrictTo.Scope.LIBRARY) | |
| fun <A, B, C> selectSubscribe( | |
| owner: LifecycleOwner, | |
| prop1: KProperty1<S, A>, | |
| prop2: KProperty1<S, B>, | |
| prop3: KProperty1<S, C>, | |
| subscriber: (A, B, C) -> Unit | |
| ) = selectSubscribeInternal(owner, prop1, prop2, prop3, subscriber) | |
| private fun <A, B, C> selectSubscribeInternal( | |
| owner: LifecycleOwner?, | |
| prop1: KProperty1<S, A>, | |
| prop2: KProperty1<S, B>, | |
| prop3: KProperty1<S, C>, | |
| subscriber: (A, B, C) -> Unit | |
| ) = stateStore.observable | |
| .map { MvRxTuple3(prop1.get(it), prop2.get(it), prop3.get(it)) } | |
| .distinctUntilChanged() | |
| .subscribeLifecycle(owner) { (a, b, c) -> subscriber(a, b, c) } | |
| /** | |
| * Subscribe to state changes for four properties. | |
| */ | |
| protected fun <A, B, C, D> selectSubscribe( | |
| prop1: KProperty1<S, A>, | |
| prop2: KProperty1<S, B>, | |
| prop3: KProperty1<S, C>, | |
| prop4: KProperty1<S, D>, | |
| subscriber: (A, B, C, D) -> Unit | |
| ) = selectSubscribeInternal(null, prop1, prop2, prop3, prop4, subscriber) | |
| @RestrictTo(RestrictTo.Scope.LIBRARY) | |
| fun <A, B, C, D> selectSubscribe( | |
| owner: LifecycleOwner, | |
| prop1: KProperty1<S, A>, | |
| prop2: KProperty1<S, B>, | |
| prop3: KProperty1<S, C>, | |
| prop4: KProperty1<S, D>, | |
| subscriber: (A, B, C, D) -> Unit | |
| ) = selectSubscribeInternal(owner, prop1, prop2, prop3, prop4, subscriber) | |
| private fun <A, B, C, D> selectSubscribeInternal( | |
| owner: LifecycleOwner?, | |
| prop1: KProperty1<S, A>, | |
| prop2: KProperty1<S, B>, | |
| prop3: KProperty1<S, C>, | |
| prop4: KProperty1<S, D>, | |
| subscriber: (A, B, C, D) -> Unit | |
| ) = stateStore.observable | |
| .map { MvRxTuple4(prop1.get(it), prop2.get(it), prop3.get(it), prop4.get(it)) } | |
| .distinctUntilChanged() | |
| .subscribeLifecycle(owner) { (a, b, c, d) -> subscriber(a, b, c, d) } | |
| private fun <T> Observable<T>.subscribeLifecycle( | |
| lifecycleOwner: LifecycleOwner? = null, | |
| subscriber: (T) -> Unit | |
| ): Disposable { | |
| if (lifecycleOwner == null) { | |
| return observeOn(AndroidSchedulers.mainThread()) | |
| .subscribe(subscriber) | |
| .disposeOnClear() | |
| } | |
| val lifecycleAwareObserver = MvRxLifecycleAwareObserver( | |
| lifecycleOwner, | |
| alwaysDeliverLastValueWhenUnlocked = true, | |
| onNext = Consumer<T> { subscriber(it) } | |
| ) | |
| return observeOn(AndroidSchedulers.mainThread()) | |
| .subscribeWith(lifecycleAwareObserver) | |
| .disposeOnClear() | |
| } | |
| protected fun Disposable.disposeOnClear(): Disposable { | |
| disposables.add(this) | |
| return this | |
| } | |
| override fun toString(): String = "${this::class.simpleName} $state" | |
| } |