Skip to content

Commit 0c6b60f

Browse files
committed
Added LifecycleObserver and tests
- Added bindUntilLifecycleEvent(), which automatically unsubscribes when a particular LifecycleEvent is emitted by a lifecycle. - Added bindActivityLifecycle() and bindFragmentLifecycle(), which figure out when you want to bind a subscription until.
1 parent e47f86f commit 0c6b60f

File tree

2 files changed

+374
-0
lines changed

2 files changed

+374
-0
lines changed
Lines changed: 180 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,180 @@
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+
15+
package rx.android.lifecycle;
16+
17+
import rx.Observable;
18+
import rx.functions.Func1;
19+
import rx.functions.Func2;
20+
21+
public class LifecycleObservable {
22+
23+
private LifecycleObservable() {
24+
throw new AssertionError("LifeCycleObservable cannot be instantiated.");
25+
}
26+
27+
/**
28+
* Binds the given source to a lifecycle.
29+
* <p/>
30+
* When the lifecycle event occurs, the source will cease to receive any notifications.
31+
*
32+
* @param lifecycle the lifecycle sequence
33+
* @param source the source sequence
34+
* @param event the event which should conclude notifications to the source
35+
*/
36+
public static <T> Observable<T> bindUntilLifecycleEvent(Observable<LifecycleEvent> lifecycle,
37+
Observable<T> source,
38+
final LifecycleEvent event) {
39+
if (lifecycle == null || source == null) {
40+
throw new IllegalArgumentException("Lifecycle and Observable must be given");
41+
}
42+
43+
return source.lift(
44+
new OperatorSubscribeUntil<T, LifecycleEvent>(
45+
lifecycle.takeFirst(new Func1<LifecycleEvent, Boolean>() {
46+
@Override
47+
public Boolean call(LifecycleEvent lifecycleEvent) {
48+
return lifecycleEvent == event;
49+
}
50+
})
51+
)
52+
);
53+
}
54+
55+
/**
56+
* Binds the given source to an Activity lifecycle.
57+
* <p/>
58+
* This helper automatically determines (based on the lifecycle sequence itself) when it should
59+
* stop sending notifications to the source. In the case that the lifecycle sequence is in the
60+
* creation phase (CREATE, START, etc) it will choose the equivalent destructive phase (DESTROY,
61+
* STOP, etc). If used in the destructive phase, the notifications will cease at the next event;
62+
* for example, if used in PAUSE, it will unsubscribe in STOP.
63+
* <p/>
64+
* Due to the differences between the Activity and Fragment lifecycles, this method should only
65+
* be used for an Activity lifecycle.
66+
*
67+
* @param lifecycle the lifecycle sequence of an Activity
68+
* @param source the source sequence
69+
*/
70+
public static <T> Observable<T> bindActivityLifecycle(Observable<LifecycleEvent> lifecycle, Observable<T> source) {
71+
return bindLifecycle(lifecycle, source, ACTIVITY_LIFECYCLE);
72+
}
73+
74+
/**
75+
* Binds the given source to a Fragment lifecycle.
76+
* <p/>
77+
* This helper automatically determines (based on the lifecycle sequence itself) when it should
78+
* stop sending notifications to the source. In the case that the lifecycle sequence is in the
79+
* creation phase (CREATE, START, etc) it will choose the equivalent destructive phase (DESTROY,
80+
* STOP, etc). If used in the destructive phase, the notifications will cease at the next event;
81+
* for example, if used in PAUSE, it will unsubscribe in STOP.
82+
* <p/>
83+
* Due to the differences between the Activity and Fragment lifecycles, this method should only
84+
* be used for a Fragment lifecycle.
85+
*
86+
* @param lifecycle the lifecycle sequence of a Fragment
87+
* @param source the source sequence
88+
*/
89+
public static <T> Observable<T> bindFragmentLifecycle(Observable<LifecycleEvent> lifecycle, Observable<T> source) {
90+
return bindLifecycle(lifecycle, source, FRAGMENT_LIFECYCLE);
91+
}
92+
93+
private static <T> Observable<T> bindLifecycle(Observable<LifecycleEvent> lifecycle,
94+
Observable<T> source,
95+
Func1<LifecycleEvent, LifecycleEvent> correspondingEvents) {
96+
if (lifecycle == null || source == null) {
97+
throw new IllegalArgumentException("Lifecycle and Observable must be given");
98+
}
99+
100+
// Make sure we're truly comparing a single stream to itself
101+
Observable<LifecycleEvent> sharedLifecycle = lifecycle.share();
102+
103+
// Keep emitting from source until the corresponding event occurs in the lifecycle
104+
return source.lift(
105+
new OperatorSubscribeUntil<T, Boolean>(
106+
Observable.combineLatest(
107+
sharedLifecycle.take(1).map(correspondingEvents),
108+
sharedLifecycle.skip(1),
109+
new Func2<LifecycleEvent, LifecycleEvent, Boolean>() {
110+
@Override
111+
public Boolean call(LifecycleEvent lifecycleEvent, LifecycleEvent bindUntilEvent) {
112+
return lifecycleEvent == bindUntilEvent;
113+
}
114+
})
115+
.takeFirst(new Func1<Boolean, Boolean>() {
116+
@Override
117+
public Boolean call(Boolean shouldComplete) {
118+
return shouldComplete;
119+
}
120+
})
121+
)
122+
);
123+
}
124+
125+
// Figures out which corresponding next lifecycle event in which to unsubscribe, for Activities
126+
private static final Func1<LifecycleEvent, LifecycleEvent> ACTIVITY_LIFECYCLE =
127+
new Func1<LifecycleEvent, LifecycleEvent>() {
128+
@Override
129+
public LifecycleEvent call(LifecycleEvent lastEvent) {
130+
if (lastEvent != null) {
131+
switch (lastEvent) {
132+
case CREATE:
133+
return LifecycleEvent.DESTROY;
134+
case START:
135+
return LifecycleEvent.STOP;
136+
case RESUME:
137+
return LifecycleEvent.PAUSE;
138+
case PAUSE:
139+
return LifecycleEvent.STOP;
140+
case STOP:
141+
return LifecycleEvent.DESTROY;
142+
}
143+
}
144+
145+
throw new IllegalStateException("Cannot bind to Activity lifecycle when outside of it.");
146+
}
147+
};
148+
149+
// Figures out which corresponding next lifecycle event in which to unsubscribe, for Fragments
150+
private static final Func1<LifecycleEvent, LifecycleEvent> FRAGMENT_LIFECYCLE =
151+
new Func1<LifecycleEvent, LifecycleEvent>() {
152+
@Override
153+
public LifecycleEvent call(LifecycleEvent lastEvent) {
154+
if (lastEvent != null) {
155+
switch (lastEvent) {
156+
case ATTACH:
157+
return LifecycleEvent.DETACH;
158+
case CREATE:
159+
return LifecycleEvent.DESTROY;
160+
case CREATE_VIEW:
161+
return LifecycleEvent.DESTROY_VIEW;
162+
case START:
163+
return LifecycleEvent.STOP;
164+
case RESUME:
165+
return LifecycleEvent.PAUSE;
166+
case PAUSE:
167+
return LifecycleEvent.STOP;
168+
case STOP:
169+
return LifecycleEvent.DESTROY_VIEW;
170+
case DESTROY_VIEW:
171+
return LifecycleEvent.DESTROY;
172+
case DESTROY:
173+
return LifecycleEvent.DETACH;
174+
}
175+
}
176+
177+
throw new IllegalStateException("Cannot bind to Fragment lifecycle when outside of it.");
178+
}
179+
};
180+
}
Lines changed: 194 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,194 @@
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+
15+
package rx.android.lifecycle;
16+
17+
import org.junit.Before;
18+
import org.junit.Test;
19+
import org.junit.runner.RunWith;
20+
import org.robolectric.RobolectricTestRunner;
21+
import org.robolectric.annotation.Config;
22+
import rx.Observable;
23+
import rx.Subscription;
24+
import rx.functions.Action1;
25+
import rx.subjects.BehaviorSubject;
26+
import rx.subjects.PublishSubject;
27+
28+
import static org.junit.Assert.assertFalse;
29+
import static org.junit.Assert.assertTrue;
30+
31+
@RunWith(RobolectricTestRunner.class)
32+
@Config(manifest = Config.NONE)
33+
public class LifecycleObservableTest {
34+
35+
private BehaviorSubject<LifecycleEvent> lifecycle;
36+
private Observable<Object> observable;
37+
38+
@Before
39+
public void setup() {
40+
lifecycle = BehaviorSubject.create();
41+
42+
// Simulate an actual lifecycle (hot Observable that does not end)
43+
observable = PublishSubject.create().asObservable();
44+
}
45+
46+
@Test
47+
public void testBindUntilLifecycleEvent() {
48+
Subscription untilStop =
49+
LifecycleObservable.bindUntilLifecycleEvent(lifecycle, observable, LifecycleEvent.STOP).subscribe();
50+
51+
lifecycle.onNext(LifecycleEvent.CREATE);
52+
assertFalse(untilStop.isUnsubscribed());
53+
lifecycle.onNext(LifecycleEvent.START);
54+
assertFalse(untilStop.isUnsubscribed());
55+
lifecycle.onNext(LifecycleEvent.RESUME);
56+
assertFalse(untilStop.isUnsubscribed());
57+
lifecycle.onNext(LifecycleEvent.PAUSE);
58+
assertFalse(untilStop.isUnsubscribed());
59+
lifecycle.onNext(LifecycleEvent.STOP);
60+
assertTrue(untilStop.isUnsubscribed());
61+
}
62+
63+
@Test
64+
public void testBindActivityLifecycle() {
65+
lifecycle.onNext(LifecycleEvent.CREATE);
66+
Subscription createSub = LifecycleObservable.bindActivityLifecycle(lifecycle, observable).subscribe();
67+
68+
lifecycle.onNext(LifecycleEvent.START);
69+
assertFalse(createSub.isUnsubscribed());
70+
Subscription startSub = LifecycleObservable.bindActivityLifecycle(lifecycle, observable).subscribe();
71+
72+
lifecycle.onNext(LifecycleEvent.RESUME);
73+
assertFalse(createSub.isUnsubscribed());
74+
assertFalse(startSub.isUnsubscribed());
75+
Subscription resumeSub = LifecycleObservable.bindActivityLifecycle(lifecycle, observable).subscribe();
76+
77+
lifecycle.onNext(LifecycleEvent.PAUSE);
78+
assertFalse(createSub.isUnsubscribed());
79+
assertFalse(startSub.isUnsubscribed());
80+
assertTrue(resumeSub.isUnsubscribed());
81+
Subscription pauseSub = LifecycleObservable.bindActivityLifecycle(lifecycle, observable).subscribe();
82+
83+
lifecycle.onNext(LifecycleEvent.STOP);
84+
assertFalse(createSub.isUnsubscribed());
85+
assertTrue(startSub.isUnsubscribed());
86+
assertTrue(pauseSub.isUnsubscribed());
87+
Subscription stopSub = LifecycleObservable.bindActivityLifecycle(lifecycle, observable).subscribe();
88+
89+
lifecycle.onNext(LifecycleEvent.DESTROY);
90+
assertTrue(createSub.isUnsubscribed());
91+
assertTrue(stopSub.isUnsubscribed());
92+
}
93+
94+
@Test(expected = RuntimeException.class)
95+
public void testThrowsExceptionOutsideActivityLifecycle() {
96+
lifecycle.onNext(LifecycleEvent.CREATE);
97+
lifecycle.onNext(LifecycleEvent.START);
98+
lifecycle.onNext(LifecycleEvent.RESUME);
99+
lifecycle.onNext(LifecycleEvent.PAUSE);
100+
lifecycle.onNext(LifecycleEvent.STOP);
101+
lifecycle.onNext(LifecycleEvent.DESTROY);
102+
103+
LifecycleObservable.bindActivityLifecycle(lifecycle, observable)
104+
.subscribe(null, new Action1<Throwable>() {
105+
@Override
106+
public void call(Throwable throwable) {
107+
throw new RuntimeException(throwable);
108+
}
109+
});
110+
}
111+
112+
@Test
113+
public void testBindFragmentLifecycle() {
114+
lifecycle.onNext(LifecycleEvent.ATTACH);
115+
Subscription attachSub = LifecycleObservable.bindFragmentLifecycle(lifecycle, observable).subscribe();
116+
117+
lifecycle.onNext(LifecycleEvent.CREATE);
118+
assertFalse(attachSub.isUnsubscribed());
119+
Subscription createSub = LifecycleObservable.bindFragmentLifecycle(lifecycle, observable).subscribe();
120+
121+
lifecycle.onNext(LifecycleEvent.CREATE_VIEW);
122+
assertFalse(attachSub.isUnsubscribed());
123+
assertFalse(createSub.isUnsubscribed());
124+
Subscription createViewSub = LifecycleObservable.bindFragmentLifecycle(lifecycle, observable).subscribe();
125+
126+
lifecycle.onNext(LifecycleEvent.START);
127+
assertFalse(attachSub.isUnsubscribed());
128+
assertFalse(createSub.isUnsubscribed());
129+
assertFalse(createViewSub.isUnsubscribed());
130+
Subscription startSub = LifecycleObservable.bindFragmentLifecycle(lifecycle, observable).subscribe();
131+
132+
lifecycle.onNext(LifecycleEvent.RESUME);
133+
assertFalse(attachSub.isUnsubscribed());
134+
assertFalse(createSub.isUnsubscribed());
135+
assertFalse(createViewSub.isUnsubscribed());
136+
assertFalse(startSub.isUnsubscribed());
137+
Subscription resumeSub = LifecycleObservable.bindFragmentLifecycle(lifecycle, observable).subscribe();
138+
139+
lifecycle.onNext(LifecycleEvent.PAUSE);
140+
assertFalse(attachSub.isUnsubscribed());
141+
assertFalse(createSub.isUnsubscribed());
142+
assertFalse(createViewSub.isUnsubscribed());
143+
assertFalse(startSub.isUnsubscribed());
144+
assertTrue(resumeSub.isUnsubscribed());
145+
Subscription pauseSub = LifecycleObservable.bindFragmentLifecycle(lifecycle, observable).subscribe();
146+
147+
lifecycle.onNext(LifecycleEvent.STOP);
148+
assertFalse(attachSub.isUnsubscribed());
149+
assertFalse(createSub.isUnsubscribed());
150+
assertFalse(createViewSub.isUnsubscribed());
151+
assertTrue(startSub.isUnsubscribed());
152+
assertTrue(pauseSub.isUnsubscribed());
153+
Subscription stopSub = LifecycleObservable.bindFragmentLifecycle(lifecycle, observable).subscribe();
154+
155+
lifecycle.onNext(LifecycleEvent.DESTROY_VIEW);
156+
assertFalse(attachSub.isUnsubscribed());
157+
assertFalse(createSub.isUnsubscribed());
158+
assertTrue(createViewSub.isUnsubscribed());
159+
assertTrue(stopSub.isUnsubscribed());
160+
Subscription destroyViewSub = LifecycleObservable.bindFragmentLifecycle(lifecycle, observable).subscribe();
161+
162+
lifecycle.onNext(LifecycleEvent.DESTROY);
163+
assertFalse(attachSub.isUnsubscribed());
164+
assertTrue(createSub.isUnsubscribed());
165+
assertTrue(destroyViewSub.isUnsubscribed());
166+
Subscription destroySub = LifecycleObservable.bindFragmentLifecycle(lifecycle, observable).subscribe();
167+
168+
lifecycle.onNext(LifecycleEvent.DETACH);
169+
assertTrue(attachSub.isUnsubscribed());
170+
assertTrue(destroySub.isUnsubscribed());
171+
}
172+
173+
@Test(expected = RuntimeException.class)
174+
public void testThrowsExceptionOutsideFragmentLifecycle() {
175+
lifecycle.onNext(LifecycleEvent.ATTACH);
176+
lifecycle.onNext(LifecycleEvent.CREATE);
177+
lifecycle.onNext(LifecycleEvent.CREATE_VIEW);
178+
lifecycle.onNext(LifecycleEvent.START);
179+
lifecycle.onNext(LifecycleEvent.RESUME);
180+
lifecycle.onNext(LifecycleEvent.PAUSE);
181+
lifecycle.onNext(LifecycleEvent.STOP);
182+
lifecycle.onNext(LifecycleEvent.DESTROY_VIEW);
183+
lifecycle.onNext(LifecycleEvent.DESTROY);
184+
lifecycle.onNext(LifecycleEvent.DETACH);
185+
186+
LifecycleObservable.bindFragmentLifecycle(lifecycle, observable)
187+
.subscribe(null, new Action1<Throwable>() {
188+
@Override
189+
public void call(Throwable throwable) {
190+
throw new RuntimeException(throwable);
191+
}
192+
});
193+
}
194+
}

0 commit comments

Comments
 (0)