web-dev-qa-db-fra.com

Rx Observable émettant des valeurs périodiquement

Je dois périodiquement interroger certains points de terminaison RESTful pour actualiser mes Android. Je dois également faire une pause et les reprendre en fonction de la connectivité (si le téléphone est hors ligne, il n'est même pas nécessaire d'essayer). Ma solution actuelle fonctionne, mais elle utilise le ScheduledExecutorService de Java standard pour effectuer des tâches périodiques, mais j'aimerais rester dans le paradigme Rx.

Voici mon code actuel, dont certaines parties sont ignorées par souci de concision.

userProfileObservable = Observable.create(new Observable.OnSubscribe<UserProfile>() {
    @Override
    public void call(final Subscriber<? super UserProfile> subscriber) {
        final ScheduledExecutorService scheduledExecutorService = Executors.newSingleThreadScheduledExecutor();
        final Runnable runnable = new Runnable() {
            @Override
            public void run() {
                // making http request here
            }
        };
        final List<ScheduledFuture<?>> futures = new ArrayList<ScheduledFuture<?>>(1);
        networkStatusObservable.subscribe(new Action1<Boolean>() {
            @Override
            public void call(Boolean networkAvailable) {
                if (!networkAvailable) {
                    pause();
                } else {
                    pause();                        
                    futures.add(scheduledExecutorService.scheduleWithFixedDelay(runnable, 0, SECOND_IN_MILLIS * SECONDS_TO_EXPIRE, TimeUnit.MILLISECONDS));
                }
            }

            private void pause() {
                for (ScheduledFuture<?> future : futures) {
                    future.cancel(true);
                }
                futures.clear();
            }
        });

        final Subscription subscription = new Subscription() {
            private boolean isUnsubscribed = false;

            @Override
            public void unsubscribe() {
                scheduledExecutorService.shutdownNow();
                isUnsubscribed = true;
            }

            @Override
            public boolean isUnsubscribed() {
                return isUnsubscribed;
            }
        };
        subscriber.add(subscription);
    }
}).multicast(BehaviorSubject.create()).refCount();

networkStatusObservable est essentiellement un récepteur de diffusion enveloppé dans Observable<Boolean>, indiquant que le téléphone est connecté au réseau.

Comme je l'ai dit, cette solution fonctionne, mais je veux utiliser l'approche Rx pour l'interrogation périodique et l'émission de nouveaux UserProfiles, car il y a de nombreux problèmes avec la planification manuelle des choses, que je veux éviter. Je connais Observable.timer et Observable.interval, mais je ne sais pas comment les appliquer à cette tâche (et je ne sais pas si je dois les utiliser du tout).

21
Haspemulator

Il existe quelques approches sur ce problème GitHub qui pourraient vous être utiles.

https://github.com/ReactiveX/RxJava/issues/448

Les trois implémentations sont:


Observable.interval

Observable.interval(delay, TimeUnit.SECONDS).timeInterval()
        .flatMap(new Func1<Long, Observable<Notification<AppState>>>() {
            public Observable<Notification<AppState>> call(Long seconds) {
                return lyftApi.updateAppState(params).materialize(); } });

Scheduler.schedulePeriodically

Observable.create({ observer ->
        Schedulers.newThread().schedulePeriodically({
            observer.onNext("application-state-from-network");
        }, 0, 1000, TimeUnit.MILLISECONDS);
    }).take(10).subscribe({ v -> println(v) });

récursivité manuelle

Observable.create(new OnSubscribeFunc<String>() {
        @Override
        public Subscription onSubscribe(final Observer<? super String> o) {
            return Schedulers.newThread().schedule(0L, new Func2<Scheduler, Long, Subscription>() {
                @Override
                public Subscription call(Scheduler inner, Long t2) {
                    o.onNext("data-from-polling");
                    return inner.schedule(t2, this, 1000, TimeUnit.MILLISECONDS);
                }
            });
        }
    }).toBlockingObservable().forEach(new Action1<String>() {
        @Override
        public void call(String v) {
            System.out.println("output: " + v);
        }
    });

Et la conclusion est que la récursivité manuelle est la voie à suivre car elle attend la fin de l'opération avant de planifier la prochaine exécution.

