diff --git a/build.gradle b/build.gradle index 7a2dad8..3efe6a4 100644 --- a/build.gradle +++ b/build.gradle @@ -20,8 +20,9 @@ ext { // Define all dependencies in the base project, to unify & make it easy to update rxJava = 'io.reactivex:rxjava:1.0.14' + rxBinding = 'com.jakewharton.rxbinding:rxbinding:0.2.0' appCompat = 'com.android.support:appcompat-v7:23.0.0' junit = 'junit:junit:4.12' mockito = 'org.mockito:mockito-core:1.10.19' robolectric = 'org.robolectric:robolectric:3.0' -} \ No newline at end of file +} diff --git a/rxlifecycle/build.gradle b/rxlifecycle/build.gradle index d32dec6..bf3a654 100644 --- a/rxlifecycle/build.gradle +++ b/rxlifecycle/build.gradle @@ -15,6 +15,7 @@ repositories { dependencies { compile rootProject.ext.rxJava + compile rootProject.ext.rxBinding testCompile rootProject.ext.junit testCompile rootProject.ext.mockito diff --git a/rxlifecycle/src/main/java/com/trello/rxlifecycle/RxLifecycle.java b/rxlifecycle/src/main/java/com/trello/rxlifecycle/RxLifecycle.java index d5ae518..9317745 100644 --- a/rxlifecycle/src/main/java/com/trello/rxlifecycle/RxLifecycle.java +++ b/rxlifecycle/src/main/java/com/trello/rxlifecycle/RxLifecycle.java @@ -14,6 +14,11 @@ package com.trello.rxlifecycle; +import android.view.View; + +import com.jakewharton.rxbinding.view.RxView; +import com.jakewharton.rxbinding.view.ViewAttachEvent; + import rx.Observable; import rx.functions.Func1; import rx.functions.Func2; @@ -135,6 +140,57 @@ public static Observable.Transformer bindFragment(Observable + * Use with {@link Observable#compose(Observable.Transformer)}: + * {@code source.compose(RxLifecycle.bindView(lifecycle)).subscribe()} + *

+ * This helper automatically determines (based on the lifecycle sequence itself) when the source + * should stop emitting items. For views, this effectively means watching for a detach event and + * unsubscribing the sequence when one occurs. + *

+ * Note that this will unsubscribe after the first {@link ViewAttachEvent.Kind#DETACH} event is received, + * and will not resume if the view is re-attached later. + * + * @param view the view to bind the source sequence to + * @return a reusable {@link Observable.Transformer} that unsubscribes the source during the View lifecycle + */ + public static Observable.Transformer bindView(final View view) { + if (view == null) { + throw new IllegalArgumentException("View must be given"); + } + + return bindView(RxView.detaches(view)); + } + + /** + * Binds the given source a View lifecycle. + *

+ * Use with {@link Observable#compose(Observable.Transformer)}: + * {@code source.compose(RxLifecycle.bindView(lifecycle)).subscribe()} + *

+ * This helper automatically determines (based on the lifecycle sequence itself) when the source + * should stop emitting items. For views, this effectively means watching for a detach event and + * unsubscribing the sequence when one occurs. Note that this assumes any event + * emitted by the given lifecycle indicates a detach event. + * + * @param lifecycle the lifecycle sequence of a View + * @return a reusable {@link Observable.Transformer} that unsubscribes the source during the View lifecycle + */ + public static Observable.Transformer bindView(final Observable lifecycle) { + if (lifecycle == null) { + throw new IllegalArgumentException("Lifecycle must be given"); + } + + return new Observable.Transformer() { + @Override + public Observable call(Observable source) { + return source.takeUntil(lifecycle); + } + }; + } + private static Observable.Transformer bind(Observable lifecycle, final Func1 correspondingEvents) { if (lifecycle == null) { diff --git a/rxlifecycle/src/test/java/com/trello/rxlifecycle/RxLifecycleTest.java b/rxlifecycle/src/test/java/com/trello/rxlifecycle/RxLifecycleTest.java index 379a3db..2993f1b 100644 --- a/rxlifecycle/src/test/java/com/trello/rxlifecycle/RxLifecycleTest.java +++ b/rxlifecycle/src/test/java/com/trello/rxlifecycle/RxLifecycleTest.java @@ -14,17 +14,27 @@ package com.trello.rxlifecycle; +import android.app.Activity; +import android.view.View; + import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; +import org.robolectric.Robolectric; import org.robolectric.RobolectricTestRunner; import org.robolectric.annotation.Config; + +import java.util.concurrent.CopyOnWriteArrayList; + import rx.Observable; +import rx.Subscription; import rx.observers.TestSubscriber; import rx.subjects.BehaviorSubject; import rx.subjects.PublishSubject; import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; @RunWith(RobolectricTestRunner.class) @Config(manifest = Config.NONE) @@ -225,4 +235,50 @@ public void testThrowsExceptionOutsideFragmentLifecycle() { observable.compose(RxLifecycle.bindFragment(lifecycle)).subscribe(testSubscriber); testSubscriber.assertError(IllegalStateException.class); } + + @Test + public void testBindViewLifecycle() { + BehaviorSubject lifecycle = BehaviorSubject.create(); + Subscription attachSub = observable.compose(RxLifecycle.bindView(lifecycle)).subscribe(); + assertFalse(attachSub.isUnsubscribed()); + lifecycle.onNext(new Object()); + assertTrue(attachSub.isUnsubscribed()); + } + + @Test + public void testBindViewLifecycleOtherObject() { + // Ensures it works with other types as well, and not just "Object" + BehaviorSubject lifecycle = BehaviorSubject.create(); + Subscription attachSub = observable.compose(RxLifecycle.bindView(lifecycle)).subscribe(); + assertFalse(attachSub.isUnsubscribed()); + lifecycle.onNext(""); + assertTrue(attachSub.isUnsubscribed()); + } + + @Test + public void testBindView() { + Activity activity = Robolectric.buildActivity(Activity.class).create().get(); + View view = new View(activity); + CopyOnWriteArrayList listeners = TestUtil.getAttachStateChangeListeners(view); + + // Do the attach notification + if (listeners != null) { + for (View.OnAttachStateChangeListener listener : listeners) { + listener.onViewAttachedToWindow(view); + } + } + + // Subscribe + Subscription viewAttachSub = observable.compose(RxLifecycle.bindView(view)).subscribe(); + assertFalse(viewAttachSub.isUnsubscribed()); + listeners = TestUtil.getAttachStateChangeListeners(view); + assertNotNull(listeners); + assertFalse(listeners.isEmpty()); + + // Now detach + for (View.OnAttachStateChangeListener listener : listeners) { + listener.onViewDetachedFromWindow(view); + } + assertTrue(viewAttachSub.isUnsubscribed()); + } } diff --git a/rxlifecycle/src/test/java/com/trello/rxlifecycle/TestUtil.java b/rxlifecycle/src/test/java/com/trello/rxlifecycle/TestUtil.java new file mode 100644 index 0000000..ca67fd3 --- /dev/null +++ b/rxlifecycle/src/test/java/com/trello/rxlifecycle/TestUtil.java @@ -0,0 +1,37 @@ +/** + * 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.trello.rxlifecycle; + +import android.view.View; + +import org.robolectric.util.ReflectionHelpers; + +import java.util.concurrent.CopyOnWriteArrayList; + +public class TestUtil { + + /** + * Manually retrieve the view's attach state change listeners of an event. Robolectric + * doesn't currently support manually firing these, and it would seem the events are not called + * in normal Robolectric usage either. + * + * @param view View with listeners to notify + */ + static CopyOnWriteArrayList getAttachStateChangeListeners(View view) { + Object listenerInfo = ReflectionHelpers.callInstanceMethod(view, "getListenerInfo"); + return ReflectionHelpers.getField(listenerInfo, "mOnAttachStateChangeListeners"); + } + +}