Quand utilisez-vous map vs flatMap dans RxJava?

180

Quand utilisez-vous mapvs flatMapdans RxJava ?

Disons, par exemple, que nous voulons mapper les fichiers contenant JSON en chaînes qui contiennent le JSON--

En utilisant map, nous devons faire face en Exceptionquelque sorte. Mais comment?:

Observable.from(jsonFile).map(new Func1<File, String>() {
    @Override public String call(File file) {
        try {
            return new Gson().toJson(new FileReader(file), Object.class);
        } catch (FileNotFoundException e) {
            // So Exception. What to do ?
        }
        return null; // Not good :(
    }
});

En utilisant flatMap, c'est beaucoup plus verbeux, mais nous pouvons faire suivre le problème dans la chaîne Observableset gérer l'erreur si nous choisissons ailleurs et même réessayons:

Observable.from(jsonFile).flatMap(new Func1<File, Observable<String>>() {
    @Override public Observable<String> call(final File file) {
        return Observable.create(new Observable.OnSubscribe<String>() {
            @Override public void call(Subscriber<? super String> subscriber) {
                try {
                    String json = new Gson().toJson(new FileReader(file), Object.class);

                    subscriber.onNext(json);
                    subscriber.onCompleted();
                } catch (FileNotFoundException e) {
                    subscriber.onError(e);
                }
            }
        });
    }
});

J'aime la simplicité du map, mais la gestion des erreurs flatmap(pas la verbosité). Je n'ai vu aucune meilleure pratique à ce sujet et je suis curieux de savoir comment cela est utilisé dans la pratique.

Christopher Perry
la source

Réponses:

121

maptransformer un événement en un autre. flatMaptransformer un événement en zéro ou plusieurs événements. (ceci est tiré d' IntroToRx )

Comme vous voulez transformer votre json en objet, l'utilisation de map devrait suffire.

Traiter avec l'exception FileNotFoundException est un autre problème (l'utilisation de map ou flatmap ne résoudrait pas ce problème).

Pour résoudre votre problème d'exception, lancez-le simplement avec une exception non cochée: RX appellera le gestionnaire onError pour vous.

Observable.from(jsonFile).map(new Func1<File, String>() {
    @Override public String call(File file) {
        try {
            return new Gson().toJson(new FileReader(file), Object.class);
        } catch (FileNotFoundException e) {
            // this exception is a part of rx-java
            throw OnErrorThrowable.addValueAsLastCause(e, file);
        }
    }
});

la même version exacte avec flatmap:

Observable.from(jsonFile).flatMap(new Func1<File, Observable<String>>() {
    @Override public Observable<String> call(File file) {
        try {
            return Observable.just(new Gson().toJson(new FileReader(file), Object.class));
        } catch (FileNotFoundException e) {
            // this static method is a part of rx-java. It will return an exception which is associated to the value.
            throw OnErrorThrowable.addValueAsLastCause(e, file);
            // alternatively, you can return Obersable.empty(); instead of throwing exception
        }
    }
});

Vous pouvez également renvoyer, dans la version flatMap, un nouvel Observable qui n'est qu'une erreur.

Observable.from(jsonFile).flatMap(new Func1<File, Observable<String>>() {
    @Override public Observable<String> call(File file) {
        try {
            return Observable.just(new Gson().toJson(new FileReader(file), Object.class));
        } catch (FileNotFoundException e) {
            return Observable.error(OnErrorThrowable.addValueAsLastCause(e, file));
        }
    }
});
dwursteisen
la source
2
Cela n'appelle pas subscriber.onError()etc. Tous les exemples que j'ai vus ont acheminé des erreurs de cette façon. Cela n'a pas d'importance?
Christopher Perry
7
Notez que les constructeurs de OnErrorThrowablesont privateet que vous devez utiliser à la OnErrorThrowable.from(e)place.
david.mihola
Je viens de mettre à jour. OnErrorThrowable.from (e) ne garde pas la valeur, donc j'utilise OnErrorThrowable.addValueAsLastCause (e, file) à la place, qui devrait garder la valeur.
dwursteisen
1
J'aime les exemples de code, mais cela aiderait si vous mettiez à jour la signature des appels flatMap pour renvoyer Observable <String> au lieu de simplement String ... car n'est-ce pas techniquement la différence entre les deux?
Rich Ehmer
78

