Skip to content

Commit

Permalink
Don't re-emit state value stop->start if the state did not change (#113)
Browse files Browse the repository at this point in the history
* Don't re-emit state value stop->start if the state did not change

* Delete wrong test

* Add uniqueOnly option to MvRxView subscribes

* Better comment
  • Loading branch information
Ben Schwab authored Oct 23, 2018
1 parent 1acbbf4 commit 4047426
Show file tree
Hide file tree
Showing 4 changed files with 308 additions and 54 deletions.
58 changes: 34 additions & 24 deletions mvrx/src/main/kotlin/com/airbnb/mvrx/BaseMvRxViewModel.kt
Original file line number Diff line number Diff line change
Expand Up @@ -146,10 +146,10 @@ abstract class BaseMvRxViewModel<S : MvRxState>(
setState { stateReducer(Loading()) }

return map {
val success = Success(mapper(it))
success.metadata = successMetaData?.invoke(it)
success as Async<V>
}
val success = Success(mapper(it))
success.metadata = successMetaData?.invoke(it)
success as Async<V>
}
.onErrorReturn { Fail(it) }
.subscribe { asyncData -> setState { stateReducer(asyncData) } }
.disposeOnClear()
Expand All @@ -167,35 +167,37 @@ abstract class BaseMvRxViewModel<S : MvRxState>(
* For ViewModels that want to subscribe to itself.
*/
protected fun subscribe(subscriber: (S) -> Unit) =
stateStore.observable.subscribeLifecycle(null, subscriber)
stateStore.observable.subscribeLifecycle(null, false, subscriber)

@RestrictTo(RestrictTo.Scope.LIBRARY)
fun subscribe(owner: LifecycleOwner, subscriber: (S) -> Unit) =
stateStore.observable.subscribeLifecycle(owner, subscriber)
fun subscribe(owner: LifecycleOwner, uniqueOnly: Boolean = false, subscriber: (S) -> Unit) =
stateStore.observable.subscribeLifecycle(owner, uniqueOnly, 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)
) = selectSubscribeInternal(null, prop1, false, subscriber)

@RestrictTo(RestrictTo.Scope.LIBRARY)
fun <A> selectSubscribe(
owner: LifecycleOwner,
prop1: KProperty1<S, A>,
uniqueOnly: Boolean = false,
subscriber: (A) -> Unit
) = selectSubscribeInternal(owner, prop1, subscriber)
) = selectSubscribeInternal(owner, prop1, uniqueOnly, subscriber)

private fun <A> selectSubscribeInternal(
owner: LifecycleOwner?,
prop1: KProperty1<S, A>,
uniqueOnly: Boolean = false,
subscriber: (A) -> Unit
) = stateStore.observable
.map { MvRxTuple1(prop1.get(it)) }
.distinctUntilChanged()
.subscribeLifecycle(owner) { (a) -> subscriber(a) }
.subscribeLifecycle(owner, uniqueOnly) { (a) -> subscriber(a) }

