Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Respect unique only on configuration changes. #207

Merged
merged 4 commits into from
Mar 12, 2019
Merged

Conversation

BenSchwab
Copy link
Collaborator

closes #204

Currently there is a bug where uniqueOnly will redeliver the last value, even if unchanged, during an orientation change. The intent of uniqueOnly was to emit a value only once, ever, to allow for actions that need to be taken only once, such as showing a PopTart or logging. (Just for posterity sake I want to note I recommend logging in ViewModel, as subscriptions there have a much simpler lifecycle).

In order to dedup a repeated value after a configuration change, the view model maintains a map between "subscriptionIds" and the last delivered value.

When you choose to subscribe with uniqueOnly, you must provide a subscription id, so the view model can perform the dedupping logic. An extension function uniqueOnly() on MvRxView will handle creating and persisting a unique subscription id across configuration changes.

@gpeal @elihart

Copy link
Contributor

@elihart elihart left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@BenSchwab this is great! nice solution to a tricky issue, didn't realize you were working on this, it was definitely something that needed solving.

general strategy lgtm and is pretty clever. I like that the enum approach allows us to expand delivery settings in the future.

just had a few questions about edge cases

@@ -12,14 +13,20 @@ abstract class BaseMvRxFragment : Fragment(), MvRxView {

override val mvrxViewModelStore by lazy { MvRxViewModelStore(viewModelStore) }

final override val mvrxViewId: String by lazy { mvrxPersistedViewId }
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maybe worth noting that accessing this before onCreate is invalid and will crash?


/**
* The subscription will receive all state updates, and may receive repeated state updates when transitioning
* from locked to unlocked states (stopped/destroyed -> started).
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: generally I don't think lifecycles recover back to started once they are destroyed. not in fragments or activities at least - the fragment view can be destroyed and recreated, but that's slightly different

val valueToDeliverOnUnlock = when {
deliveryMode is UniqueOnly -> lastUndeliveredValue
deliveryMode is Standard && lastUndeliveredValue != null -> lastUndeliveredValue
deliveryMode is Standard && lastUndeliveredValue == null -> lastValue
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what about the case where the state property is nullable, and there is a valid value of null to deliver? Seems like we need to better differentiate between a value of null and not having a lastUndeliveredValue set

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We always wrap properties in tuples, so they can never be null. I updated the generics to reflect this (RxJava doesn't allow null anyway)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ah, great. In that case it seems the deliveredFirstValue atomic boolean is unnecessary because it will always be true when lastDeliveredValueFromPriorObserver is non null (I think), so checking both seems redundant.

it is maybe more clear though

@@ -37,101 +45,125 @@ 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
* @param deliveryMode If [UniqueOnly], when this MvRxView goes from a stopped to start lifecycle a state value
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit start should be started

@@ -35,6 +36,7 @@ abstract class BaseMvRxViewModel<S : MvRxState>(
private val tag by lazy { javaClass.simpleName }
private val disposables = CompositeDisposable()
private lateinit var mutableStateChecker: MutableStateChecker<S>
private val lastDeliveredStates = ConcurrentHashMap<String, Any>()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think there is any check that the same id isn't used twice in separate subscriptions. The potential bug where different subscriptions with the same properties leads to collision worries me, especially since it doesn't seem like it would be an obvious issue.

Could we store a map of Id to Disposable here, and it is only valid to register the same id if the disposable is disposed?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good idea! I'll add this, but this makes it very important for us to fix the duplicate subscriptions if you subscribe outside of onCreate.

I have a fix in progress for that.

Copy link
Collaborator

@gpeal gpeal left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is really awesome. I'm impressed with the solution you've turned around here 😄 I had a minor comment and LGTM once Eli is on board too!

* A globally unique id for this MvRxView. If your MvRxView is being recreated due to a lifecycle event (e.g. rotation)
* you should assign a consistent id. Likely this means you should save the id in onSaveInstance state.
* The viewId will not be accessed until a subscribe method is called. Accessing mvrxViewId before calling
* super.onCreate() will cause a crash.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You might want to give an example implementation of this for people who are only looking at this and are implementing MvRxView themself.


internal fun appendPropertiesToId(vararg properties: KProperty1<*, *>) : DeliveryMode {
return when (this) {
is Standard -> Standard
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How about something more explicit like RedeliverOnStart?

Copy link
Contributor

@elihart elihart left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@BenSchwab looks great! thanks for this

lastDeliveredValue = if (deliveryMode is UniqueOnly) {
if (activeSubscriptions.contains(deliveryMode.subscriptionId)) {
throw IllegalStateException("Subscribing with a duplicate subscription id: ${deliveryMode.subscriptionId}. " +
"If you have multiple unique only subscriptions in a MvRx view that listen to the same")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

looks like this comment wasn't finished

"If you have multiple unique only subscriptions in a MvRx view that listen to the same")
}
activeSubscriptions.add(deliveryMode.subscriptionId)
lastDeliveredStates[deliveryMode.subscriptionId] as? T
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can we remove the value from the map lastDeliveredStates too, or does it need to be stored for later?

Copy link
Collaborator Author

@BenSchwab BenSchwab Mar 12, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, I guess we need to keep in the edge case that the state doesn't change across two configuration changes.

},
onDispose = {
if (deliveryMode is UniqueOnly) {
activeSubscriptions.remove(deliveryMode.subscriptionId)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nice

Copy link
Collaborator

@gpeal gpeal left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM once comments are addressed :)

@BenSchwab BenSchwab merged commit 883437d into master Mar 12, 2019
@BenSchwab BenSchwab deleted the bschwab--unique-id branch September 4, 2019 23:36
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Is that expected behavior of uniqueOnly = true?
3 participants