FlatMap se comporte beaucoup comme map, la différence est que la fonction qu'il applique renvoie elle-même une observable, il est donc parfaitement adapté pour mapper sur des opérations asynchrones.

Dans le sens pratique, la fonction Map applique effectue simplement une transformation sur la réponse chaînée (ne renvoyant pas d'observable); tandis que la fonction FlatMap s'applique renvoie unObservable<T> , c'est pourquoi FlatMap est recommandé si vous prévoyez d'effectuer un appel asynchrone à l'intérieur de la méthode.

Résumé:

  • Map renvoie un objet de type T
  • FlatMap renvoie un Observable.

Un exemple clair peut être vu ici: http://blog.couchbase.com/why-couchbase-chose-rxjava-new-java-sdk .

Le client Couchbase Java 2.X utilise Rx pour fournir des appels asynchrones de manière pratique. Puisqu'il utilise Rx, il a les méthodes map et FlatMap, l'explication dans leur documentation peut être utile pour comprendre le concept général.

Pour gérer les erreurs, remplacez onError sur votre abonné.

Subscriber<String> mySubscriber = new Subscriber<String>() {
    @Override
    public void onNext(String s) { System.out.println(s); }

    @Override
    public void onCompleted() { }

    @Override
    public void onError(Throwable e) { }
};

Il peut être utile de consulter ce document: http://blog.danlew.net/2014/09/15/grokking-rxjava-part-1/

Une bonne source sur la gestion des erreurs avec RX peut être trouvée à: https://gist.github.com/daschl/db9fcc9d2b932115b679

1vand1ng0
la source
Le résumé est faux. Map et FlatMap renvoient le même type mais la fonction qu'ils appliquent renvoient un type différent.
CoXier
61

Dans votre cas, vous avez besoin d'une carte, car il n'y a qu'une entrée et une sortie.

La fonction fournie par la carte accepte simplement un élément et renvoie un élément qui sera émis plus loin (une seule fois) vers le bas.

flatMap - la fonction fournie accepte un élément puis renvoie un "Observable", ce qui signifie que chaque élément du nouveau "Observable" sera émis séparément plus bas.

Peut-être que le code éclaircira les choses pour vous:

Observable.just("item1").map( str -> {
    System.out.println("inside the map " + str);
    return str;
}).subscribe(System.out::println);

Observable.just("item2").flatMap( str -> {
    System.out.println("inside the flatMap " + str);
    return Observable.just(str + "+", str + "++" , str + "+++");
}).subscribe(System.out::println);

Production:

inside the map item1
item1
inside the flatMap item2
item2+
item2++
item2+++
mt.uulu
la source
Je ne sais pas si utiliser une carte est la meilleure idée, même si cela fonctionnerait. Supposons que FileReader devienne un appel asynchrone. Ensuite, vous devez changer la carte en flatMap. Le laisser sous forme de carte signifierait que vous n'obtiendrez pas les événements déclenchés comme prévu et entraînerait de la confusion. J'ai été mordu par cela à quelques reprises alors que j'apprends toujours RX Java. Je trouve que flatMap est un moyen infaillible de garantir que les choses soient traitées comme prévu.
user924272
24

La façon dont je pense les choses est que vous utilisez flatMaplorsque la fonction que vous vouliez mettre à l'intérieur de map()renvoie un fichier Observable. Dans ce cas, vous pourriez toujours essayer d'utiliser map()mais ce ne serait pas pratique. Laissez-moi essayer d'expliquer pourquoi.

Si dans ce cas vous décidiez de vous en tenir map, vous obtiendrez un fichier Observable<Observable<Something>>. Par exemple, dans votre cas, si nous utilisions une bibliothèque RxGson imaginaire, qui retournait une méthode de Observable<String>sa toJson()méthode (au lieu de simplement renvoyer a String), cela ressemblerait à ceci:

Observable.from(jsonFile).map(new Func1<File, Observable<String>>() {
    @Override public Observable<String>> call(File file) {
        return new RxGson().toJson(new FileReader(file), Object.class);
    }
}); // you get Observable<Observable<String>> here

À ce stade, il serait assez difficile de subscribe()faire un tel observable. À l'intérieur de celui-ci, vous obtiendrez un Observable<String>auquel vous auriez à nouveau besoin subscribe()pour obtenir la valeur. Ce qui n'est ni pratique ni agréable à regarder.

Donc, pour le rendre utile, une idée est d '"aplatir" cette observable d'observables (vous pourriez commencer à voir d'où vient le nom _flat_Map). RxJava fournit quelques moyens d'aplatir les observables et, par souci de simplicité, supposons que la fusion est ce que nous voulons. Merge prend essentiellement un tas d'observables et émet chaque fois que l'un d'eux émet. (Beaucoup de gens diraient que le commutateur serait une meilleure valeur par défaut. Mais si vous n'émettez qu'une seule valeur, cela n'a pas d'importance de toute façon.)

Donc, en modifiant notre extrait de code précédent, nous obtiendrions:

Observable.from(jsonFile).map(new Func1<File, Observable<String>>() {
    @Override public Observable<String>> call(File file) {
        return new RxGson().toJson(new FileReader(file), Object.class);
    }
}).merge(); // you get Observable<String> here

C'est beaucoup plus utile, car en vous abonnant à cela (ou en mappant, ou en filtrant, ou ...) vous obtenez juste la Stringvaleur. (De plus, merge()remarquez qu'une telle variante de n'existe pas dans RxJava, mais si vous comprenez l'idée de fusion, j'espère que vous comprenez également comment cela fonctionnerait.)

Donc, fondamentalement, parce que tel merge()ne devrait probablement jamais être utile que quand il réussit à map()renvoyer un observable et que vous n'avez donc pas à le taper encore et encore, a flatMap()été créé comme un raccourci. Il applique la fonction de mappage comme d'habitude map(), mais plus tard, au lieu d'émettre les valeurs renvoyées, il les "aplatit" (ou les fusionne).

C'est le cas d'utilisation général. C'est très utile dans une base de code qui utilise Rx partout et vous avez de nombreuses méthodes renvoyant des observables, que vous souhaitez enchaîner avec d'autres méthodes renvoyant des observables.

Dans votre cas d'utilisation, cela s'avère également utile, car map()vous ne pouvez transformer qu'une valeur émise onNext()dans une autre valeur émise dans onNext(). Mais il ne peut pas le transformer en plusieurs valeurs, aucune valeur du tout ou une erreur. Et comme akarnokd l'a écrit dans sa réponse (et attention, il est beaucoup plus intelligent que moi, probablement en général, mais au moins quand il s'agit de RxJava), vous ne devriez pas jeter d'exceptions à votre map(). Donc à la place, vous pouvez utiliser flatMap()et

return Observable.just(value);

quand tout va bien, mais

return Observable.error(exception);

quand quelque chose échoue.
Voir sa réponse pour un extrait complet: https://stackoverflow.com/a/30330772/1402641

Marcin Koziński
la source
1
c'est ma réponse préférée. vous finissez par imbriquer une observable DANS un IF observable qui est ce que votre méthode renvoie.
filthy_wizard
21

La question est: Quand utilisez-vous map vs flatMap dans RxJava? . Et je pense qu'une simple démo est plus spécifique.

Lorsque vous souhaitez convertir un élément émis vers un autre type, dans votre cas, la conversion du fichier en String, map et flatMap peut fonctionner. Mais je préfère l'opérateur cartographique car c'est plus clair.

Cependant, dans certains endroits, flatMappeut faire un travail de magie mais mapne peut pas. Par exemple, je veux obtenir les informations d'un utilisateur, mais je dois d'abord obtenir son identifiant lorsque l'utilisateur se connecte. De toute évidence, j'ai besoin de deux demandes et elles sont en ordre.

Commençons.

Observable<LoginResponse> login(String email, String password);

Observable<UserInfo> fetchUserInfo(String userId);

Voici deux méthodes, une pour la connexion renvoyée Responseet une autre pour récupérer les informations utilisateur.

login(email, password)
        .flatMap(response ->
                fetchUserInfo(response.id))
        .subscribe(userInfo -> {
            // get user info and you update ui now
        });

Comme vous le voyez, dans la fonction flatMap s'applique, dans un premier temps, j'obtiens l'identifiant de Responsel'utilisateur, puis je récupère les informations sur l'utilisateur. Lorsque deux demandes sont terminées, nous pouvons faire notre travail, comme mettre à jour l'interface utilisateur ou enregistrer les données dans la base de données.

Cependant, si vous utilisez, mapvous ne pouvez pas écrire un code aussi agréable. En un mot, flatMappeut nous aider à sérialiser les requêtes.

Plus confortable
la source
18

Voici un simple règle de pouce que j'utilise me aider à décider que quand utiliser flatMap()sur map()Rx de Observable.

Une fois que vous avez décidé d'utiliser une maptransformation, vous écririez votre code de transformation pour renvoyer un objet, n'est-ce pas?

Si ce que vous retournez comme résultat final de votre transformation est:

  • un objet non observable alors vous utiliseriez justemap() . Et map()enveloppe cet objet dans un observable et l'émet.

  • un Observableobjet, alors vous utiliseriezflatMap() . Et flatMap()déballe l'Observable, sélectionne l'objet retourné, l'enveloppe avec son propre Observable et l'émet.

Disons, par exemple, que nous avons une méthode titleCase (String inputParam) qui renvoie l'objet Titled Cased String du paramètre d'entrée. Le type de retour de cette méthode peut être Stringou Observable<String>.

  • Si le type de retour de titleCase(..)devait être simple String, vous utiliseriezmap(s -> titleCase(s))

  • Si le type de retour titleCase(..)devait être Observable<String>, vous utiliseriezflatMap(s -> titleCase(s))

J'espère que cela clarifie.

karthiks
la source
11

Je voulais juste ajouter qu'avec flatMap, vous n'avez pas vraiment besoin d'utiliser votre propre Observable personnalisé dans la fonction et vous pouvez compter sur des méthodes / opérateurs d'usine standard:

Observable.from(jsonFile).flatMap(new Func1<File, Observable<String>>() {
    @Override public Observable<String> call(final File file) {
        try {
            String json = new Gson().toJson(new FileReader(file), Object.class);
            return Observable.just(json);
        } catch (FileNotFoundException ex) {
            return Observable.<String>error(ex);
        }
    }
});

Généralement, vous devriez éviter de lancer des exceptions (Runtime-) à partir des méthodes onXXX et des rappels si possible, même si nous avons placé autant de sauvegardes que possible dans RxJava.

Akarnokd
la source
Mais je pense que la carte est suffisante. Donc, flatMap et map est une habitude, non?
CoXier
6

Dans ce scénario, utilisez la carte, vous n'avez pas besoin d'un nouvel observable pour cela.

vous devez utiliser Exceptions.propagate, qui est un wrapper afin que vous puissiez envoyer ces exceptions vérifiées au mécanisme rx

Observable<String> obs = Observable.from(jsonFile).map(new Func1<File, String>() { 
    @Override public String call(File file) {
        try { 
            return new Gson().toJson(new FileReader(file), Object.class);
        } catch (FileNotFoundException e) {
            throw Exceptions.propagate(t); /will propagate it as error
        } 
    } 
});

Vous devez ensuite gérer cette erreur dans l'abonné

obs.subscribe(new Subscriber<String>() {
    @Override 
    public void onNext(String s) { //valid result }

    @Override 
    public void onCompleted() { } 

    @Override 
    public void onError(Throwable e) { //e might be the FileNotFoundException you got }
};); 

Il existe un excellent article à ce sujet: http://blog.danlew.net/2015/12/08/error-handling-in-rxjava/

ndori
la source
0

Dans certains cas, vous pourriez finir par avoir une chaîne d'observables, dans laquelle votre observable renverrait une autre observable. «flatmap» déballe la deuxième observable qui est enterrée dans la première et vous permet d'accéder directement aux données que la deuxième observable crache lors de l'abonnement.

Anoop Isaac
la source
0

Flatmap mappe les observables aux observables. Mappez les éléments aux éléments.

Flatmap est plus flexible mais Map est plus léger et direct, donc cela dépend en quelque sorte de votre cas d'utilisation.

Si vous faites TOUT asynchrone (y compris le changement de thread), vous devriez utiliser Flatmap, car Map ne vérifiera pas si le consommateur est supprimé (partie de la légèreté)

skr1p7k1dd
la source