-
Notifications
You must be signed in to change notification settings - Fork 7.6k
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
OperatorObserveFromAndroidComponent to support Fragment.isVisible() #899
Comments
So basically, a callback to indicate if the fragment is active can improve the API. What's your opinion, @mttkay
|
Here is my thought that will solve this issue and another issue I mentioned in #880 (comment) Essentially, we need some api to make an Observable become inactive and keep silent (we can not use At first, we need the following public final class OperatorAttach<T> implements Observable.OnSubscribe<T> {
private final Observable<T> source;
private final Func1<Notification<T>, Boolean> isAttached;
public OperatorAttach(Observable<T> source, final Subscription detach) {
this(source, new Func1<Notification<T>, Boolean>() {
@Override
public Boolean call(Notification<T> t1) {
return !detach.isUnsubscribed();
}
});
}
/**
* Generate a new Observable that mirrors the source but will swallow messages once isAttached return false.
*
* @param source
* @param isAttached
*/
public OperatorAttach(Observable<T> source, Func1<Notification<T>, Boolean> isAttached) {
this.source = source;
this.isAttached = isAttached;
}
@Override
public void call(final Subscriber<? super T> subscriber) {
source.materialize().takeWhile(isAttached).subscribe(new Subscriber<Notification<T>>(subscriber) {
@Override
public void onCompleted() {
// ignore
}
@Override
public void onError(Throwable e) {
// ignore
}
@Override
public void onNext(Notification<T> notification) {
if (notification.isOnNext()) {
subscriber.onNext(notification.getValue());
} else if (notification.isOnError()) {
subscriber.onError(notification.getThrowable());
} else {
subscriber.onCompleted();
}
}
});
}
} Then, public static <T> Observable<T> observeFromAndroidComponent(Observable<T> source,
android.app.Fragment fragment, final Func1<android.app.Fragment, Boolean> isAttached) {
final WeakReference<android.app.Fragment> fragmentRef;
Object memoryBarrier = new Object();
synchronized (memoryBarrier) { // force a memory barrier
fragmentRef = new WeakReference<android.app.Fragment>(fragment);
}
return Observable.create(new OperatorAttach<T>(source.observeOn(AndroidSchedulers.mainThread()),
new Func1<Notification<T>, Boolean>() {
@Override
public Boolean call(Notification<T> t1) {
android.app.Fragment fragment = fragmentRef.get();
return fragment != null && isAttached.call(fragment);
}
}));
}
public static <T> Observable<T> observeFromAndroidComponent(Observable<T> source, Subscription detach) {
return Observable
.create(new OperatorAttach<T>(source.observeOn(AndroidSchedulers.mainThread()), detach));
} Use case:
public class TestActivity extends Activity {
private volatile Subscription detach;
private Observable<Integer> o;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
detach = new BooleanSubscription();
o = OperatorObserveFromAndroidComponent.observeFromAndroidComponent(
Observable.from(Arrays.asList(1, 2, 3)), detach);
}
@Override
protected void onDestroy() {
detach.unsubscribe();
super.onDestroy();
}
}
android.app.Fragment fragment = ...;
OperatorObserveFromAndroidComponent.observeFromAndroidComponent(
Observable.from(Arrays.asList(1, 2, 3)), fragment, new Func1<android.app.Fragment, Boolean>() {
@Override
public Boolean call(Fragment t1) {
return t1.isAdded(); // or t1.isVisible();
}
}); @mttkay, any idea? |
@zsxwing, my guess is that |
@mironov-nsk , I think creating two operator (one use |
@benjchristensen is it worth to put such |
@zsxwing I came to a similar conclusion with my initial forked implementation - I actually ended up needing a slightly custom
|
I just realized I did the same thing as
|
I don't fully understand the use case as I don't work with UIs or Android so can't help very much, but are you trying to unsubscribe from the event source when it is hidden, or just ignore them until it becomes visible? In other words, must you keep listening to the events so as to know when it becomes visible again? |
If I understand correctly, if the visibility is not taken into account we receive events when hidden?
I imagine you want this?
public static Observable<FragmentEvent> listenToFragment(final Fragment fragment) {
// simulate registering with the listener for a fragment
return Observable.create(new OnSubscribe<FragmentEvent>() {
@Override
public void call(final Subscriber<? super FragmentEvent> s) {
s.add(Schedulers.newThread().scheduleRecursive(new Action1<Recurse>() {
boolean hidden = false;
@Override
public void call(Recurse r) {
if (Math.random() < 0.2) {
hidden = !hidden;
if (hidden) {
s.onNext(new FragmentEvent(fragment, "hidden"));
} else {
s.onNext(new FragmentEvent(fragment, "visible"));
}
} else {
s.onNext(new FragmentEvent(fragment, "touch event"));
}
r.schedule(500, TimeUnit.MILLISECONDS);
}
}));
}
});
}
public static void main(String[] args) {
listenToFragment(new Fragment("f1")).toBlockingObservable().forEach(new Action1<FragmentEvent>() {
boolean visible = true;
@Override
public void call(FragmentEvent e) {
if (e.event.equals("hidden")) {
visible = false;
} else if (e.event.equals("visible")) {
visible = true;
}
System.out.println("e: " + e.event + " [" + (visible ? "visible" : "hidden") + "]");
}
});
} |
I want to ignore the events of the observable while the fragment is hidden. However, as I mentioned, actually whether or not it is hidden is not a property but a custom function. Therefore, as @zsxwing suggested, there would need to be a boolean function In the The usage of the code in question would end up something like this: Observable<String> src = ...;
android.app.Fragment fragment = ...;
Observable<String> filteredSource =
OperatorObserveFromAndroidComponent.observeFromAndroidComponent(
src, fragment, new Func1<android.app.Fragment, Boolean>() {
@Override
public Boolean call(Fragment t1) {
android.app.Fragment parent = t1.getParentFragment();
return t1.isVisible() && parent != null && parent.isVisible();
}
});
filteredSource.subscribe(new Action1<String>() {
@Override
public void call(String str) {
// Update the UI with the new data
}); |
Should it actually subscribe/unsubscribe, or just ignore events? If it keeps the subscription open here are two approaches, one with public static Observable<FragmentEvent> fragmentWithVisibilityCheckUsingSwitch(final Fragment fragment) {
return Observable.switchOnNext(listenToFragment(fragment).map(new Func1<FragmentEvent, Observable<FragmentEvent>>() {
boolean visible = true;
@Override
public Observable<FragmentEvent> call(FragmentEvent e) {
if (e.event.equals("hidden")) {
visible = false;
return Observable.from(e); // could be Observable.never() to ignore
} else if (e.event.equals("visible")) {
visible = true;
return Observable.from(e); // could be Observable.never() to ignore
} else {
if (visible) {
return Observable.from(e);
} else {
return Observable.never();
}
}
}
}));
}
public static Observable<FragmentEvent> fragmentWithVisibilityCheckUsingFilter(final Fragment fragment) {
return listenToFragment(fragment).filter(new Func1<FragmentEvent, Boolean>() {
boolean visible = true;
@Override
public Boolean call(FragmentEvent e) {
if (e.event.equals("hidden")) {
visible = false;
return true; // could be false to ignore
} else if (e.event.equals("visible")) {
visible = true;
return true; // could be false to ignore
} else {
if (visible) {
return true;
} else {
return false;
}
}
}
});
} Of course the function for determining whether it is visible can be injected as you state.
What is different from the |
Maybe I'm missing something, but doesn't the operator already do that? It will skip all notifications unless the fragment |
Now I think we don't need to add this operator. Here I think there are two use cases:
|
I'm trying to support that the Observable can always be subscribed by many Observers if the Activity is alive. |
OK. From the the initial discussion it sounded as if this was merely about not emitting notifications if a fragment is in the background. As for the use cases you mentioned:
We can't do that; going through a configuration change means the fragment will get re-attached, and the observable must continue emitting. We use that pattern frequently with
What do you mean by disable? Could this also be solved by using cache and replay? You just keep emitting items into the cached observable, and once your fragment/activity is back to life, you re-subscribe |
That suggests use of a
This sounds correct, but what are the lifecycle implications as far as memory is concerned? The Curious, why would there be events when an activity is not alive? or a fragment is not visible? And if there are events while hidden or not alive, should they be retained or just ignored completely? |
Here the events are not the UI events. It's some events emitted from a background thread. For example, a thread may read some information from a web server in a background thread, and send it to the UI thread. In the UI thread, the UI elements will be updated according to the information. In Android, I think it's a common pattern that creating an Observable that has the same life-cycle as the Activity. In addition, I don't want to store a strong reference to an Activity or a Fragment in the Observable, or it will not be cleaned by GC. So I use a WeakReference. |
The operator already takes care of this? If you unsubscribe in onDestroy, Android guarantees that no messages will be delivered between onDestroy and
That said, I'm still not sure what the problem is we're trying to solve or |
public class TestActivity extends Activity {
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
Observable<Integer> o = OperatorObserveFromAndroidComponent.observeFromAndroidComponent(
Observable.from(1), this);
// the following codes output "1"
o.subscribe(new Observer<Integer>() {
@Override
public void onCompleted() {
}
@Override
public void onError(Throwable e) {
e.printStackTrace();
}
@Override
public void onNext(Integer t) {
System.out.println(t);
}
});
// the following codes output nothing
// this is a wrong behavior, should output "1"
o.subscribe(new Observer<Integer>() {
@Override
public void onCompleted() {
}
@Override
public void onError(Throwable e) {
e.printStackTrace();
}
@Override
public void onNext(Integer t) {
System.out.println(t);
}
});
}
}
|
Here I'm not sure which one is better, using |
Just to summarize, what we want to fix is this yes:
|
Yes, thanks for your summary. |
Thanks @zsxwing for elaborating the use case. @mttkay I agree with the summary, it supports my need of having a more complex function to determine the state. @benjchristensen, as suggested, the source observable I have is sending events whenever it receives them from a push network API. Subscribing/Unsubscribing is done in fragment onCreate/onDestroy, but in between there are moments when the UI is hidden or otherwise inactive and the events can be discarded. The story of keeping the displayed data consistent over the application is of course a lot longer and we have an elaborate system for it, but in this case each notification was only shown in the UI for a short moment. |
Not sure anymore where this should be best discussed, but will leave my comment here: While working on the samples project for rxjava-android, I actually discovered a few other issues that I was unaware of previously. Currently,
As said, I have fixes for the latter two issues, although they require more testing. However, I agree this operator demands rethinking on a bigger scale I guess. Especially the eager (and final) binding to the observed UI component in the constructor is a culprit here that needs to be addressed. |
So, I gave this some deeper thought over the weekend and came to the conclusion that I would prefer to deprecate and completely rewrite the current operator. Here is is what I came up with: https://gist.github.com/mttkay/0590979394aec6144a2e TL;DR: Benefits:
Drawbacks:
I only tested this guy quickly in the samples project in both a fragment that retains instance state and an activity which retains the sequence by piping it through What's missing: Could you guys test this, review this and give me your thoughts? Thanks! |
I realized this can even be further simplified by providing new helpers through
In the Activity, you can then simplify to:
and the Fragment even to:
So the only assumption that remains is that the source sequence must always be |
Just tested this in one of our apps' core screens, works nicely so far. I've prepared a branch against netflix/master that deprecates |
Maybe it's just getting late, but while working on the RxJava Android samples project, I made an observation: it seems the very problem all the Android operator implementations so far try to address, i.e. binding the sequence to the life time of a fragment or activity without leaking it, seems only to actually be an issue when using When using e.g. just I've opened an issue report here since I don't want to drive this issue further off topic, but would be glad if someone could either verify or falsify it: I'm just wondering whether the whole issue we're trying to fix might simply be down to a bug in |
So, I invested more time into this today, and the result is in the PR mentioned before: I have added support to bind a sequence via a predicate function that can be controlled from the outside.
In summary:
==> keeps a weak reference to both
==> same, just that the predicate test is for @zsxwing @tehmou let me know whether this addresses all the problems we discussed? |
/cc @samueltardieu |
This looks nice indeed! |
I suggest to close this and in case of follow up problems / discussion to open a new issue. |
- move the UI thread assert out of the operator and into the helpers; this way, we don't fail the observer anymore with an exception, but the caller. - do not loop unsubscribe through the main thread anymore. This unnecessarily defers releasing the references, and might in fact be processed only after Android creates the component after a rotation change. I had to make the references volatile for this to work. - immediately unsubscribe in case we detect the componentRef has become invalid. This solves the problem that dangling observers would continue to listen to notifications with no observer alive anymore. refs: ReactiveX/RxJava#754 ReactiveX/RxJava#899
I have a use case on Android in which I would want a fragment to stop updating its data while it's hidden. It would seem natural to add a support for this in OperatorObserveFromAndroidComponent. Maybe there could be a switch for whether the observable should be inactive only when the fragment is not added or also when it's hidden?
The text was updated successfully, but these errors were encountered: