web-dev-qa-db-fra.com

RxJava et données mises en cache

Je suis encore assez nouveau sur RxJava et je l’utilise dans une application Android. J'ai lu une tonne métrique sur le sujet, mais j'ai toujours l'impression qu'il me manque quelque chose.

J'ai le scénario suivant: 

J'ai des données stockées dans le système auquel on accède via différentes connexions de service (AIDL) et j'ai besoin de récupérer les données de ce système (un nombre d'appels asynchrones pouvant être 1 à n peut survenir). Rx m'a beaucoup aidé à simplifier ce code. Cependant, ce processus prend généralement quelques secondes (plus de 5 secondes et plus). Je dois donc mettre ces données en cache pour accélérer l'application native. 

Les exigences à ce stade sont les suivantes: 

  1. Souscription initiale, le cache sera vide, nous devons donc attendre le temps requis pour charger. Pas grand chose. Après cela, les données doivent être mises en cache. 

  2. Les chargements ultérieurs doivent extraire les données du cache, mais les données doivent ensuite être rechargées et le cache de disque doit se trouver en arrière-plan. 

Le problème: J'ai deux observables - A et B. A contient les observables imbriqués qui extraient des données des services locaux (des tonnes se passent ici). B est beaucoup plus simple. B contient simplement le code permettant d'extraire les données du cache disque. 

Besoin à résoudre: A) Retournez un élément mis en cache (s'il est mis en cache) et continuez à recharger le cache disque. B) Le cache est vide, chargez les données du système, mettez-les en cache et renvoyez il. Les appels suivants reviennent à "a". 

Certaines personnes ont recommandé quelques opérations telles que le flatmap, la fusion et même des sujets, mais pour une raison quelconque, j'ai des difficultés à relier les points. 

Comment puis-je faire ceci? 

21
Donn Felker

Voici quelques options sur la façon de procéder. Je vais essayer de les expliquer du mieux que je peux au fur et à mesure. C'est du code nappe, et j'utilise une syntaxe lambda de style Java8 parce que je suis paresseux et que c'est plus joli. :)

  1. Un sujet, comme AsyncSubject, serait parfait si vous pouviez conserver ces états d'instance en mémoire, même s'il semble que vous ayez besoin de les stocker sur disque. Cependant, je pense que cette approche mérite d'être mentionnée au cas où vous en seriez capable. En outre, il s’agit d’une technique astucieuse à connaître. AsyncSubject est un observable qui n’émet que la dernière valeur publiée (un sujet est à la fois un observateur et un observable), et ne commencera à émettre que lorsque onCompleted aura été appelé . Ainsi, tout ce qui s'abonne après cette fin recevra la valeur suivante.

    Dans ce cas, vous pourriez avoir (dans une classe d'application ou une autre instance singleton au niveau de l'application):

    public class MyApplication extends Application {    
        private final AsyncSubject<Foo> foo = AsyncSubject.create();
    
        /** Asynchronously gets foo and stores it in the subject. */
        public void fetchFooAsync() {
            // Gets the observable that does all the heavy lifting.
            // It should emit one item and then complete.
            FooHelper.getTheFooObservable().subscribe(foo);
        }
    
        /** Provides the foo for any consumers who need a foo. */
        public Observable<Foo> getFoo() {
            return foo;
        }
    
    }
    
  2. Reporter l'observable. Observable.defer vous permet d'attendre la création d'un observable jusqu'à ce qu'il soit abonné. Vous pouvez l'utiliser pour permettre à l'extraction du cache disque de s'exécuter en arrière-plan, puis renvoyer la version mise en cache ou, si ce n'est pas en cache, effectuer la transaction réelle.

    Cette version suppose que votre code d'accesseur, à la fois l'extraction du cache et la création non interceptée, bloque les appels et non les observables, et que le différé fonctionne en arrière-plan. Par exemple:

    public Observable<Foo> getFoo() {
        Observable.defer(() -> {
            if (FooHelper.isFooCached()) {
                return Observable.just(FooHelper.getFooFromCacheBlocking());
            }
            return Observable.just(FooHelper.createNewFooBlocking());
        }).subscribeOn(Schedulers.io());
    }
    
  3. Utilisez concatWith et take. Ici, nous supposons que notre méthode pour extraire le Foo du cache disque émet un seul élément et se termine ou complète sans émettre si elle est vide.

    public Observable<Foo> getFoo() {
        return FooHelper.getCachedFooObservable()
                .concatWith(FooHelper.getRealFooObservable())
                .take(1);
    }
    

    Cette méthode ne devrait tenter d'extraire la vraie affaire que si l'observable en cache se termine à vide. 

  4. Utilisez amb ou ambWith. C’est probablement l’une des solutions les plus folles, mais amusante à signaler. amb prend fondamentalement quelques observables (ou plus avec les surcharges) et attend que l’un d’eux émette un objet, puis rejette complètement l’autre observable et ne prend que celui qui a remporté la course. Cela ne serait utile que s'il est possible que l'étape de calcul de la création d'un nouveau Foo soit plus rapide que son extraction sur disque. Dans ce cas, vous pourriez faire quelque chose comme ceci:

    public Observable<Foo> getFoo() {
        return Observable.amb(
                FooHelper.getCachedFooObservable(),
                FooHelper.getRealFooObservable());
    }
    

Je préfère un peu l'option 3. En ce qui concerne la mise en cache, vous pourriez avoir quelque chose comme ceci à l'un des points d'entrée (de préférence avant d'avoir besoin du Foo, car comme vous l'avez dit, l'opération est longue). devrait obtenir la version en cache tant qu’elle a fini d’écrire. L'utilisation d'une AsyncSubject ici peut également être utile, afin de ne pas déclencher le travail plusieurs fois en attendant qu'il soit écrit. Les consommateurs n’obtiendraient que le résultat final, mais là encore, cela ne fonctionne que s’il peut être raisonnablement conservé en mémoire.

if (!FooHelper.isFooCached()) {
    getFoo()
        .subscribeOn(Schedulers.io())
        .subscribe((foo) -> FooHelper.cacheTheFoo(foo));
}

Notez que vous devez conserver un planificateur à un seul thread destiné à l'écriture (et à la lecture) sur le disque et utiliser .observeOn(foo) après le .subscribeOn(...), ou bien synchroniser l'accès au cache disque pour éviter les problèmes de simultanéité.

28
lopar

J'ai récemment publié une bibliothèque sur Github pour Android et Java, appelée RxCache , qui répond à vos besoins en matière de mise en cache de données à l'aide d'observables.

RxCache implémente deux couches de mise en cache, mémoire et disque, et compte avec plusieurs annotations afin de configurer le comportement de chaque fournisseur. 

Il est fortement recommandé d'utiliser avec Retrofit pour les données extraites d'appels http. En utilisant l'expression lambda, vous pouvez formuler une expression comme suit:

    rxCache.getUser(retrofit.getUser(id), () -> true).flatmap(user -> user);

J'espère que tu vas trouver ça intéressant :)

2
Víctor Albertos

Regardez le projet ci-dessous. Ceci est mon point de vue personnel sur les choses et j'ai utilisé ce modèle dans un certain nombre d'applications.

https://github.com/zsiegel/rxandroid-architecture-sample

Jetez un coup d'œil à PersistenceService. Plutôt que de frapper la base de données (ou MockService dans l'exemple de projet), vous pouvez simplement avoir une liste locale d'utilisateurs mis à jour avec la méthode save () et la renvoyer dans get ().

Faites moi savoir si vous avez des questions.

0
Zac Siegel