Skip to content

Commit bc6ad01

Browse files
committed
OperatorWeakBinding to not use WeakReferences anymore
related issues: #1008 #1006 #979
1 parent 3c6fbe0 commit bc6ad01

File tree

8 files changed

+222
-130
lines changed

8 files changed

+222
-130
lines changed

rxjava-contrib/rxjava-android-samples/samples/src/main/AndroidManifest.xml

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,15 @@
2929
<activity
3030
android:name=".ListFragmentActivity">
3131

32+
<intent-filter>
33+
<category android:name="android.intent.category.LAUNCHER"/>
34+
<category android:name="android.intent.category.DEFAULT"/>
35+
<action android:name="android.intent.action.MAIN"/>
36+
</intent-filter>
37+
</activity>
38+
<activity
39+
android:name=".ListenInOutActivity">
40+
3241
<intent-filter>
3342
<category android:name="android.intent.category.LAUNCHER"/>
3443
<category android:name="android.intent.category.DEFAULT"/>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
package com.netflix.rxjava.android.samples;
2+
3+
import android.app.Activity;
4+
import android.os.Bundle;
5+
import android.view.View;
6+
import android.widget.TextView;
7+
import android.widget.Toast;
8+
import android.widget.ToggleButton;
9+
10+
import rx.Observable;
11+
import rx.Observer;
12+
import rx.Subscription;
13+
import rx.observables.ConnectableObservable;
14+
15+
import static rx.android.observables.AndroidObservable.bindActivity;
16+
17+
/**
18+
* Activity that binds to a counting sequence and is able to listen in and out to that
19+
* sequence by pressing a toggle button. The button disables itself once the sequence
20+
* finishes.
21+
*/
22+
public class ListenInOutActivity extends Activity implements Observer<String> {
23+
24+
private Observable<String> source;
25+
private Subscription subscription;
26+
private TextView textView;
27+
28+
@Override
29+
protected void onCreate(Bundle savedInstanceState) {
30+
super.onCreate(savedInstanceState);
31+
32+
setContentView(R.layout.listen_in_out_activity);
33+
34+
textView = (TextView) findViewById(android.R.id.text1);
35+
36+
// in a production app, you would use dependency injection, fragments, or other
37+
// means to preserve the observable, but this will suffice here
38+
source = (Observable<String>) getLastNonConfigurationInstance();
39+
if (source == null) {
40+
source = SampleObservables.numberStrings(1, 100, 200).publish();
41+
((ConnectableObservable) source).connect();
42+
}
43+
44+
subscribeToSequence();
45+
}
46+
47+
private void subscribeToSequence() {
48+
subscription = bindActivity(this, source).subscribe(this);
49+
}
50+
51+
@Override
52+
public Object onRetainNonConfigurationInstance() {
53+
return source;
54+
}
55+
56+
@Override
57+
protected void onDestroy() {
58+
subscription.unsubscribe();
59+
super.onDestroy();
60+
}
61+
62+
@Override
63+
public void onCompleted() {
64+
TextView button = (TextView) findViewById(R.id.toggle_button);
65+
button.setText("Completed");
66+
button.setEnabled(false);
67+
}
68+
69+
@Override
70+
public void onError(Throwable e) {
71+
e.printStackTrace();
72+
Toast.makeText(this, "Error: " + e, Toast.LENGTH_SHORT).show();
73+
}
74+
75+
@Override
76+
public void onNext(String s) {
77+
textView.setText(s);
78+
}
79+
80+
public void onSequenceToggleClicked(View view) {
81+
if (((ToggleButton) view).isChecked()) {
82+
subscription.unsubscribe();
83+
} else {
84+
subscribeToSequence();
85+
}
86+
}
87+
}

rxjava-contrib/rxjava-android-samples/samples/src/main/java/com/netflix/rxjava/android/samples/ListeningFragmentActivity.java

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
import rx.observables.ConnectableObservable;
1515
import rx.subscriptions.Subscriptions;
1616

17-
import static rx.android.schedulers.AndroidSchedulers.mainThread;
17+
import static rx.android.observables.AndroidObservable.bindFragment;
1818

