-
Notifications
You must be signed in to change notification settings - Fork 97
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
Added ToggledEventSource and added it to the MobiusLoopViewModel #137
Added ToggledEventSource and added it to the MobiusLoopViewModel #137
Conversation
81effc4
to
ba02e34
Compare
Codecov Report
@@ Coverage Diff @@
## master #137 +/- ##
=========================================
Coverage ? 79.35%
Complexity ? 337
=========================================
Files ? 87
Lines ? 1773
Branches ? 113
=========================================
Hits ? 1407
Misses ? 337
Partials ? 29
Continue to review full report at Codecov.
|
ba02e34
to
71b6159
Compare
/** An extension of MutableLiveData that allows its Active/Inactive state to be observed. */ | ||
final class ObservableMutableLiveData<T> extends MutableLiveData<T> | ||
implements EventSource<Boolean> { | ||
private final List<Consumer<Boolean>> stateListeners = new ArrayList<>(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
i think this would do better to be a CopyOnWriteArrayList instead, and remove the synchronization blocks
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hmm, I can convert it to that, my only concern (and why there's a copy done in notifyListeners
is someone deciding to unsubscribe in the middle of a notify back call - so it would modify the array while its being iterated over. I'm not sure if a CopyOnWriteArrayList would fix this, would it? Edit: I experimented around, i think a CopyOnWriteArrayList works fine.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So I did experiment around with this a little, seemed to work fine but I think maybe I still need to copy the array before sending since something might remove itself from the array while iterating, and that might cause issues.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
From the COWAL docs: the iterator is guaranteed not to throw ConcurrentModificationException. The iterator will not reflect additions, removals, or changes to the list since the iterator was created
. That class is super useful for cases like listeners/observers because it has the semantics you usually want, and it's generally better than any implementation you can come up with yourself. :)
mobius-android/src/main/java/com/spotify/mobius/android/MobiusLoopViewModel.java
Show resolved
Hide resolved
mobius-android/src/test/java/com/spotify/mobius/android/ObservableMutableLiveDataTest.java
Show resolved
Hide resolved
@Nonnull private final Function<T, Boolean> condition; | ||
|
||
@Nonnull | ||
final List<WeakReference<FilteredEventSource<?>>> filteredEventSources = new ArrayList<>(1); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This should probably also be a CopyOnWriteArrayList?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, this one should be fine to convert.
* | ||
* @param <T> The event class | ||
*/ | ||
public interface FilteringEventSource<T> extends EventSource<T> { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not a huge fan of calling this "filtering". I associate that word much stronger with just ignoring some events, and not starting/stopping something based on a condition. In Rx (JS/Java/C#/etc) this kind of behavior is referred to as "switching" (technically it's a special case of switchMap). I don't know if it's the perfect name for us, but I believe "filter" is usually one of the most well defined operators when it comes to reactive functional programming, and the classes here that use the name "filter" aren't following that definition.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hm, true, this doesn't filter on a per-event basis. Happy to rename it to something like SwitchMappingEventSource
? or better name if you have any suggestions?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think this interface is needed. What seems to be happening is:
- EventSource A produces data that shouldn't always be passed along.
- EventSource B produces some other data that determines whether A's data should be thrown away.
So what's needed is like an operator like
(EventSource<A>, EventSource<B>, Function<B, Boolean>) -> EventSource<A>
I think it's like a 'gate' in electronics, right? Only open if the signal on 'B' evaluates to true
. Maybe that's what it should be called? Something like:
EventSource<A> gated = EventSources.gatedBy(eventSourceB, conditionOnB).apply(eventSourceA);
* | ||
* @param <F> The Event class | ||
*/ | ||
final class FilteredEventSource<F> implements EventSource<F> { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
How come this is a separate thing from ConditionalFilteringEventSource? Did it end up being very complex?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, it seemed to also mix responsibilities a little. Originally it was an internal non-static class in ConditionalFilteringEventSource, but when writing tests i realized how big it was getting, and that really I was testing two separate things at once - the wrapping around some decider-event-source + forwarding its events, and the actual managing of subscribe/unsubscribe from to-be-filtered event sources. Splitting it seemed to make sense, and tests became smaller and more readable.
aea1b6b
to
853cd03
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think we should talk a bit about names, maybe this is more easily done offline.
* | ||
* @param <T> The event class | ||
*/ | ||
public interface FilteringEventSource<T> extends EventSource<T> { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think this interface is needed. What seems to be happening is:
- EventSource A produces data that shouldn't always be passed along.
- EventSource B produces some other data that determines whether A's data should be thrown away.
So what's needed is like an operator like
(EventSource<A>, EventSource<B>, Function<B, Boolean>) -> EventSource<A>
I think it's like a 'gate' in electronics, right? Only open if the signal on 'B' evaluates to true
. Maybe that's what it should be called? Something like:
EventSource<A> gated = EventSources.gatedBy(eventSourceB, conditionOnB).apply(eventSourceA);
2b313f3
to
28aae27
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Some comments and questions, good changes overall!
@@ -75,9 +75,24 @@ protected MobiusLoopViewModel( | |||
@Nonnull Init<M, F> init, | |||
@Nonnull WorkRunner mainLoopWorkRunner, | |||
int maxEffectQueueSize) { | |||
this( | |||
(Consumer<V> viewEffectConsumer, EventSource<Boolean> activeModelFilter) -> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit:
(Consumer<V> viewEffectConsumer, EventSource<Boolean> activeModelFilter) -> | |
(Consumer<V> viewEffectConsumer, EventSource<Boolean> activeModelEventSource) -> |
mobius-android/src/test/java/com/spotify/mobius/android/ObservableMutableLiveDataTest.java
Show resolved
Hide resolved
boolean initialToggleState) { | ||
final ToggledEventSource<E> toggledEventSource = | ||
new ToggledEventSource<>(targetEventSource, initialToggleState); | ||
new TogglingSourceObserver(togglingSource, toggledEventSource); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't understand this implementation - is the idea that when the wrapped event source gets garbage collected, then the TogglingSourceObserver should stop subscribing to it? (as in, dispose its subscription). Can that even happen? Is it possible to write this in a more explicit way - if not, could there be some more documentation on how it works and why that solution was chosen?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Kind of, yes. The idea was to not keep the toggledEventSource
(which is returned to the user) alive unless there's something else that has a reference to it. That's how I expect stand-alone EventSource objects to exist - only while something else can reach them.
My reasoning is like this: since this object (the TogglingSourceObserver
) subscribes to the togglingSource
at all times, that means the togglingSource
would keep this object in memory, which would then keep the toggledEventSource
alive too (unless I make it WeakRef'd). That would not make sense since there's no way to reach the toggledEventSource
in any way from the togglingEventSource
.
With the weak ref, at least I can do some memory cleanup the next time the togglingEventSource
changes true/false state.
I agree I should try to add some documentation to the constructor to make it clearer, and I'll check to make sure it isn't causing any issues.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I also updated the name of the class, plus added some extra documentation to it, even though its internal.
dataEventSource.emit("a"); | ||
|
||
dataEventSource.assertConsumerCount(1); | ||
assertThat(received.size(), equalTo(1)); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
A little tip: you can also use assertj here:
org.assertj.core.api.Assertions.assertThat(received).containsExactly("a");
dataEventSource.emit("b"); | ||
|
||
dataEventSource.assertConsumerCount(0); | ||
assertThat(received.size(), equalTo(0)); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
and here (with an import of the right assertThat
), you could write:
assertThat(received).isEmpty();
it's a matter of taste, but I quite like AssertJ - and Truth even better, but we're currently using AssertJ in Mobius.
} | ||
|
||
@Test | ||
public void testThatToggledEventSourceIsNotKeptAliveByUUnderlyingDataSource() { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Does this test still work if you create a subscription?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, it does - just tested. If you subscribe to an EventSource, but don't have a direct reference, it should be free to be GC'd - right? Actually need to retest, seems to somewhat work if the TogglingEventSource
is toggled after the =null
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ok, figured it out, was being kept alive by the Disposable, so once its disposed, it should not be kept alive by a subscription.
28aae27
to
fbb6b6c
Compare
…a Boolean-emitting EventSource
fbb6b6c
to
24a4915
Compare
Hi @togi @pettermahlen - So this PR has been around since last year, I wanted to figure out how to move it forward, either close it, or merge it. Any thoughts? |
Tbh I thought this was merged already, sorry about that. Looking through it, I don't see any problems with the PR and it seems to me like all the comments have been addressed, so I don't see any reason not to merge it. |
onClearedInternal
method in theMobiusLoopViewModel
that can be overriden and will be called by the onCleared method just before the loop is shut downObservableMutableLiveData
class that allows its state (active/inactive) to be observed as a `EventSourceMobiusLoopViewModel
creator function that supplies aEventSource
that's tied to the ViewModel'sgetModels(): LiveData<M>
state (specifically whether it has any active observers or not)ToggledEventSource
which forwards values from a target event source, and can additionally be controlled whether to forward those values from a secondary Boolean-emitting toggling-event-source.