All Downloads are FREE. Search and download functionalities are using the official Maven repository.

io.nextop.rx.RxLifecycleBinder Maven / Gradle / Ivy

The newest version!
package io.nextop.rx;

import android.view.View;
import com.google.common.base.Objects;
import immutablecollections.ImSet;
import rx.*;
import rx.Observable;
import rx.Observer;
import rx.android.schedulers.AndroidSchedulers;
import rx.functions.Action0;
import rx.functions.Action2;
import rx.observers.Subscribers;
import rx.subscriptions.BooleanSubscription;
import rx.subscriptions.CompositeSubscription;

import javax.annotation.Nullable;

public interface RxLifecycleBinder extends Subscription {

    void reset();

    /** this version compares the current id to the given id.
     * This is useful for recycler views where tearing down and
     * setting up the same id is wasteful/visually jarring.
     * if different, does a reset and returns true.
     * if the same, does nothing and returns false. */
    boolean reset(Object id);

    /** Wraps the given observable in an observable bound to the lifecyle of a container.
     * The wrapper allocates on subscribe and cleans up on unsubscribe.
     * Unsubscription of all can be forced with {@link #reset}.
     *
     * This ensures:
     * - the subscription is connected to the source when started
     * - the subscription is dropped from the source when stopped
     *   (onComplete is not called, because the subscription will resume when restarted) */
     Observable bind(Observable source);
    void bind(Subscription sub);




    /** Binds to an internal lifecycle start/stop. */
    final class Lifted implements RxLifecycleBinder {
        private final Scheduler scheduler;

        @Nullable
        private Object currentId = null;

        private ImSet> binds = ImSet.empty();
        private final CompositeSubscription subscriptions = new CompositeSubscription();

        private boolean connected = false;
        @Nullable
        private View connectedView = null;

        private boolean closed = false;

        @Nullable
        private Subscription cascadeSubscription = null;

        @Nullable
        private RxDebugger debugger;


        public Lifted() {
            this(RxDebugger.get());
        }

        public Lifted(@Nullable RxDebugger debugger) {
            scheduler = AndroidSchedulers.mainThread();
            this.debugger = debugger;
        }


        public void setDebugger(@Nullable RxDebugger debugger) {
            if (null != this.debugger) {
                // FIXME
                // FIXME detach
            }
            this.debugger = debugger;
            if (null != debugger) {
                // FIXME
                // FIXME attach
            }
        }


        public boolean isClosed() {
            return closed;
        }


        public void connect() {
            connect(null);
        }

        public void connect(@Nullable View view) {
            if (closed) {
                throw new IllegalStateException();
            }
            if (!connected) {
                connected = true;
                connectedView = view;
                for (Bind bind : binds) {
                    bind.connect();
                }
            }
        }
        public void disconnect() {
            if (closed) {
                throw new IllegalStateException();
            }
            if (connected) {
                connected = false;
                connectedView = null;
                for (Bind bind : binds) {
                    bind.disconnect();
                }
            }
        }

        public void close() {
            if (closed) {
                throw new IllegalStateException();
            }
            closed = true;
            removeCascadeUnsubscribe();
            clear();
            subscriptions.unsubscribe();
        }


        public void removeCascadeUnsubscribe() {
            if (null != cascadeSubscription) {
                cascadeSubscription.unsubscribe();
                cascadeSubscription = null;
            }
        }

        // replaces previous cascade parent
        public void cascadeUnsubscribe(@Nullable RxLifecycleBinder parent) {
            removeCascadeUnsubscribe();
            if (null != parent) {
                cascadeSubscription = parent.bind(MoreObservables.hanging()).doOnCompleted(new Action0() {
                    @Override
                    public void call() {
                        unsubscribe();
                    }
                }).subscribe();
            }
        }


        private void clear() {
            ImSet> _binds = binds;
            binds = ImSet.empty();
            for (Bind bind : _binds) {
                bind.close();
            }
            subscriptions.clear();
        }



