Commit afbebd5b authored by Simeon Anfinrud's avatar Simeon Anfinrud Committed by Commit Bot

[Chromecast] Observable: add map() and filter().

Renamed the transform() method into the terser map(), which is
more in line with other reactive and functional programming
frameworks and languages, and added a filter() method as well.

Updated documentation with examples for each.

Bug: NONE
Test: cast_base_junit_tests, cast_shell_junit_tests
Change-Id: I45797698500d829f175e1b350abe71185c03ff7f
Reviewed-on: https://chromium-review.googlesource.com/956970
Commit-Queue: Simeon Anfinrud <sanfin@chromium.org>
Reviewed-by: default avatarLuke Halliwell <halliwell@chromium.org>
Cr-Commit-Position: refs/heads/master@{#546186}
parent c3e9cc67
...@@ -289,6 +289,7 @@ if (is_android) { ...@@ -289,6 +289,7 @@ if (is_android) {
"$java_src_dir/org/chromium/chromecast/base/Itertools.java", "$java_src_dir/org/chromium/chromecast/base/Itertools.java",
"$java_src_dir/org/chromium/chromecast/base/Observable.java", "$java_src_dir/org/chromium/chromecast/base/Observable.java",
"$java_src_dir/org/chromium/chromecast/base/Scope.java", "$java_src_dir/org/chromium/chromecast/base/Scope.java",
"$java_src_dir/org/chromium/chromecast/base/Predicate.java",
"$java_src_dir/org/chromium/chromecast/base/ScopeFactories.java", "$java_src_dir/org/chromium/chromecast/base/ScopeFactories.java",
"$java_src_dir/org/chromium/chromecast/base/ScopeFactory.java", "$java_src_dir/org/chromium/chromecast/base/ScopeFactory.java",
"$java_src_dir/org/chromium/chromecast/base/Unit.java", "$java_src_dir/org/chromium/chromecast/base/Unit.java",
......
...@@ -55,30 +55,45 @@ public abstract class Observable<T> { ...@@ -55,30 +55,45 @@ public abstract class Observable<T> {
return new BothStateObserver<>(this, other).asObservable(); return new BothStateObserver<>(this, other).asObservable();
} }
/**
* Returns an Observable that is activated when `this` and `other` are activated in order.
*
* This is similar to `and()`, but does not activate if `other` is activated before `this`.
*
* @param <U> The activation data type of the other Observable.
*/
public final <U> Observable<Both<T, U>> andThen(Observable<U> other) {
return new SequenceStateObserver<>(this, other).asObservable();
}
/** /**
* Returns an Observable that applies the given Function to this Observable's activation * Returns an Observable that applies the given Function to this Observable's activation
* values. * values.
* *
* @param <R> The return type of the transform function. * @param <R> The return type of the transform function.
*/ */
public final <R> Observable<R> transform(Function<? super T, ? extends R> transform) { public final <R> Observable<R> map(Function<? super T, ? extends R> transform) {
Controller<R> controller = new Controller<>(); Controller<R> controller = new Controller<>();
watch((T value) -> { watch((T value) -> {
controller.set(transform.apply(value)); controller.set(transform.apply(value));
return () -> controller.reset(); return controller::reset;
}); });
return controller; return controller;
} }
/** /**
* Returns an Observable that is activated when `this` and `other` are activated in order. * Returns an Observable that is only activated when `this` is activated with a value such that
* * the given `predicate` returns true.
* This is similar to `and()`, but does not activate if `other` is activated before `this`.
*
* @param <U> The activation data type of the other Observable.
*/ */
public final <U> Observable<Both<T, U>> andThen(Observable<U> other) { public final Observable<T> filter(Predicate<? super T> predicate) {
return new SequenceStateObserver<>(this, other).asObservable(); Controller<T> controller = new Controller<>();
watch((T value) -> {
if (predicate.test(value)) {
controller.set(value);
}
return controller::reset;
});
return controller;
} }
/** /**
......
// Copyright 2018 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
package org.chromium.chromecast.base;
/**
* A function that takes a single argument and returns a boolean.
*
* TODO(sanfin): replace with Java 8 library if we're ever able to use the Java 8 library.
*
* @param <T> The argument type.
*/
public interface Predicate<T> { public boolean test(T value); }
...@@ -262,13 +262,13 @@ public class ObservableAndControllerTest { ...@@ -262,13 +262,13 @@ public class ObservableAndControllerTest {
} }
@Test @Test
public void testTransform() { public void testMap() {
Controller<String> a = new Controller<>(); Controller<String> a = new Controller<>();
List<String> result = new ArrayList<>(); List<String> result = new ArrayList<>();
a.watch(report(result, "unchanged")) a.watch(report(result, "unchanged"))
.transform(String::toLowerCase) .map(String::toLowerCase)
.watch(report(result, "lower")) .watch(report(result, "lower"))
.transform(String::toUpperCase) .map(String::toUpperCase)
.watch(report(result, "upper")); .watch(report(result, "upper"));
a.set("sImPlY sTeAmEd KaLe"); a.set("sImPlY sTeAmEd KaLe");
assertThat(result, assertThat(result,
...@@ -276,6 +276,24 @@ public class ObservableAndControllerTest { ...@@ -276,6 +276,24 @@ public class ObservableAndControllerTest {
"enter upper: SIMPLY STEAMED KALE")); "enter upper: SIMPLY STEAMED KALE"));
} }
@Test
public void testFilter() {
Controller<String> a = new Controller<>();
List<String> result = new ArrayList<>();
a.filter(String::isEmpty).watch(report(result, "empty"));
a.filter(s -> s.startsWith("a")).watch(report(result, "starts with a"));
a.filter(s -> s.endsWith("a")).watch(report(result, "ends with a"));
a.set("");
a.set("none");
a.set("add");
a.set("doa");
a.set("ada");
assertThat(result,
contains("enter empty: ", "exit empty", "enter starts with a: add",
"exit starts with a", "enter ends with a: doa", "exit ends with a",
"enter starts with a: ada", "enter ends with a: ada"));
}
@Test @Test
public void testSetControllerWithNullImplicitlyResets() { public void testSetControllerWithNullImplicitlyResets() {
Controller<String> a = new Controller<>(); Controller<String> a = new Controller<>();
......
...@@ -159,9 +159,9 @@ This really does cover all the cases we need. If multiple `set*()` calls are ...@@ -159,9 +159,9 @@ This really does cover all the cases we need. If multiple `set*()` calls are
made, an implicit `reset()` call will be made to the relevant `Controller` and made, an implicit `reset()` call will be made to the relevant `Controller` and
the `C` object associated with the first scope will be `close()`d. the `C` object associated with the first scope will be `close()`d.
What's better about this? First, notice that we **don't need to have any mutable What's better about this? First, notice we **don't need mutable variables**.
variables**. Both `Controller` objects are `final`, and are never `null`. We Both `Controller` objects are `final`, and are never `null`. We don't at any
don't at any point need to know what state the object is in inside any method point need to know what state the object is in inside any method
implementations; the `Controller`s and the pipeline set up by the `and()` and implementations; the `Controller`s and the pipeline set up by the `and()` and
`watch()` calls handle that for you. `watch()` calls handle that for you.
...@@ -595,8 +595,8 @@ and return an appropriate `ScopeFactory<Both>`: ...@@ -595,8 +595,8 @@ and return an appropriate `ScopeFactory<Both>`:
There are numerous instances where one may want to take the activation data of There are numerous instances where one may want to take the activation data of
some `Observable` and use it to set the state of a `Controller`, and reset that some `Observable` and use it to set the state of a `Controller`, and reset that
`Controller` when the `Observable` is deactivated. A shortcut to doing this `Controller` when the `Observable` is deactivated. A shortcut to doing this
without having to instantiate any `Controller` is provided with the without having to instantiate any `Controller` is provided with the `map()`
`transform()` method in the `Observable` interface. method in the `Observable` interface.
For example, we might have an `Activity` that overrides `onNewIntent()`, and For example, we might have an `Activity` that overrides `onNewIntent()`, and
extracts some data from the `Intent` it receives. We might want to register extracts some data from the `Intent` it receives. We might want to register
...@@ -608,8 +608,8 @@ public class MyActivity extends Activity { ...@@ -608,8 +608,8 @@ public class MyActivity extends Activity {
private final Controller<Intent> mIntentState = new Controller<>(); private final Controller<Intent> mIntentState = new Controller<>();
{ {
Observable<Uri> uriState = mIntentState.transform(Intent::getData); Observable<Uri> uriState = mIntentState.map(Intent::getData);
Observable<String> instanceIdState = uriState.transform(Uri::getPath); Observable<String> instanceIdState = uriState.map(Uri::getPath);
... ...
} }
...@@ -625,11 +625,10 @@ public class MyActivity extends Activity { ...@@ -625,11 +625,10 @@ public class MyActivity extends Activity {
} }
``` ```
The `transform()` method takes any function on the `Observable`'s activation The `map()` method takes any function on the `Observable`'s activation data and
data and creates a new `Observable` of the result of that function applied to creates a new `Observable` of the result of that function applied to the
the original `Observable`'s activation data. So the activation lifetime of original `Observable`'s activation data. So the activation lifetime of
`uriState` and `instanceIdState` are the same as `mIntentState` in this `uriState` and `instanceIdState` are the same as `mIntentState` in this example.
example.
The instance initializer can then call `watch()` on `uriState` or The instance initializer can then call `watch()` on `uriState` or
`instanceIdState` to register callbacks for when we get a new URI or instance `instanceIdState` to register callbacks for when we get a new URI or instance
...@@ -638,15 +637,15 @@ from the `Uri` is delegated to methods with no side-effects. ...@@ -638,15 +637,15 @@ from the `Uri` is delegated to methods with no side-effects.
### Handling null ### Handling null
If a function provided to a `transform()` method returns `null`, then the If a function provided to a `map()` method returns `null`, then the resulting
resulting `Observable` will be put in a deactivated state, even if the source `Observable` will be put in a deactivated state, even if the source `Observable`
`Observable` is activated. This can be used to **filter** invalid data from is activated. This can be used to **filter** invalid data from `Observable`s in
`Observable`s in the pipeline: the pipeline:
```java ```java
{ {
mIntentState.transform(Intent::getExtras) mIntentState.map(Intent::getExtras)
.transform((Bundle bundle) -> bundle.getString(INTENT_EXTRA_FOO)) .map((Bundle bundle) -> bundle.getString(INTENT_EXTRA_FOO))
.watch((String foo) -> ...); .watch((String foo) -> ...);
} }
``` ```
...@@ -656,6 +655,76 @@ not have the correct extra data field set. When this happens, the resulting ...@@ -656,6 +655,76 @@ not have the correct extra data field set. When this happens, the resulting
`Observable` simply does not activate, so the `ScopeFactory` registered in the `Observable` simply does not activate, so the `ScopeFactory` registered in the
`watch()` call does not need to worry that `foo` might be `null`. `watch()` call does not need to worry that `foo` might be `null`.
### Filtering data
One may wish to construct an `Observable` that is only activated if some
*predicate* on some other `Observable`'s activation data is true. This is easily
done using the `filter()` method on `Observable`.
This example will only log `"Got FOO intent"` if `mIntentState` was `set()` with
an `Intent` with action `"org.my.app.action.FOO"`:
```java
{
String ACTION_FOO = "org.my.app.action.FOO";
mIntentState.map(Intent::getAction)
.filter(ACTION_FOO::equals)
.watch(ScopeFactories.onEnter(() -> {
Log.d(TAG, "Got FOO intent");
}));
}
```
Since `Observable<T>#filter()` takes any `Predicate<T>`, which is a functional
interface whose method takes a `T` and returns a `boolean`, the parameter can be
an instance of a class that implements `Predicate<T>`:
```java
class InRangePredicate implements Predicate<Integer> {
private final int mMin;
private final int mMax;
private InRangePredicate(int min, int max) {
mMin = min;
mMax = max;
}
@Override
public boolean test(Integer value) {
return mMin <= value && value <= mMax;
}
}
InRangePredicate inRange(int min, int max) {
return new InRangePredicate(min, max);
}
Controller<Integer> hasIntState = new Controller<>();
Observable<Integer> hasValidIntState = hasIntState.filter(inRange(0, 10));
}
```
... or a method reference for a method that takes the activation data and
returns a boolean:
```java
class Util {
static boolean inRange(int i) {
return 0 <= i && i <= 10;
}
}
Controller<Integer> hasIntState = new Controller<>();
Observable<Integer> hasValidIntState = hasIntState.filter(Util::inRange);
```
... or a lambda that takes the activation data and returns a boolean:
```java
Controller<Integer> hasIntState = new Controller<>();
Observable<Integer> hasValidIntState =
hasIntState.filter(i -> 0 <= i && i <= 10);
```
## Tips and best practices ## Tips and best practices
### Construct the pipeline before modifying it ### Construct the pipeline before modifying it
...@@ -678,7 +747,7 @@ Generally, `Observable` methods like `watch()` should be called before any ...@@ -678,7 +747,7 @@ Generally, `Observable` methods like `watch()` should be called before any
`Controller` methods. A couple of things that one can do to help with this: `Controller` methods. A couple of things that one can do to help with this:
* Instantiate `Controller` objects in field initializers, not the constructor. * Instantiate `Controller` objects in field initializers, not the constructor.
* Set up the pipeline (`watch()`, `and()`, `transform()`, etc.) in an instance * Set up the pipeline (`watch()`, `and()`, `map()`, etc.) in an instance
initializer. This is run before anything else when creating an instance, initializer. This is run before anything else when creating an instance,
including the constructor, and is the same regardless of which constructor including the constructor, and is the same regardless of which constructor
is being used. This also removes the potential of accidentally depending on is being used. This also removes the potential of accidentally depending on
......
...@@ -55,9 +55,9 @@ public class CastWebContentsActivity extends Activity { ...@@ -55,9 +55,9 @@ public class CastWebContentsActivity extends Activity {
{ {
// Create an Observable that only supplies the Intent when not finishing. // Create an Observable that only supplies the Intent when not finishing.
Observable<Intent> hasIntentState = Observable<Intent> hasIntentState =
mGotIntentState.and(Observable.not(mIsFinishingState)).transform(Both::getFirst); mGotIntentState.and(Observable.not(mIsFinishingState)).map(Both::getFirst);
Observable<Intent> gotIntentAfterFinishingState = Observable<Intent> gotIntentAfterFinishingState =
mIsFinishingState.andThen(mGotIntentState).transform(Both::getSecond); mIsFinishingState.andThen(mGotIntentState).map(Both::getSecond);
mCreatedState.and(Observable.not(mIsTestingState)) mCreatedState.and(Observable.not(mIsTestingState))
.watch(() -> { .watch(() -> {
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment