Skip to content

Commit e3cf42a

Browse files
committed
Refactoring OperatorViewDetachedFromWindowFirst
1. Renaming to OnSubscribeViewDetachedFromWindowFirst.java 2. Adding tests to assert the desired behaviour 3. Refactoring to avoid the "instantiate and forget" approach within OnSubscribe.call(..) Signed-off-by: David Marques <[email protected]>
1 parent 0893101 commit e3cf42a

File tree

3 files changed

+100
-17
lines changed

3 files changed

+100
-17
lines changed

rxandroid/src/main/java/rx/android/view/OperatorViewDetachedFromWindowFirst.java renamed to rxandroid/src/main/java/rx/android/view/OnSubscribeViewDetachedFromWindowFirst.java

Lines changed: 13 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -20,32 +20,33 @@
2020
import rx.Subscription;
2121

2222
/**
23-
* An internal class that is used from #{@link ViewObservable#bindView}.
23+
* An internal class that is used from #{@link rx.android.view.ViewObservable#bindView}.
2424
* This emits an event when the given #{@code View} is detached from the window for the first time.
2525
*/
26-
final class OperatorViewDetachedFromWindowFirst implements Observable.OnSubscribe<View> {
26+
public class OnSubscribeViewDetachedFromWindowFirst implements Observable.OnSubscribe<View> {
2727
private final View view;
2828

29-
public OperatorViewDetachedFromWindowFirst(View view) {
29+
public OnSubscribeViewDetachedFromWindowFirst(View view) {
3030
this.view = view;
3131
}
3232

3333
@Override
3434
public void call(final Subscriber<? super View> subscriber) {
35-
new ListenerSubscription(subscriber, view);
35+
final SubscriptionAdapter adapter = new SubscriptionAdapter(subscriber, view);
36+
subscriber.add(adapter);
37+
view.addOnAttachStateChangeListener(adapter);
3638
}
3739

3840
// This could be split into a couple of anonymous classes.
3941
// We pack it into one for the sake of memory efficiency.
40-
private static class ListenerSubscription implements View.OnAttachStateChangeListener, Subscription {
42+
private static class SubscriptionAdapter implements View.OnAttachStateChangeListener,
43+
Subscription {
4144
private Subscriber<? super View> subscriber;
4245
private View view;
4346

44-
public ListenerSubscription(Subscriber<? super View> subscriber, View view) {
47+
public SubscriptionAdapter(Subscriber<? super View> subscriber, View view) {
4548
this.subscriber = subscriber;
4649
this.view = view;
47-
view.addOnAttachStateChangeListener(this);
48-
subscriber.add(this);
4950
}
5051

5152
@Override
@@ -56,7 +57,7 @@ public void onViewAttachedToWindow(View v) {
5657
public void onViewDetachedFromWindow(View v) {
5758
if (!isUnsubscribed()) {
5859
Subscriber<? super View> originalSubscriber = subscriber;
59-
clear();
60+
unsubscribe();
6061
originalSubscriber.onNext(v);
6162
originalSubscriber.onCompleted();
6263
}
@@ -65,19 +66,15 @@ public void onViewDetachedFromWindow(View v) {
6566
@Override
6667
public void unsubscribe() {
6768
if (!isUnsubscribed()) {
68-
clear();
69+
view.removeOnAttachStateChangeListener(this);
70+
view = null;
71+
subscriber = null;
6972
}
7073
}
7174

7275
@Override
7376
public boolean isUnsubscribed() {
7477
return view == null;
7578
}
76-
77-
private void clear() {
78-
view.removeOnAttachStateChangeListener(this);
79-
view = null;
80-
subscriber = null;
81-
}
8279
}
8380
}

rxandroid/src/main/java/rx/android/view/ViewObservable.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,6 @@ public static <T> Observable<T> bindView(View view, Observable<T> source) {
5151
if (view == null || source == null)
5252
throw new IllegalArgumentException("View and Observable must be given");
5353
Assertions.assertUiThread();
54-
return source.takeUntil(Observable.create(new OperatorViewDetachedFromWindowFirst(view))).observeOn(mainThread());
54+
return source.takeUntil(Observable.create(new OnSubscribeViewDetachedFromWindowFirst(view))).observeOn(mainThread());
5555
}
5656
}
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
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 junit.framework.Assert;
19+
20+
import org.junit.Test;
21+
import org.junit.runner.RunWith;
22+
import org.mockito.ArgumentCaptor;
23+
import org.mockito.Matchers;
24+
import org.robolectric.RobolectricTestRunner;
25+
26+
import rx.Observable;
27+
import rx.Subscriber;
28+
import rx.android.view.OnSubscribeViewDetachedFromWindowFirst;
29+
import rx.observers.TestSubscriber;
30+
31+
import static org.mockito.Mockito.mock;
32+
import static org.mockito.Mockito.never;
33+
import static org.mockito.Mockito.spy;
34+
import static org.mockito.Mockito.verify;
35+
36+
@RunWith(RobolectricTestRunner.class)
37+
public class OnSubscribeViewDetachedFromWindowFirstTest {
38+
39+
@Test
40+
public void testGivenSubscriptionWhenViewDetachedThenUnsubscribesAndRemovesListener() {
41+
final Subscriber<View> subscriber = spy(new TestSubscriber<View>());
42+
final View view = mock(View.class);
43+
final Observable<View> observable = Observable.create(new OnSubscribeViewDetachedFromWindowFirst(view));
44+
observable.subscribe(subscriber);
45+
46+
verify(subscriber, never()).onNext(view);
47+
verify(subscriber, never()).onError(Matchers.any(Throwable.class));
48+
verify(subscriber, never()).onCompleted();
49+
50+
final ArgumentCaptor<View.OnAttachStateChangeListener> captor =
51+
ArgumentCaptor.forClass(View.OnAttachStateChangeListener.class);
52+
verify(view).addOnAttachStateChangeListener(captor.capture());
53+
54+
final View.OnAttachStateChangeListener listener = captor.getValue();
55+
Assert.assertNotNull("Should have added listener on subscription.", listener);
56+
57+
listener.onViewDetachedFromWindow(view);
58+
59+
verify(subscriber, never()).onError(Matchers.any(Throwable.class));
60+
verify(subscriber).onNext(view);
61+
verify(subscriber).onCompleted();
62+
63+
verify(view).removeOnAttachStateChangeListener(listener);
64+
}
65+
66+
@Test
67+
public void testGivenSubscriptionWhenUnsubscribedThenStateListenerRemoved() {
68+
final Subscriber<View> subscriber = spy(new TestSubscriber<View>());
69+
final View view = mock(View.class);
70+
final Observable<View> observable = Observable.create(new OnSubscribeViewDetachedFromWindowFirst(view));
71+
observable.subscribe(subscriber).unsubscribe();
72+
73+
verify(subscriber, never()).onNext(view);
74+
verify(subscriber, never()).onError(Matchers.any(Throwable.class));
75+
verify(subscriber, never()).onCompleted();
76+
77+
final ArgumentCaptor<View.OnAttachStateChangeListener> captor =
78+
ArgumentCaptor.forClass(View.OnAttachStateChangeListener.class);
79+
verify(view).addOnAttachStateChangeListener(captor.capture());
80+
81+
final View.OnAttachStateChangeListener listener = captor.getValue();
82+
Assert.assertNotNull("Should have added listener on subscription.", listener);
83+
84+
verify(view).removeOnAttachStateChangeListener(listener);
85+
}
86+
}

0 commit comments

Comments
 (0)