diff --git a/autodispose-rxlifecycle/build.gradle b/autodispose-rxlifecycle/build.gradle new file mode 100644 index 000000000..56c49924d --- /dev/null +++ b/autodispose-rxlifecycle/build.gradle @@ -0,0 +1,58 @@ +/* + * Copyright (C) 2017. Uber Technologies + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +buildscript { + repositories { + jcenter() + maven { url deps.build.repositories.plugins } + } + dependencies { + classpath deps.build.gradlePlugins.apt + classpath deps.build.gradlePlugins.errorProne + } +} + +apply plugin: 'java-library' +apply plugin: 'net.ltgt.apt' +apply plugin: 'net.ltgt.errorprone' + +sourceCompatibility = "1.7" +targetCompatibility = "1.7" + +test { + testLogging.showStandardStreams = true +} + +dependencies { + apt deps.build.nullAway + testApt deps.build.nullAway + + compile project(':autodispose') + compile deps.misc.rxlifecycle + compileOnly deps.misc.errorProneAnnotations + compileOnly deps.misc.javaxExtras + + errorprone deps.build.checkerFramework + errorprone deps.build.errorProne + + testCompile project(':test-utils') +} + +tasks.withType(JavaCompile) { + options.compilerArgs += ["-Xep:NullAway:ERROR", "-XepOpt:NullAway:AnnotatedPackages=com.uber"] +} + +apply from: rootProject.file('gradle/gradle-mvn-push.gradle') diff --git a/autodispose-rxlifecycle/gradle.properties b/autodispose-rxlifecycle/gradle.properties new file mode 100755 index 000000000..d1f035f8d --- /dev/null +++ b/autodispose-rxlifecycle/gradle.properties @@ -0,0 +1,19 @@ +# +# Copyright (C) 2017. Uber Technologies +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +POM_NAME=AutoDispose (RxLifecycle Interop) +POM_ARTIFACT_ID=autodispose-rxlifecycle +POM_PACKAGING=jar diff --git a/autodispose-rxlifecycle/src/main/java/com/ubercab/autodispose/rxlifecycle/RxLifecycleInterop.java b/autodispose-rxlifecycle/src/main/java/com/ubercab/autodispose/rxlifecycle/RxLifecycleInterop.java new file mode 100644 index 000000000..a11185687 --- /dev/null +++ b/autodispose-rxlifecycle/src/main/java/com/ubercab/autodispose/rxlifecycle/RxLifecycleInterop.java @@ -0,0 +1,102 @@ +/* + * Copyright (c) 2017. Uber Technologies + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.ubercab.autodispose.rxlifecycle; + +import com.trello.rxlifecycle2.LifecycleProvider; +import com.trello.rxlifecycle2.OutsideLifecycleException; +import com.uber.autodispose.AutoDispose.ScopeHandler; +import com.uber.autodispose.LifecycleEndedException; +import com.uber.autodispose.ScopeProvider; +import io.reactivex.Maybe; + +/** + * Interop for RxLifecycle. It provides static utility methods to convert {@link + * LifecycleProvider} to {@link ScopeProvider}. + * + *

There are several static utility converter + * methods such as {@link #from(LifecycleProvider)} for {@link + * LifecycleProvider#bindToLifecycle()} and {@link #from(LifecycleProvider, Object)} for + * {@link LifecycleProvider#bindUntilEvent(Object)}. + *

+ * + * Note: RxLifecycle treats the {@link OutsideLifecycleException} + * as normal terminal event. There is no mapping to {@link LifecycleEndedException} and in such + * cases the stream is normally disposed. + */ +public final class RxLifecycleInterop { + + private RxLifecycleInterop() { + throw new AssertionError("No Instances"); + } + + private static final Object DEFAULT_THROWAWAY_OBJECT = new Object(); + + /** + * Converter for transforming {@link LifecycleProvider} to {@link ScopeProvider}. + * It disposes the source when the next reasonable event occurs. + *

+ * Example usage: + *


+   *   Observable.just(1)
+   *        .to(RxLifecycleInterop.from(lifecycleProvider))
+   *        .subscribe(...)
+   * 
+ * + * @param the lifecycle event. + * @param provider the {@link LifecycleProvider} for RxLifecycle. + * @return a {@link ScopeHandler} to create AutoDisposing transformation + */ + public static ScopeProvider from(final LifecycleProvider provider) { + return new ScopeProvider() { + @Override public Maybe requestScope() { + return provider.lifecycle() + .compose(provider.bindToLifecycle()) + .ignoreElements() + .toMaybe() + .defaultIfEmpty(DEFAULT_THROWAWAY_OBJECT); + } + }; + } + + /** + * Converter for transforming {@link LifecycleProvider} to {@link ScopeProvider}. + * It disposes the source when a specific event occurs. + *

+ * Example usage: + *


+   *   Observable.just(1)
+   *        .to(RxLifecycleInterop.from(lifecycleProvider, event))
+   *        .subscribe(...)
+   * 
+ * + * @param the lifecycle event. + * @param provider the {@link LifecycleProvider} for RxLifecycle. + * @param event the event at which the source is disposed. + * @return a {@link ScopeHandler} to create AutoDisposing transformation + */ + public static ScopeProvider from(final LifecycleProvider provider, final E event) { + return new ScopeProvider() { + @Override public Maybe requestScope() { + return provider.lifecycle() + .compose(provider.bindUntilEvent(event)) + .ignoreElements() + .toMaybe() + .defaultIfEmpty(DEFAULT_THROWAWAY_OBJECT); + } + }; + } +} diff --git a/autodispose-rxlifecycle/src/main/java/com/ubercab/autodispose/rxlifecycle/package-info.java b/autodispose-rxlifecycle/src/main/java/com/ubercab/autodispose/rxlifecycle/package-info.java new file mode 100644 index 000000000..6a9760a72 --- /dev/null +++ b/autodispose-rxlifecycle/src/main/java/com/ubercab/autodispose/rxlifecycle/package-info.java @@ -0,0 +1,23 @@ +/* + * Copyright (c) 2017. Uber Technologies + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * AutoDispose extensions for interop with RxLifecycle. This namely supports + * {@link com.trello.rxlifecycle2.LifecycleProvider}. + */ +@com.uber.javaxextras.FieldsMethodsAndParametersAreNonNullByDefault +package com.ubercab.autodispose.rxlifecycle; + diff --git a/autodispose-rxlifecycle/src/test/java/com/ubercab/autodispose/rxlifecycle/RxLifecycleInteropTest.java b/autodispose-rxlifecycle/src/test/java/com/ubercab/autodispose/rxlifecycle/RxLifecycleInteropTest.java new file mode 100644 index 000000000..cf6101a86 --- /dev/null +++ b/autodispose-rxlifecycle/src/test/java/com/ubercab/autodispose/rxlifecycle/RxLifecycleInteropTest.java @@ -0,0 +1,146 @@ +/* + * Copyright (c) 2017. Uber Technologies + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.ubercab.autodispose.rxlifecycle; + +import com.uber.autodispose.AutoDispose; +import com.uber.autodispose.test.RecordingObserver; +import io.reactivex.disposables.Disposable; +import io.reactivex.observers.TestObserver; +import io.reactivex.subjects.PublishSubject; +import org.junit.Test; + +import static com.google.common.truth.Truth.assertThat; + +public class RxLifecycleInteropTest { + + private static final RecordingObserver.Logger LOGGER = new RecordingObserver.Logger() { + @Override public void log(String message) { + System.out.println(RxLifecycleInteropTest.class.getSimpleName() + ": " + message); + } + }; + + private TestLifecycleProvider lifecycleProvider = new TestLifecycleProvider(); + + @Test + public void bindLifecycle_normalTermination_completeTheStream() { + lifecycleProvider.emitCreate(); + TestObserver o = new TestObserver<>(); + PublishSubject source = PublishSubject.create(); + Disposable d = source.to(AutoDispose.with( + RxLifecycleInterop.from(lifecycleProvider)).forObservable()) + .subscribeWith(o); + o.assertSubscribed(); + + assertThat(source.hasObservers()).isTrue(); + + source.onNext(1); + o.assertValue(1); + + source.onNext(2); + source.onComplete(); + o.assertValues(1, 2); + o.assertComplete(); + assertThat(d.isDisposed()).isFalse(); // Because it completed normally, was not disposed. + assertThat(source.hasObservers()).isFalse(); + } + + @Test + public void bindLifecycle_normalTermination_unsubscribe() { + lifecycleProvider.emitCreate(); + RecordingObserver o = new RecordingObserver<>(LOGGER); + PublishSubject source = PublishSubject.create(); + source.to(AutoDispose.with( + RxLifecycleInterop.from(lifecycleProvider)).forObservable()) + .subscribe(o); + o.takeSubscribe(); + + assertThat(source.hasObservers()).isTrue(); + + source.onNext(1); + assertThat(o.takeNext()).isEqualTo(1); + + lifecycleProvider.emitDestroy(); + source.onNext(2); + o.assertNoMoreEvents(); + assertThat(source.hasObservers()).isFalse(); + } + + @Test + public void bindLifecycle_outsideLifecycleBound_unsubscribe() { + lifecycleProvider.emitCreate(); + RecordingObserver o = new RecordingObserver<>(LOGGER); + PublishSubject source = PublishSubject.create(); + lifecycleProvider.emitDestroy(); + source + .to(AutoDispose.with( + RxLifecycleInterop.from(lifecycleProvider)).forObservable()) + .subscribe(o); + + o.takeSubscribe(); + + source.onNext(2); + o.assertNoMoreEvents(); + assertThat( + source.hasObservers()).isFalse(); // Because RxLifecycle + // treats OutsideLifecycleException as terminal event. + } + + @Test + public void bindUntilEvent_normalTermination_completeTheStream() { + lifecycleProvider.emitCreate(); + TestObserver o = new TestObserver<>(); + PublishSubject source = PublishSubject.create(); + Disposable d = source.to(AutoDispose.with(RxLifecycleInterop.from( + lifecycleProvider, + TestLifecycleProvider.Event.DESTROY)).forObservable()) + .subscribeWith(o); + o.assertSubscribed(); + + assertThat(source.hasObservers()).isTrue(); + + source.onNext(1); + o.assertValue(1); + + source.onNext(2); + source.onComplete(); + o.assertValues(1, 2); + o.assertComplete(); + assertThat(d.isDisposed()).isFalse(); // Because it completed normally, was not disposed. + assertThat(source.hasObservers()).isFalse(); + } + + @Test + public void bindUntilEvent_interruptedTermination_unsubscribe() { + lifecycleProvider.emitCreate(); + RecordingObserver o = new RecordingObserver<>(LOGGER); + PublishSubject source = PublishSubject.create(); + source.to(AutoDispose.with(RxLifecycleInterop.from(lifecycleProvider, + TestLifecycleProvider.Event.DESTROY)).forObservable()) + .subscribe(o); + o.takeSubscribe(); + + assertThat(source.hasObservers()).isTrue(); + + source.onNext(1); + assertThat(o.takeNext()).isEqualTo(1); + + lifecycleProvider.emitDestroy(); + source.onNext(2); + o.assertNoMoreEvents(); + assertThat(source.hasObservers()).isFalse(); + } +} diff --git a/autodispose-rxlifecycle/src/test/java/com/ubercab/autodispose/rxlifecycle/TestLifecycleProvider.java b/autodispose-rxlifecycle/src/test/java/com/ubercab/autodispose/rxlifecycle/TestLifecycleProvider.java new file mode 100644 index 000000000..7cac2cb8c --- /dev/null +++ b/autodispose-rxlifecycle/src/test/java/com/ubercab/autodispose/rxlifecycle/TestLifecycleProvider.java @@ -0,0 +1,68 @@ +/* + * Copyright (c) 2017. Uber Technologies + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.ubercab.autodispose.rxlifecycle; + +import com.trello.rxlifecycle2.LifecycleProvider; +import com.trello.rxlifecycle2.LifecycleTransformer; +import com.trello.rxlifecycle2.OutsideLifecycleException; +import com.trello.rxlifecycle2.RxLifecycle; +import io.reactivex.Observable; +import io.reactivex.functions.Function; +import io.reactivex.subjects.BehaviorSubject; + +final class TestLifecycleProvider implements LifecycleProvider { + + private static final Function CORRESPONDING_EVENTS = new Function() { + @Override public Event apply(Event event) + throws Exception { + switch (event) { + case CREATE: + return Event.DESTROY; + default: + throw new OutsideLifecycleException("Lifecycle ended"); + } + } + }; + + private final BehaviorSubject lifecycle = BehaviorSubject.create(); + + @Override public Observable lifecycle() { + return lifecycle.hide(); + } + + @Override + public LifecycleTransformer bindUntilEvent(Event event) { + return RxLifecycle.bindUntilEvent(lifecycle, event); + } + + @Override public LifecycleTransformer bindToLifecycle() { + return RxLifecycle.bind(lifecycle, CORRESPONDING_EVENTS); + } + + void emitCreate() { + lifecycle.onNext(Event.CREATE); + } + + void emitDestroy() { + lifecycle.onNext(Event.DESTROY); + } + + enum Event { + CREATE, + DESTROY + } +} diff --git a/gradle/dependencies.gradle b/gradle/dependencies.gradle index 7f013ae71..c6bec0e6d 100755 --- a/gradle/dependencies.gradle +++ b/gradle/dependencies.gradle @@ -52,7 +52,8 @@ def kotlin = [ def misc = [ errorProneAnnotations: "com.google.errorprone:error_prone_annotations:${versions.errorProne}", javaxExtras: "com.uber.javaxextras:javax-extras:0.1.0", - jsr305: 'com.google.code.findbugs:jsr305:3.0.2' + jsr305: 'com.google.code.findbugs:jsr305:3.0.2', + rxlifecycle: 'com.trello.rxlifecycle2:rxlifecycle:2.2.0' ] def rx = [ diff --git a/settings.gradle b/settings.gradle index b2f57264d..a74b395c8 100755 --- a/settings.gradle +++ b/settings.gradle @@ -23,5 +23,7 @@ include ':android:autodispose-android-archcomponents-test' include ':android:autodispose-android-archcomponents-test-kotlin' include ':autodispose' include ':autodispose-kotlin' +include 'autodispose-rxlifecycle' include ':sample' include ':test-utils' +