/**
* Subscribe to changes in an async property. There are optional parameters for onSuccess
Expand All @@ -205,22 +207,24 @@ abstract class BaseMvRxViewModel<S : MvRxState>(
asyncProp: KProperty1<S, Async<T>>,
onFail: ((Throwable) -> Unit)? = null,
onSuccess: ((T) -> Unit)? = null
) = asyncSubscribeInternal(null, asyncProp, onFail, onSuccess)
) = asyncSubscribeInternal(null, asyncProp, false, onFail, onSuccess)

@RestrictTo(RestrictTo.Scope.LIBRARY)
fun <T> asyncSubscribe(
owner: LifecycleOwner,
asyncProp: KProperty1<S, Async<T>>,
uniqueOnly: Boolean = false,
onFail: ((Throwable) -> Unit)? = null,
onSuccess: ((T) -> Unit)? = null
) = asyncSubscribeInternal(owner, asyncProp, onFail, onSuccess)
) = asyncSubscribeInternal(owner, asyncProp, uniqueOnly, onFail, onSuccess)

private fun <T> asyncSubscribeInternal(
owner: LifecycleOwner?,
asyncProp: KProperty1<S, Async<T>>,
uniqueOnly: Boolean = false,
onFail: ((Throwable) -> Unit)? = null,
onSuccess: ((T) -> Unit)? = null
) = selectSubscribeInternal(owner, asyncProp) {
) = selectSubscribeInternal(owner, asyncProp, uniqueOnly) {
if (onSuccess != null && it is Success) {
onSuccess(it())
} else if (onFail != null && it is Fail) {
Expand All @@ -235,26 +239,27 @@ abstract class BaseMvRxViewModel<S : MvRxState>(
prop1: KProperty1<S, A>,
prop2: KProperty1<S, B>,
subscriber: (A, B) -> Unit
) = selectSubscribeInternal(null, prop1, prop2, subscriber)
) = selectSubscribeInternal(null, prop1, prop2, false, subscriber)

@RestrictTo(RestrictTo.Scope.LIBRARY)
fun <A, B> selectSubscribe(
owner: LifecycleOwner,
prop1: KProperty1<S, A>,
prop2: KProperty1<S, B>,
uniqueOnly: Boolean = false,
subscriber: (A, B) -> Unit
) = selectSubscribeInternal(owner, prop1, prop2, subscriber)

) = selectSubscribeInternal(owner, prop1, prop2, uniqueOnly, subscriber)

private fun <A, B> selectSubscribeInternal(
owner: LifecycleOwner?,
prop1: KProperty1<S, A>,
prop2: KProperty1<S, B>,
uniqueOnly: Boolean = false,
subscriber: (A, B) -> Unit
) = stateStore.observable
.map { MvRxTuple2(prop1.get(it), prop2.get(it)) }
.distinctUntilChanged()
.subscribeLifecycle(owner) { (a, b) -> subscriber(a, b) }
.subscribeLifecycle(owner, uniqueOnly) { (a, b) -> subscriber(a, b) }

/**
* Subscribe to state changes for three properties.
Expand All @@ -264,27 +269,29 @@ abstract class BaseMvRxViewModel<S : MvRxState>(
prop2: KProperty1<S, B>,
prop3: KProperty1<S, C>,
subscriber: (A, B, C) -> Unit
) = selectSubscribeInternal(null, prop1, prop2, prop3, subscriber)
) = selectSubscribeInternal(null, prop1, prop2, prop3, false, subscriber)

@RestrictTo(RestrictTo.Scope.LIBRARY)
fun <A, B, C> selectSubscribe(
owner: LifecycleOwner,
prop1: KProperty1<S, A>,
prop2: KProperty1<S, B>,
prop3: KProperty1<S, C>,
uniqueOnly: Boolean = false,
subscriber: (A, B, C) -> Unit
) = selectSubscribeInternal(owner, prop1, prop2, prop3, subscriber)
) = selectSubscribeInternal(owner, prop1, prop2, prop3, uniqueOnly, subscriber)

private fun <A, B, C> selectSubscribeInternal(
owner: LifecycleOwner?,
prop1: KProperty1<S, A>,
prop2: KProperty1<S, B>,
prop3: KProperty1<S, C>,
uniqueOnly: Boolean = false,
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) }
.subscribeLifecycle(owner, uniqueOnly) { (a, b, c) -> subscriber(a, b, c) }

/**
* Subscribe to state changes for four properties.
Expand All @@ -295,7 +302,7 @@ abstract class BaseMvRxViewModel<S : MvRxState>(
prop3: KProperty1<S, C>,
prop4: KProperty1<S, D>,
subscriber: (A, B, C, D) -> Unit
) = selectSubscribeInternal(null, prop1, prop2, prop3, prop4, subscriber)
) = selectSubscribeInternal(null, prop1, prop2, prop3, prop4, false, subscriber)

@RestrictTo(RestrictTo.Scope.LIBRARY)
fun <A, B, C, D> selectSubscribe(
Expand All @@ -304,23 +311,26 @@ abstract class BaseMvRxViewModel<S : MvRxState>(
prop2: KProperty1<S, B>,
prop3: KProperty1<S, C>,
prop4: KProperty1<S, D>,
uniqueOnly: Boolean = false,
subscriber: (A, B, C, D) -> Unit
) = selectSubscribeInternal(owner, prop1, prop2, prop3, prop4, subscriber)
) = selectSubscribeInternal(owner, prop1, prop2, prop3, prop4, uniqueOnly, 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>,
uniqueOnly: Boolean,
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) }
.subscribeLifecycle(owner, uniqueOnly) { (a, b, c, d) -> subscriber(a, b, c, d) }

private fun <T> Observable<T>.subscribeLifecycle(
lifecycleOwner: LifecycleOwner? = null,
uniqueOnly: Boolean,
subscriber: (T) -> Unit
): Disposable {
if (lifecycleOwner == null) {
Expand All @@ -331,7 +341,7 @@ abstract class BaseMvRxViewModel<S : MvRxState>(

val lifecycleAwareObserver = MvRxLifecycleAwareObserver(
lifecycleOwner,
alwaysDeliverLastValueWhenUnlocked = true,
alwaysDeliverLastValueWhenUnlocked = !uniqueOnly,
onNext = Consumer<T> { subscriber(it) }
)
return observeOn(AndroidSchedulers.mainThread())
Expand Down
59 changes: 53 additions & 6 deletions mvrx/src/main/kotlin/com/airbnb/mvrx/MvRxView.kt
Original file line number Diff line number Diff line change
Expand Up @@ -36,55 +36,102 @@ interface MvRxView : MvRxViewModelStoreOwner, LifecycleOwner {

/**
* Subscribes to all state updates for the given viewModel.
*
* @param uniqueOnly If true, when this MvRxView goes from a stopped to start lifecycle a state value
* will only be emitted if the state changed. This is useful for transient views that should only
* be shown once (toasts, poptarts), or logging. Most other views should use false, as when a view is destroyed
* and recreated the previous state is necessary to recreate the view.
*
* Default: false.
*/
fun <S : MvRxState> BaseMvRxViewModel<S>.subscribe(subscriber: (S) -> Unit) = subscribe(this@MvRxView, subscriber)
fun <S : MvRxState> BaseMvRxViewModel<S>.subscribe(uniqueOnly: Boolean = false, subscriber: (S) -> Unit) = subscribe(this@MvRxView, uniqueOnly, subscriber)

