Skip to content

Commit d630d00

Browse files
committed
Merge pull request ReactiveX#53 from omo/bindView
Add AndroidObservable#bindView()
2 parents 2c9a406 + c5f534e commit d630d00

File tree

3 files changed

+236
-0
lines changed

3 files changed

+236
-0
lines changed

rxandroid/src/main/java/rx/android/observables/AndroidObservable.java

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,12 +21,14 @@
2121
import android.content.SharedPreferences;
2222
import android.os.Build;
2323
import android.os.Handler;
24+
import android.view.View;
2425

2526
import rx.Observable;
2627
import rx.android.operators.OperatorBroadcastRegister;
2728
import rx.android.operators.OperatorConditionalBinding;
2829
import rx.android.operators.OperatorLocalBroadcastRegister;
2930
import rx.android.operators.OperatorSharedPreferenceChange;
31+
import rx.android.operators.OperatorViewDetachedFromWindowFirst;
3032
import rx.functions.Func1;
3133

3234
import static rx.android.schedulers.AndroidSchedulers.mainThread;
@@ -118,6 +120,27 @@ public static <T> Observable<T> bindFragment(Object fragment, Observable<T> sour
118120
}
119121
}
120122

123+
/**
124+
* Binds the given source sequence to the view.
125+
* <p>
126+
* This helper will schedule the given sequence to be observed on the main UI thread and ensure
127+
* that no notifications will be forwarded to the view in case it gets detached from its the window.
128+
* <p>
129+
* Unlike {@link #bindActivity} or {@link #bindFragment}, you don't have to unsubscribe the returned {@code Observable}
130+
* on the detachment. {@link #bindView} does it automatically.
131+
* That means that the subscriber doesn't see further sequence even if the view is recycled and
132+
* attached again.
133+
*
134+
* @param view the view to bind the source sequence to
135+
* @param source the source sequence
136+
*/
137+
public static <T> Observable<T> bindView(View view, Observable<T> source) {
138+
if (view == null || source == null)
139+
throw new IllegalArgumentException("View and Observable must be given");
140+
Assertions.assertUiThread();
141+
return source.takeUntil(Observable.create(new OperatorViewDetachedFromWindowFirst(view))).observeOn(mainThread());
142+
}
143+
121144
/**
122145
* Create Observable that wraps BroadcastReceiver and emmit received intents.
123146
*
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
/**
2+
* Licensed under the Apache License, Version 2.0 (the "License");
3+
* you may not use this file except in compliance with the License.
4+
* You may obtain a copy of the License at
5+
*
6+
* http://www.apache.org/licenses/LICENSE-2.0
7+
*
8+
* Unless required by applicable law or agreed to in writing, software
9+
* distributed under the License is distributed on an "AS IS" BASIS,
10+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11+
* See the License for the specific language governing permissions and
12+
* limitations under the License.
13+
*/
14+
package rx.android.operators;
15+
16+
import android.view.View;
17+
18+
import rx.Observable;
19+
import rx.Subscriber;
20+
import rx.Subscription;
21+
22+
/**
23+
* An internal class that is used from #{@link rx.android.observables.AndroidObservable#bindView}.
24+
* This emits an event when the given #{@code View} is detached from the window for the first time.
25+
*/
26+
public class OperatorViewDetachedFromWindowFirst implements Observable.OnSubscribe<View> {
27+
private final View view;
28+
29+
public OperatorViewDetachedFromWindowFirst(View view) {
30+
this.view = view;
31+
}
32+
33+
@Override
34+
public void call(final Subscriber<? super View> subscriber) {
35+
new ListenerSubscription(subscriber, view);
36+
}
37+
38+
// This could be split into a couple of anonymous classes.
39+
// We pack it into one for the sake of memory efficiency.
40+
private static class ListenerSubscription implements View.OnAttachStateChangeListener, Subscription {
41+
private Subscriber<? super View> subscriber;
42+
private View view;
43+
44+
public ListenerSubscription(Subscriber<? super View> subscriber, View view) {
45+
this.subscriber = subscriber;
46+
this.view = view;
47+
view.addOnAttachStateChangeListener(this);
48+
subscriber.add(this);
49+
}
50+
51+
@Override
52+
public void onViewAttachedToWindow(View v) {
53+
}
54+
55+
@Override
56+
public void onViewDetachedFromWindow(View v) {
57+
if (!isUnsubscribed()) {
58+
Subscriber<? super View> originalSubscriber = subscriber;
59+
clear();
60+
originalSubscriber.onNext(v);
61+
originalSubscriber.onCompleted();
62+
}
63+
}
64+
65+
@Override
66+
public void unsubscribe() {
67+
if (!isUnsubscribed()) {
68+
clear();
69+
}
70+
}
71+
72+
@Override
73+
public boolean isUnsubscribed() {
74+
return view == null;
75+
}
76+
77+
private void clear() {
78+
view.removeOnAttachStateChangeListener(this);
79+
view = null;
80+
subscriber = null;
81+
}
82+
}
83+
}
Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
package rx.android.observables;
2+
3+
import static org.mockito.Mockito.verify;
4+
import static org.mockito.Mockito.verifyNoMoreInteractions;
5+
6+
import android.app.Activity;
7+
import android.view.View;
8+
import android.widget.FrameLayout;
9+
10+
import junit.framework.Assert;
11+
12+
import org.junit.Before;
13+
import org.junit.Test;
14+
import org.junit.runner.RunWith;
15+
import org.mockito.Mock;
16+
import org.mockito.MockitoAnnotations;
17+
import org.robolectric.Robolectric;
18+
import org.robolectric.RobolectricTestRunner;
19+
import org.robolectric.annotation.Config;
20+
21+
import java.util.concurrent.atomic.AtomicBoolean;
22+
23+
import rx.Observer;
24+
import rx.Subscription;
25+
import rx.subjects.PublishSubject;
26+
27+
@RunWith(RobolectricTestRunner.class)
28+
@Config(manifest = Config.NONE)
29+
public class BindViewTest {
30+
31+
private Activity activity;
32+
private FrameLayout contentView;
33+
private View target;
34+
35+
@Mock
36+
private Observer<String> observer;
37+
private PublishSubject<String> subject;
38+
39+
@Before
40+
public void setup() {
41+
MockitoAnnotations.initMocks(this);
42+
subject = PublishSubject.create();
43+
activity = Robolectric.buildActivity(Activity.class).create().visible().get();
44+
contentView = new FrameLayout(activity);
45+
activity.setContentView(contentView);
46+
target = new View(activity);
47+
}
48+
49+
@Test
50+
public void viewIsNotifiedEvenBeforeAttach() {
51+
AndroidObservable.bindView(target, subject).subscribe(observer);
52+
53+
subject.onNext("hello");
54+
subject.onCompleted();
55+
56+
verify(observer).onNext("hello");
57+
verify(observer).onCompleted();
58+
}
59+
60+
@Test
61+
public void attachedViewIsNotified() {
62+
AndroidObservable.bindView(target, subject).subscribe(observer);
63+
contentView.addView(target);
64+
65+
subject.onNext("hello");
66+
subject.onCompleted();
67+
68+
verify(observer).onNext("hello");
69+
verify(observer).onCompleted();
70+
}
71+
72+
@Test
73+
public void detachedViewIsNotNotified() {
74+
AndroidObservable.bindView(target, subject).subscribe(observer);
75+
contentView.addView(target);
76+
contentView.removeView(target);
77+
78+
subject.onNext("hello");
79+
subject.onCompleted();
80+
81+
// No onNext() here.
82+
verify(observer).onCompleted();
83+
}
84+
85+
@Test
86+
public void recycledViewIsNotNotified() {
87+
AndroidObservable.bindView(target, subject).subscribe(observer);
88+
contentView.addView(target);
89+
contentView.removeView(target);
90+
contentView.addView(target);
91+
92+
subject.onNext("hello");
93+
subject.onCompleted();
94+
95+
// No onNext() here.
96+
verify(observer).onCompleted();
97+
}
98+
99+
@Test
100+
public void unsubscribeStopsNotifications() {
101+
Subscription subscription = AndroidObservable.bindView(target, subject).subscribe(observer);
102+
contentView.addView(target);
103+
104+
subscription.unsubscribe();
105+
106+
subject.onNext("hello");
107+
subject.onCompleted();
108+
contentView.removeView(target);
109+
110+
verifyNoMoreInteractions(observer);
111+
}
112+
113+
@Test
114+
public void earlyUnsubscribeStopsNotifications() {
115+
Subscription subscription = AndroidObservable.bindView(target, subject).subscribe(observer);
116+
subscription.unsubscribe();
117+
118+
contentView.addView(target);
119+
subject.onNext("hello");
120+
subject.onCompleted();
121+
contentView.removeView(target);
122+
123+
verifyNoMoreInteractions(observer);
124+
}
125+
126+
@Test(expected = IllegalArgumentException.class)
127+
public void nullViewIsNotAllowed() {
128+
AndroidObservable.bindView(null, subject);
129+
}
130+
}

0 commit comments

Comments
 (0)