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

Added ToggledEventSource and added it to the MobiusLoopViewModel #137

Conversation

milchopenchev
Copy link
Contributor

@milchopenchev milchopenchev commented Oct 9, 2020

  • Added a protected onClearedInternal method in the MobiusLoopViewModel that can be overriden and will be called by the onCleared method just before the loop is shut down
  • Added an (internal) ObservableMutableLiveData class that allows its state (active/inactive) to be observed as a `EventSource
  • Added new MobiusLoopViewModel creator function that supplies a EventSource that's tied to the ViewModel's getModels(): LiveData<M> state (specifically whether it has any active observers or not)
  • Added a utility class 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.

@milchopenchev milchopenchev force-pushed the milchop-mobius-loop-viewmodel-filtering-and-on-cleared-v2 branch from 81effc4 to ba02e34 Compare October 9, 2020 07:04
@codecov
Copy link

codecov bot commented Oct 9, 2020

Codecov Report

❗ No coverage uploaded for pull request base (master@a588f8a). Click here to learn what that means.
The diff coverage is 95.74%.

Impacted file tree graph

@@            Coverage Diff            @@
##             master     #137   +/-   ##
=========================================
  Coverage          ?   79.35%           
  Complexity        ?      337           
=========================================
  Files             ?       87           
  Lines             ?     1773           
  Branches          ?      113           
=========================================
  Hits              ?     1407           
  Misses            ?      337           
  Partials          ?       29           
Impacted Files Coverage Δ Complexity Δ
.../com/spotify/mobius/extras/ToggledEventSource.java 95.74% <95.74%> (ø) 13.00 <13.00> (?)
...main/java/com/spotify/mobius/rx2/Transformers.java 100.00% <0.00%> (ø) 6.00% <0.00%> (?%)
...main/java/com/spotify/mobius/rx3/RxMobiusLoop.java 100.00% <0.00%> (ø) 2.00% <0.00%> (?%)
...om/spotify/mobius/internal_util/ImmutableUtil.java 100.00% <0.00%> (ø) 5.00% <0.00%> (?%)
...xtras/connections/MergeConnectablesConnection.java 92.85% <0.00%> (ø) 8.00% <0.00%> (?%)
...in/java/com/spotify/mobius/rx3/RxEventSources.java 45.45% <0.00%> (ø) 1.00% <0.00%> (?%)
.../src/main/java/com/spotify/mobius/MobiusStore.java 100.00% <0.00%> (ø) 3.00% <0.00%> (?%)
...ain/java/com/spotify/mobius/rx/RxEventSources.java 48.14% <0.00%> (ø) 1.00% <0.00%> (?%)
...ava/com/spotify/mobius/ControllerStateRunning.java 92.85% <0.00%> (ø) 8.00% <0.00%> (?%)
...java/com/spotify/mobius/rx2/MergedTransformer.java 100.00% <0.00%> (ø) 2.00% <0.00%> (?%)
... and 78 more

Continue to review full report at Codecov.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update a588f8a...24a4915. Read the comment docs.

@milchopenchev milchopenchev force-pushed the milchop-mobius-loop-viewmodel-filtering-and-on-cleared-v2 branch from ba02e34 to 71b6159 Compare October 9, 2020 07:12
/** 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<>();
Copy link
Collaborator

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

Copy link
Contributor Author

@milchopenchev milchopenchev Oct 16, 2020

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.

Copy link
Contributor Author

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.

Copy link
Member

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. :)

@Nonnull private final Function<T, Boolean> condition;

@Nonnull
final List<WeakReference<FilteredEventSource<?>>> filteredEventSources = new ArrayList<>(1);
Copy link
Collaborator

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?

Copy link
Contributor Author

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> {
Copy link
Collaborator

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.

Copy link
Contributor Author

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?

Copy link
Member

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:

  1. EventSource A produces data that shouldn't always be passed along.
  2. 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> {
Copy link
Collaborator

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?

Copy link
Contributor Author

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.

@milchopenchev milchopenchev force-pushed the milchop-mobius-loop-viewmodel-filtering-and-on-cleared-v2 branch 2 times, most recently from aea1b6b to 853cd03 Compare October 17, 2020 13:10
Copy link
Member

@pettermahlen pettermahlen left a 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> {
Copy link
Member

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:

  1. EventSource A produces data that shouldn't always be passed along.
  2. 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);

@milchopenchev milchopenchev force-pushed the milchop-mobius-loop-viewmodel-filtering-and-on-cleared-v2 branch 2 times, most recently from 2b313f3 to 28aae27 Compare October 23, 2020 13:26
@milchopenchev milchopenchev changed the title Added FilteringEventSource and added it to the MobiusLoopViewModel Added ToggledEventSource and added it to the MobiusLoopViewModel Oct 23, 2020
togi
togi previously approved these changes Oct 23, 2020
Copy link
Member

@pettermahlen pettermahlen left a 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) ->
Copy link
Member

Choose a reason for hiding this comment

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

nit:

Suggested change
(Consumer<V> viewEffectConsumer, EventSource<Boolean> activeModelFilter) ->
(Consumer<V> viewEffectConsumer, EventSource<Boolean> activeModelEventSource) ->

boolean initialToggleState) {
final ToggledEventSource<E> toggledEventSource =
new ToggledEventSource<>(targetEventSource, initialToggleState);
new TogglingSourceObserver(togglingSource, toggledEventSource);
Copy link
Member

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?

Copy link
Contributor Author

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.

Copy link
Contributor Author

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));
Copy link
Member

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));
Copy link
Member

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() {
Copy link
Member

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?

Copy link
Contributor Author

@milchopenchev milchopenchev Nov 4, 2020

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

Copy link
Contributor Author

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.

@milchopenchev milchopenchev force-pushed the milchop-mobius-loop-viewmodel-filtering-and-on-cleared-v2 branch from 28aae27 to fbb6b6c Compare November 4, 2020 15:03
@milchopenchev milchopenchev force-pushed the milchop-mobius-loop-viewmodel-filtering-and-on-cleared-v2 branch from fbb6b6c to 24a4915 Compare November 5, 2020 10:27
@milchopenchev
Copy link
Contributor Author

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?

@togi
Copy link
Collaborator

togi commented Jan 21, 2021

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.

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.

3 participants