1919
/**
2020
* Problem:
@@ -52,7 +52,7 @@ public ListeningFragment() {
5252
public void onCreate(Bundle savedInstanceState) {
5353
super.onCreate(savedInstanceState);
5454

55-
strings = SampleObservables.numberStrings(1, 50, 250).observeOn(mainThread()).publish();
55+
strings = SampleObservables.numberStrings(1, 50, 250).publish();
5656
strings.connect(); // trigger the sequence
5757
}
5858

@@ -74,7 +74,7 @@ public void onViewCreated(final View view, Bundle savedInstanceState) {
7474
final TextView textView = (TextView) view.findViewById(android.R.id.text1);
7575

7676
// re-connect to sequence
77-
subscription = strings.subscribe(new Subscriber<String>() {
77+
subscription = bindFragment(this, strings).subscribe(new Subscriber<String>() {
7878

7979
@Override
8080
public void onCompleted() {

rxjava-contrib/rxjava-android-samples/samples/src/main/java/com/netflix/rxjava/android/samples/RetainedFragmentActivity.java

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -14,11 +14,12 @@
1414

1515
import rx.Observable;
1616
import rx.Subscription;
17-
import rx.android.schedulers.AndroidSchedulers;
1817
import rx.functions.Action1;
1918
import rx.functions.Func1;
2019
import rx.subscriptions.Subscriptions;
2120

21+
import static rx.android.observables.AndroidObservable.bindFragment;
22+
2223
/**
2324
* Problem:
2425
* You have a data source (where that data is potentially expensive to obtain), and you want to
@@ -68,9 +69,7 @@ public void onCreate(Bundle savedInstanceState) {
6869
super.onCreate(savedInstanceState);
6970

7071
// simulate fetching a JSON document with a latency of 2 seconds
71-
strings = SampleObservables.fakeApiCall(2000).map(PARSE_JSON)
72-
.observeOn(AndroidSchedulers.mainThread())
73-
.cache();
72+
strings = SampleObservables.fakeApiCall(2000).map(PARSE_JSON).cache();
7473
}
7574

7675
@Override
@@ -93,7 +92,7 @@ public void onViewCreated(final View view, Bundle savedInstanceState) {
9392

9493
// (re-)subscribe to the sequence, which either emits the cached result or simply re-
9594
// attaches the subscriber to wait for it to arrive
96-
subscription = strings.subscribe(new Action1<String>() {
95+
subscription = bindFragment(this, strings).subscribe(new Action1<String>() {
9796
@Override
9897
public void call(String result) {
9998
textView.setText(result);
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
2+
xmlns:tools="http://schemas.android.com/tools"
3+
android:layout_width="match_parent"
4+
android:layout_height="match_parent"
5+
android:paddingLeft="@dimen/activity_horizontal_margin"
6+
android:paddingRight="@dimen/activity_horizontal_margin"
7+
android:paddingTop="@dimen/activity_vertical_margin"
8+
android:paddingBottom="@dimen/activity_vertical_margin"
9+
tools:context="com.netflix.rxjava.android.samples.ListenInOutActivity">
10+
11+
<TextView
12+
android:id="@android:id/text1"
13+
android:layout_centerInParent="true"
14+
android:layout_width="wrap_content"
15+
android:layout_height="wrap_content" />
16+
17+
<ToggleButton
18+
android:id="@+id/toggle_button"
19+
android:layout_width="wrap_content"
20+
android:layout_height="wrap_content"
21+
android:layout_below="@android:id/text1"
22+
android:layout_centerHorizontal="true"
23+
android:textOff="Pause"
24+
android:textOn="Resume"
25+
android:onClick="onSequenceToggleClicked" />
26+
27+
28+
</RelativeLayout>

rxjava-contrib/rxjava-android/src/main/java/rx/android/observables/AndroidObservable.java

Lines changed: 19 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -51,15 +51,15 @@ public Boolean call(Activity activity) {
5151
private static final Func1<Fragment, Boolean> FRAGMENT_VALIDATOR = new Func1<Fragment, Boolean>() {
5252
@Override
5353
public Boolean call(Fragment fragment) {
54-
return fragment.isAdded();
54+
return fragment.isAdded() && !fragment.getActivity().isFinishing();
5555
}
5656
};
5757

5858
private static final Func1<android.support.v4.app.Fragment, Boolean> FRAGMENTV4_VALIDATOR =
5959
new Func1<android.support.v4.app.Fragment, Boolean>() {
6060
@Override
6161
public Boolean call(android.support.v4.app.Fragment fragment) {
62-
return fragment.isAdded();
62+
return fragment.isAdded() && !fragment.getActivity().isFinishing();
6363
}
6464
};
6565

@@ -131,11 +131,15 @@ public static <T> Observable<T> fromFragment(Object fragment, Observable<T> sour
131131
}
132132

133133
/**
134-
* Binds the given source sequence to the life-cycle of an activity.
134+
* Binds the given source sequence to an activity.
135135
* <p/>
136136
* This helper will schedule the given sequence to be observed on the main UI thread and ensure
137-
* that no notifications will be forwarded to the activity in case it gets destroyed by the Android runtime
138-
* or garbage collected by the VM.
137+
* that no notifications will be forwarded to the activity in case it is scheduled to finish.
138+
* <p/>
139+
* You should unsubscribe from the returned Observable in onDestroy at the latest, in order to not
140+
* leak the activity or an inner subscriber. Conversely, when the source sequence can outlive the activity,
141+
* make sure to bind to new instances of the activity again, e.g. after going through configuration changes.
142+
* Refer to the samples project for actual examples.
139143
*
140144
* @param activity the activity to bind the source sequence to
141145
* @param source the source sequence
@@ -146,24 +150,28 @@ public static <T> Observable<T> bindActivity(Activity activity, Observable<T> so
146150
}
147151

148152
/**
149-
* Binds the given source sequence to the life-cycle of a fragment (native or support-v4).
153+
* Binds the given source sequence to a fragment (native or support-v4).
150154
* <p/>
151155
* This helper will schedule the given sequence to be observed on the main UI thread and ensure
152156
* that no notifications will be forwarded to the fragment in case it gets detached from its
153-
* activity or garbage collected by the VM.
157+
* activity or the activity is scheduled to finish.
158+
* <p/>
159+
* You should unsubscribe from the returned Observable in onDestroy for normal fragments, or in onDestroyView
160+
* for retained fragments, in order to not leak any references to the host activity or the fragment.
161+
* Refer to the samples project for actual examples.
154162
*
155163
* @param fragment the fragment to bind the source sequence to
156164
* @param source the source sequence
157165
*/
158-
public static <T> Observable<T> bindFragment(Object fragment, Observable<T> cachedSequence) {
166+
public static <T> Observable<T> bindFragment(Object fragment, Observable<T> source) {
159167
Assertions.assertUiThread();
160-
final Observable<T> source = cachedSequence.observeOn(mainThread());
168+
final Observable<T> o = source.observeOn(mainThread());
161169
if (USES_SUPPORT_FRAGMENTS && fragment instanceof android.support.v4.app.Fragment) {
162170
android.support.v4.app.Fragment f = (android.support.v4.app.Fragment) fragment;
163-
return source.lift(new OperatorWeakBinding<T, android.support.v4.app.Fragment>(f, FRAGMENTV4_VALIDATOR));
171+
return o.lift(new OperatorWeakBinding<T, android.support.v4.app.Fragment>(f, FRAGMENTV4_VALIDATOR));
164172
} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB && fragment instanceof Fragment) {
165173
Fragment f = (Fragment) fragment;
166-
return source.lift(new OperatorWeakBinding<T, Fragment>(f, FRAGMENT_VALIDATOR));
174+
return o.lift(new OperatorWeakBinding<T, Fragment>(f, FRAGMENT_VALIDATOR));
167175
} else {
168176
throw new IllegalArgumentException("Target fragment is neither a native nor support library Fragment");
169177
}

0 commit comments

Comments
 (0)