        /////// Subscription ///////

        @Override
        public void unsubscribe() {
            close();
        }

        @Override
        public boolean isUnsubscribed() {
            return closed;
        }

        /////// RxLifecycleBinder ///////


        @Override
        public void reset() {
            if (closed) {
                throw new IllegalStateException();
            }
            currentId = null;
            clear();
        }

        @Override
        public boolean reset(Object id) {
            if (closed) {
                throw new IllegalStateException();
            }
            if (!Objects.equal(currentId, id)) {
                currentId = id;
                clear();
                return true;
            } else {
                return false;
            }
        }

        @Override
        public  Observable bind(Observable source) {
            if (closed) {
                return Observable.empty();
            }
            Bind bind = new Bind(source.subscribeOn(scheduler));
            binds = binds.adding(bind);
            if (connected) {
                bind.connect();
            }
            return bind.adapter;
        }

        @Override
        public void bind(Subscription sub) {
            if (closed) {
                sub.unsubscribe();
            } else {
                subscriptions.add(sub);
            }
        }

        private final class Bind {
            private final Observable source;
            final Observable adapter;

            private boolean closed = false;
            private boolean connected;

            private ImSet subscribers = ImSet.empty();


            Bind(Observable source) {
                this.source = source;

                adapter = Observable.create(new Observable.OnSubscribe() {
                    @Override
                    public void call(Subscriber subscriber) {
                        add(subscriber);
                    }
                });
            }

            final class Bridge {
                final Subscriber subscriber;
                private @Nullable Subscription subscription = null;


                // DEBUG STATISTICS

                private int onNextCount = 0;
                private int onCompletedCount = 0;
                private int onErrorCount = 0;
                @Nullable
                private Notification mostRecentNotification = null;
                private int failedNotificationCount = 0;
                @Nullable
                private Notification mostRecentFailedNotification = null;
                @Nullable
                private Throwable mostRecentFailedNotificationReason = null;


                Bridge(Subscriber subscriber) {
                    this.subscriber = subscriber;
                }


                void subscribe(Subscription subscription) {
                    unsubscribe();
                    if (subscriber.isUnsubscribed()) {
                        subscription.unsubscribe();
                    } else {
                        this.subscription = subscription;

                        if (null != debugger && debugger.isEnabled()) {
                            debugger.update(new RxDebugger.Stats(RxDebugger.Stats.F_CONNECTED, subscriber, connectedView, false, true,
                                    onNextCount, onCompletedCount, onErrorCount, mostRecentNotification,
                                    failedNotificationCount, mostRecentFailedNotification, mostRecentFailedNotificationReason));
                        }
                    }
                }

                void unsubscribe() {
                    if (null != subscription) {
                        subscription.unsubscribe();
                        subscription = null;

                        if (null != debugger && debugger.isEnabled()) {
                            debugger.update(new RxDebugger.Stats(RxDebugger.Stats.F_DISCONNECTED, subscriber, connectedView, false, false,
                                    onNextCount, onCompletedCount, onErrorCount, mostRecentNotification,
                                    failedNotificationCount, mostRecentFailedNotification, mostRecentFailedNotificationReason));
                        }
                    }
                }

                void close() {
                    unsubscribe();
                    if (!subscriber.isUnsubscribed()) {
                        subscriber.onCompleted();
                        subscriber.unsubscribe();

                        if (null != debugger && debugger.isEnabled()) {
                            onCompletedCount += 1;
                            debugger.update(new RxDebugger.Stats(RxDebugger.Stats.F_COMPLETED, subscriber, connectedView, true, false,
                                    onNextCount, onCompletedCount, onErrorCount, mostRecentNotification,
                                    failedNotificationCount, mostRecentFailedNotification, mostRecentFailedNotificationReason));
                        }
                    }
                }


