Skip to content

Commit

Permalink
Merge pull request #12 from hzsweers/z/bindview
Browse files Browse the repository at this point in the history
Add bindView implementation and tests
  • Loading branch information
dlew committed Aug 29, 2015
2 parents 44f8e76 + e8fdbdb commit 308093e
Show file tree
Hide file tree
Showing 5 changed files with 152 additions and 1 deletion.
3 changes: 2 additions & 1 deletion build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -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'
}
}
1 change: 1 addition & 0 deletions rxlifecycle/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ repositories {

dependencies {
compile rootProject.ext.rxJava
compile rootProject.ext.rxBinding

testCompile rootProject.ext.junit
testCompile rootProject.ext.mockito
Expand Down
56 changes: 56 additions & 0 deletions rxlifecycle/src/main/java/com/trello/rxlifecycle/RxLifecycle.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -135,6 +140,57 @@ public static <T> Observable.Transformer<T, T> bindFragment(Observable<FragmentE
return bind(lifecycle, FRAGMENT_LIFECYCLE);
}

/**
* Binds the given source a View lifecycle.
* <p>
* Use with {@link Observable#compose(Observable.Transformer)}:
* {@code source.compose(RxLifecycle.bindView(lifecycle)).subscribe()}
* <p>
* 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.
* <p>
* 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 <T> Observable.Transformer<T, T> 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.
* <p>
* Use with {@link Observable#compose(Observable.Transformer)}:
* {@code source.compose(RxLifecycle.bindView(lifecycle)).subscribe()}
* <p>
* 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 <em>any</em> 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 <T, E> Observable.Transformer<T, T> bindView(final Observable<? extends E> lifecycle) {
if (lifecycle == null) {
throw new IllegalArgumentException("Lifecycle must be given");
}

return new Observable.Transformer<T, T>() {
@Override
public Observable<T> call(Observable<T> source) {
return source.takeUntil(lifecycle);
}
};
}

private static <T, R> Observable.Transformer<T, T> bind(Observable<R> lifecycle,
final Func1<R, R> correspondingEvents) {
if (lifecycle == null) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -225,4 +235,50 @@ public void testThrowsExceptionOutsideFragmentLifecycle() {
observable.compose(RxLifecycle.bindFragment(lifecycle)).subscribe(testSubscriber);
testSubscriber.assertError(IllegalStateException.class);
}

@Test
public void testBindViewLifecycle() {
BehaviorSubject<Object> 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<String> 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<View.OnAttachStateChangeListener> 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());
}
}
37 changes: 37 additions & 0 deletions rxlifecycle/src/test/java/com/trello/rxlifecycle/TestUtil.java
Original file line number Diff line number Diff line change
@@ -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<View.OnAttachStateChangeListener> getAttachStateChangeListeners(View view) {
Object listenerInfo = ReflectionHelpers.callInstanceMethod(view, "getListenerInfo");
return ReflectionHelpers.getField(listenerInfo, "mOnAttachStateChangeListeners");
}

}

0 comments on commit 308093e

Please sign in to comment.