/**
* Subscribes to state changes for only a specific property and calls the subscribe with
* only that single property.
*
* @param uniqueOnly If true, when this MvRxView goes from a stopped to start lifecycle a state value
* will only be emitted if the state changed. This is useful for transient views that should only
* be shown once (toasts, poptarts), or logging. Most other views should use false, as when a view is destroyed
* and recreated the previous state is necessary to recreate the view.
*
* Default: false.
*/
fun <S : MvRxState, A> BaseMvRxViewModel<S>.selectSubscribe(
prop1: KProperty1<S, A>,
uniqueOnly: Boolean = false,
subscriber: (A) -> Unit
) = selectSubscribe(this@MvRxView, prop1, subscriber)
) = selectSubscribe(this@MvRxView, prop1, uniqueOnly, subscriber)

/**
* Subscribe to changes in an async property. There are optional parameters for onSuccess
* and onFail which automatically unwrap the value or error.
*
* @param uniqueOnly If true, when this MvRxView goes from a stopped to start lifecycle a state value
* will only be emitted if the state changed. This is useful for transient views that should only
* be shown once (toasts, poptarts), or logging. Most other views should use false, as when a view is destroyed
* and recreated the previous state is necessary to recreate the view.
*
* Default: false.
*/
fun <S : MvRxState, T> BaseMvRxViewModel<S>.asyncSubscribe(
asyncProp: KProperty1<S, Async<T>>,
uniqueOnly: Boolean = false,
onFail: ((Throwable) -> Unit)? = null,
onSuccess: ((T) -> Unit)? = null
) = asyncSubscribe(this@MvRxView, asyncProp, onFail, onSuccess)
) = asyncSubscribe(this@MvRxView, asyncProp, uniqueOnly, onFail, onSuccess)

/**
* Subscribes to state changes for two properties.
*
* @param uniqueOnly If true, when this MvRxView goes from a stopped to start lifecycle a state value
* will only be emitted if the state changed. This is useful for transient views that should only
* be shown once (toasts, poptarts), or logging. Most other views should use false, as when a view is destroyed
* and recreated the previous state is necessary to recreate the view.
*
* Default: false.
*/
fun <S : MvRxState, A, B> BaseMvRxViewModel<S>.selectSubscribe(
prop1: KProperty1<S, A>,
prop2: KProperty1<S, B>,
uniqueOnly: Boolean = false,
subscriber: (A, B) -> Unit
) = selectSubscribe(this@MvRxView, prop1, prop2, subscriber)
) = selectSubscribe(this@MvRxView, prop1, prop2, uniqueOnly, subscriber)

/**
* Subscribes to state changes for three properties.
*
* @param uniqueOnly If true, when this MvRxView goes from a stopped to start lifecycle a state value
* will only be emitted if the state changed. This is useful for transient views that should only
* be shown once (toasts, poptarts), or logging. Most other views should use false, as when a view is destroyed
* and recreated the previous state is necessary to recreate the view.
*
* Default: false.
*/
fun <S : MvRxState, A, B, C> BaseMvRxViewModel<S>.selectSubscribe(
prop1: KProperty1<S, A>,
prop2: KProperty1<S, B>,
prop3: KProperty1<S, C>,
uniqueOnly: Boolean = false,
subscriber: (A, B, C) -> Unit
) = selectSubscribe(this@MvRxView, prop1, prop2, prop3, subscriber)
) = selectSubscribe(this@MvRxView, prop1, prop2, prop3, uniqueOnly, subscriber)