26
Robert Estivill

L'une des options consiste à utiliser Observable.interval et à vérifier l'état de l'utilisateur lorsque les intervalles sont émis:

     Observable<Long> interval = Observable.interval(1, TimeUnit.SECONDS);

    //pulling the user data
    Observable<Observable<String>> userObservable = interval.map(new Func1<Long, Observable<String>>() {
        Random random = new Random();
        @Override
        public Observable<String> call(Long tick) {
            //here you are pulling user data; you should do it asynchronously - rx way - because the interval is using Schedulers.computation which is not best suited for doing io operations
            switch(random.nextInt(10)){
                case 0://suppose this is for cases when network in  not available or exception happens
                    return Observable.<String>just(null);
                case 1:
                case 2:
                    return Observable.just("Alice");
                default:
                    return Observable.just("Bob");
            }
        }
    });

    Observable<String> flatUsers = userObservable.flatMap(new Func1<Observable<String>, Observable<? extends String>>() {
        @Override
        public Observable<? extends String> call(Observable<String> stringObservable) {
            return stringObservable;
        }
    });

    //filter valid data
    Observable<String> usersWithoutErrors = flatUsers.filter(new Func1<String, Boolean>() {
        @Override
        public Boolean call(String s) {
            return s != null;
        }
    });

    //publish only changes
    Observable<String> uniqueUsers = usersWithoutErrors.distinctUntilChanged();

Vous pouvez le faire encore plus simplement si votre networkStatusObservable émet des événements au moins aussi souvent que nécessaire pour vérifier les données utilisateur

 networkStatusObservable.sample(1,TimeUnit.Seconds).filter(/*the best is to filter only connected state */).map(/*now start pulling the user data*/)

Enfin, vous pouvez créer un observable qui utilise le planificateur pour émettre périodiquement les états de l'utilisateur - reportez-vous à Documentation des planificateurs pour savoir quel planificateur vous convient le mieux:

public abstract class ScheduledOnSubscribe<T> implements Observable.OnSubscribe<T>{
    private final Scheduler scheduler;
    private final long initialDelay;
    private final long period;
    private final TimeUnit unit;

    public ScheduledOnSubscribe(Scheduler scheduler, long initialDelay, long period, TimeUnit unit) {
        this.scheduler = scheduler;
        this.initialDelay = initialDelay;
        this.period = period;
        this.unit = unit;
    }

    abstract T next() throws Exception;


    @Override
    public void call(final Subscriber<? super T> subscriber) {
        final Scheduler.Worker worker = scheduler.createWorker();
        subscriber.add(worker);
        worker.schedulePeriodically(new Action0() {
            @Override
            public void call() {
                try {
                    subscriber.onNext(next());
                } catch (Throwable e) {
                    try {
                        subscriber.onError(e);
                    } finally {
                        worker.unsubscribe();
                    }
                }
            }

        }, initialDelay, period, unit);
    }
}

//And here is the sample usage
 Observable<String> usersObservable = Observable.create(new ScheduledOnSubscribe(Schedulers.io(), 1, 1, TimeUnit.SECONDS ){
        Random random = new Random();
        @Override
        String next() throws Exception {
            //if you use Schedulers.io, you can call the remote service synchronously
            switch(random.nextInt(10)){
                case 0:
                    return null;
                case 1:
                case 2:
                    return "Alice";
                default:
                    return "Bob";
            }
        }
    });
5
Marek Hawrylczak

Réponse courte. RxJava2:

Observable.interval(initialDelay, unitAmount, timeUnit)
            .subscribe(value -> {
                // code for periodic execution
            });

Choisissez initialDelay, unitAmount et TimeUnit selon vos besoins.

Exemple: 0, 1, TimeUnit.MINUTES.

3
Johnny Five

Il existe un moyen plus simple de le faire en utilisant interval (). J'ai testé ce code et cela fonctionne. Mais d'abord, vous devez encapsuler le travail que vous souhaitez exécuter périodiquement dans une sous-classe d'Action1.