                public Subscriber inSubscriber() {
                    // if in debug, the notification is routed through the debugger
                    // which may step/filter notifications

                    final Action2 debugDelivery = new Action2() {
                        @Override
                        public void call(Subscriber subscriber, Notification notification) {
                            if (!subscriber.isUnsubscribed()) {
                                mostRecentNotification = notification;

                                int flags = 0;

                                try {
                                    notification.accept(subscriber);
                                    switch (notification.getKind()) {
                                        case OnNext:
                                            onNextCount += 1;
                                            flags |= RxDebugger.Stats.F_NEXT;
                                            break;
                                        case OnError:
                                            onErrorCount += 1;
                                            flags |= RxDebugger.Stats.F_ERROR;
                                            break;
                                        case OnCompleted:
                                            onCompletedCount += 1;
                                            flags |= RxDebugger.Stats.F_COMPLETED;
                                            break;
                                        default:
                                            throw new IllegalArgumentException();
                                    }
                                } catch (Throwable t) {
                                    failedNotificationCount += 1;
                                    mostRecentFailedNotification = notification;
                                    mostRecentFailedNotificationReason = t;
                                    flags |= RxDebugger.Stats.F_FAILED;
                                }

                                if (null != debugger && debugger.isEnabled()) {
                                    debugger.update(new RxDebugger.Stats(flags, subscriber, connectedView, false, !subscription.isUnsubscribed(),
                                            onNextCount, onCompletedCount, onErrorCount, mostRecentNotification,
                                            failedNotificationCount, mostRecentFailedNotification, mostRecentFailedNotificationReason));
                                }
                            }
                        }
                    };

                    return Subscribers.from(new Observer() {
                        @Override
                        public void onNext(T t) {
                            if (null != debugger && debugger.isEnabled()) {
                                debugger.deliver(subscriber, Notification.createOnNext(t), debugDelivery);
                            } else if (!subscriber.isUnsubscribed()) {
                                subscriber.onNext(t);
                            }
                        }

                        @Override
                        public void onCompleted() {
                            if (null != debugger && debugger.isEnabled()) {
                                debugger.deliver(subscriber, Notification.createOnCompleted(), debugDelivery);
                            } else if (!subscriber.isUnsubscribed()) {
                                subscriber.onCompleted();
                            }
                        }

                        @Override
                        public void onError(Throwable e) {
                            if (null != debugger && debugger.isEnabled()) {
                                debugger.deliver(subscriber, Notification.createOnError(e), debugDelivery);
                            } else if (!subscriber.isUnsubscribed()) {
                                subscriber.onError(e);
                            }
                        }
                    });
                }

                public Subscription outSubscription() {
                    return BooleanSubscription.create(new Action0() {
                        @Override
                        public void call() {
                            unsubscribe();
                            subscribers = subscribers.removing(Bridge.this);
                        }
                    });
                }
            };



            private void add(final Subscriber subscriber) {
                Bridge bridge = new Bridge(subscriber);
                subscribers = subscribers.adding(bridge);

                subscriber.add(bridge.outSubscription());
                connect(bridge);
            }

            void connect() {
                if (closed) {
                    throw new IllegalStateException();
                }
                if (!connected) {
                    connected = true;
                    for (Bridge bridge : subscribers) {
                        connect(bridge);
                    }
                }
            }
            void connect(Bridge bridge) {
                if (connected && !bridge.subscriber.isUnsubscribed()) {
                    bridge.subscribe(source.subscribe(bridge.inSubscriber()));
                }
            }


            void disconnect() {
                if (closed) {
                    throw new IllegalStateException();
                }
                if (connected) {
                    connected = false;
                    for (Bridge bridge : subscribers) {
                        bridge.unsubscribe();
                    }
                }
            }

            // implies disconnect
            void close() {
                if (closed) {
                    throw new IllegalStateException();
                }
                disconnect();
                if (!closed) {
                    closed = true;
                    for (Bridge bridge : subscribers) {
                        bridge.close();
                    }
                }
            }
        }
    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy