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

Add bindView implementation and tests #12

Merged
merged 7 commits into from
Aug 29, 2015
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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");
}

}