class Act<T> implements Action1<T> {
     public Service service;
     public String data;
     public void call(T t){
         service.log(data); //the periodic job
     }
}

(J'ai gardé les champs publics par souci de concision, mais ce n'est pas conseillé). Vous pouvez maintenant le planifier de la manière suivante:

Act<Long> act=new Act<>();
act.data="dummy data";
act.service=this;
Observable.interval(0l, period, TimeUnit.SECONDS).subscribeOn(Schedulers.from(Executors.newFixedThreadPool(10))).subscribe((Action1<Long>)act);

Cela ne bloquera vos discussions nulle part, contrairement à l'approche donnée dans l'autre réponse. Cette approche nous permet de passer une variable comme une sorte de stockage mutable à l'intérieur de l'action qui pourrait être utile dans les invocations suivantes. De cette façon, vous pouvez également abonner votre appel sur votre propre pool de threads.

3
zafar142003

D'accord, je vais publier ma propre solution, peut-être que quelqu'un en bénéficiera. Je ne posterai que la partie liée à la question, en omettant le HTTP et la mise en cache. Voici comment je le fais:

private ConnectableObservable<Long> createNetworkBoundHeartbeatObservable(final Observable<Boolean> networkStatusObservable,
                                                                          final Observable<Boolean> pauseResumeObservable) {

    final Observable<Boolean> pausableHeartbeatObservable = Observable.combineLatest(networkStatusObservable, pauseResumeObservable,
            new Func2<Boolean, Boolean, Boolean>() {
                @Override
                public Boolean call(Boolean networkAvailable, Boolean mustPause) {
                    return mustPause && networkAvailable;
                }
            }
    ).distinctUntilChanged();

    final Observable<Boolean> hasToResumeObservable = pausableHeartbeatObservable.filter(new Func1<Boolean, Boolean>() {
        @Override
        public Boolean call(Boolean networkAvailable) {
            return networkAvailable;
        }
    });
    final Observable<Boolean> hasToStopObservable = pausableHeartbeatObservable.filter(new Func1<Boolean, Boolean>() {
        @Override
        public Boolean call(Boolean networkAvailable) {
            return !networkAvailable;
        }
    });


    return pausableHeartbeatObservable.concatMap(new Func1<Boolean, Observable<Long>>() {
        @Override
        public Observable<Long> call(Boolean shouldResumeRequests) {
            if (shouldResumeRequests) {
                long timeToUpdate;
                final Date oldestModifiedExpiresAt = cache.oldestModifiedExpiresAt();
                timeToUpdate = Math.max(0, oldestModifiedExpiresAt.getTime() - System.currentTimeMillis());
                Log.d(TAG, String.format("Have to restart updates, %d seconds till next update", timeToUpdate / SECOND_IN_MILLIS));
                return Observable
                        .timer(timeToUpdate, SECONDS_TO_EXPIRE * SECOND_IN_MILLIS, TimeUnit.MILLISECONDS)
                        .takeUntil(hasToStopObservable);
            } else {
                Log.d(TAG, "Have to pause updates");
                return Observable.<Long>never().takeUntil(hasToResumeObservable);
            }
        }
    }).multicast(PublishSubject.<Long>create());
}

Comme vous pouvez le voir, les conditions pour suspendre ou reprendre les mises à jour deviennent un peu plus compliquées, avec un nouvel observable ajouté pour prendre en charge la pause lorsque l'application passe en arrière-plan.

Ensuite, au cœur de la solution se trouve l'opération concatMap qui émet le Observables séquentiellement (d'où concatMap, pas flatMap, voir cette question: Quelle est la différence entre concatMap et flatMap dans RxJava ). Il émet interval ou neverObservables, selon que les mises à jour doivent être poursuivies ou interrompues. Ensuite, chaque Observable est takenUntil 'un opposé' Observable émet une nouvelle valeur.

ConnectableObservable est renvoyé car le Observable créé est chaud et tous les abonnés prévus doivent s'y abonner avant qu'il ne commence à émettre quelque chose, sinon les événements initiaux pourraient être perdus. J'appelle connect dessus plus tard.

J'accepterai ma ou une autre réponse sur la base des votes, le cas échéant.

1
Haspemulator