/**
* Subscribes to state changes for four properties.
*
* @param uniqueOnly If true, when this MvRxView goes from a stopped to start lifecycle a state value
* will only be emitted if the state changed. This is useful for transient views that should only
* be shown once (toasts, poptarts), or logging. Most other views should use false, as when a view is destroyed
* and recreated the previous state is necessary to recreate the view.
*
* Default: false.
*/
fun <S : MvRxState, A, B, C, D> BaseMvRxViewModel<S>.selectSubscribe(
prop1: KProperty1<S, A>,
prop2: KProperty1<S, B>,
prop3: KProperty1<S, C>,
prop4: KProperty1<S, D>,
uniqueOnly: Boolean = false,
subscriber: (A, B, C, D) -> Unit
) = selectSubscribe(this@MvRxView, prop1, prop2, prop3, prop4, subscriber)
) = selectSubscribe(this@MvRxView, prop1, prop2, prop3, prop4, uniqueOnly, subscriber)
}
64 changes: 48 additions & 16 deletions mvrx/src/test/kotlin/com/airbnb/mvrx/ViewModelSubscriberTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -475,38 +475,70 @@ class ViewModelSubscriberTest : BaseTest() {
assertEquals(3, callCount)
}


@Test
fun testAsync() {
fun testSubscribeCalledOnRestart() {
owner.lifecycle.markState(Lifecycle.State.RESUMED)
var callCount = 0
val success = "Hello World"
val fail = IllegalStateException("Uh oh")
viewModel.asyncSubscribe(owner, ViewModelTestState::async, onFail = {
callCount++
assertEquals(fail, it)
}) {
viewModel.subscribe(owner) {
callCount++
assertEquals(success, it)
}
viewModel.setAsync(Success(success))
viewModel.setAsync(Fail(fail))
assertEquals(1, callCount)
owner.lifecycle.handleLifecycleEvent(Lifecycle.Event.ON_PAUSE)
assertEquals(1, callCount)
owner.lifecycle.handleLifecycleEvent(Lifecycle.Event.ON_STOP)
assertEquals(1, callCount)
owner.lifecycle.handleLifecycleEvent(Lifecycle.Event.ON_START)
assertEquals(2, callCount)
}

@Test
fun testSubscribeCalledOnRestart() {
owner.lifecycle.markState(Lifecycle.State.RESUMED)
fun testUniqueOnlySubscribeCalledOnStartIfUpdateOccurredInStop() {
owner.lifecycle.markState(Lifecycle.State.STARTED)

var callCount = 0
viewModel.subscribe(owner) {
viewModel.subscribe(owner, uniqueOnly = true) {
callCount++
}

owner.lifecycle.handleLifecycleEvent(Lifecycle.Event.ON_STOP)

viewModel.setFoo(1)
assertEquals(1, callCount)
owner.lifecycle.handleLifecycleEvent(Lifecycle.Event.ON_PAUSE)
assertEquals(1, callCount)

owner.lifecycle.handleLifecycleEvent(Lifecycle.Event.ON_START)
assertEquals(2, callCount)
}

@Test
fun testSubscribeNotCalledOnStartIfNoUpdateOccurredInStop() {
owner.lifecycle.markState(Lifecycle.State.STARTED)

var callCount = 0
viewModel.subscribe(owner, uniqueOnly = true) {
callCount++
}

owner.lifecycle.handleLifecycleEvent(Lifecycle.Event.ON_STOP)
assertEquals(1, callCount)

owner.lifecycle.handleLifecycleEvent(Lifecycle.Event.ON_START)
assertEquals(1, callCount)
}

@Test
fun testAsync() {
var callCount = 0
val success = "Hello World"
val fail = IllegalStateException("Uh oh")
viewModel.asyncSubscribe(owner, ViewModelTestState::async, onFail = {
callCount++
assertEquals(fail, it)
}) {
callCount++
assertEquals(success, it)
}
viewModel.setAsync(Success(success))
viewModel.setAsync(Fail(fail))
assertEquals(2, callCount)
}

Expand Down
Loading

0 comments on commit 4047426

Please sign in to comment.