¿Cuándo usas map vs flatMap en RxJava?

180

¿Cuándo usas mapvs flatMapen RxJava ?

Digamos, por ejemplo, que queremos asignar archivos que contienen JSON en cadenas que contienen el JSON:

Usando map, tenemos que lidiar con el de Exceptionalguna manera. ¿Pero cómo?:

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 :(
    }
});

El uso flatMapes mucho más detallado, pero podemos reenviar el problema por la cadena Observablesy manejar el error si elegimos otro lugar e incluso volvemos a intentarlo:

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);
                }
            }
        });
    }
});

Me gusta la simplicidad de la map, pero el manejo de errores de flatmap(no la verbosidad). No he visto ninguna mejor práctica sobre esto flotando y tengo curiosidad por saber cómo se usa esto en la práctica.

Christopher Perry
fuente

Respuestas:

121

maptransforma un evento a otro. flatMaptransforma un evento a cero o más eventos. (Esto está tomado de IntroToRx )

Como desea transformar su json en un objeto, usar map debería ser suficiente.

Tratar con FileNotFoundException es otro problema (el uso de map o flatmap no resolvería este problema).

Para resolver su problema de excepción, simplemente tírelo con una excepción no marcada: RX llamará al controlador onError por usted.

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 misma versión exacta con 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
        }
    }
});

También puede devolver, en la versión flatMap, un nuevo Observable que es solo un error.

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
fuente
2
Esto no llama, subscriber.onError()etc. Todos los ejemplos que he visto tienen errores enrutados de esa manera. ¿Eso no importa?
Christopher Perry
77
Tenga en cuenta que los constructores de OnErrorThrowableare privatey que necesita usar OnErrorThrowable.from(e)en su lugar.
david.mihola
Acabo de actualizar. OnErrorThrowable.from (e) no mantiene el valor, por lo que utilizo OnErrorThrowable.addValueAsLastCause (e, file) en su lugar, lo que debería mantener el valor.
dwursteisen
1
Me gustan los ejemplos de código, pero ayudaría si actualizara la firma de las llamadas flatMap para devolver Observable <String> en lugar de solo String ... porque ¿no es eso técnicamente la diferencia entre los dos?
Rich Ehmer
78

FlatMap se comporta de manera muy similar a map, la diferencia es que la función que aplica devuelve un observable en sí mismo, por lo que es perfectamente adecuada para mapear sobre operaciones asincrónicas.

En el sentido práctico, la función que aplica Map simplemente hace una transformación sobre la respuesta encadenada (no devuelve un Observable); mientras que la función FlatMap aplica devuelve un Observable<T>, por eso se recomienda FlatMap si planea hacer una llamada asincrónica dentro del método.

Resumen:

  • Map devuelve un objeto de tipo T
  • FlatMap devuelve un Observable.

Un claro ejemplo se puede ver aquí: http://blog.couchbase.com/why-couchbase-chose-rxjava-new-java-sdk .

Couchbase Java 2.X Client utiliza Rx para proporcionar llamadas asíncronas de una manera conveniente. Como utiliza Rx, tiene los métodos map y FlatMap, la explicación en su documentación puede ser útil para comprender el concepto general.

Para manejar errores, anule onError en su suscriptor.

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) { }
};

Puede ser útil mirar este documento: http://blog.danlew.net/2014/09/15/grokking-rxjava-part-1/

Puede encontrar una buena fuente sobre cómo administrar los errores con RX en: https://gist.github.com/daschl/db9fcc9d2b932115b679

1vand1ng0
fuente
El resumen está mal. Map y FlatMap devuelven el mismo tipo, pero la función que aplican devuelve un tipo diferente.
CoXier
61

En su caso, necesita un mapa, ya que solo hay 1 entrada y 1 salida.

La función suministrada por map simplemente acepta un artículo y devuelve un artículo que se emitirá más (solo una vez) hacia abajo.

La función proporcionada por flatMap acepta un elemento y luego devuelve un "Observable", lo que significa que cada elemento del nuevo "Observable" se emitirá por separado más abajo.

Puede que el código te aclare las cosas:

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);

Salida:

inside the map item1
item1
inside the flatMap item2
item2+
item2++
item2+++
mt.uulu
fuente
No estoy seguro de si usar un mapa es la mejor idea, aunque funcionaría. Supongamos que FileReader se convertiría en una llamada asincrónica. Entonces necesitaría cambiar el mapa a un plano. Dejarlo como un mapa significaría que no se dispararían los eventos como se esperaba y causaría confusión. Esto me ha mordido varias veces ya que todavía estoy aprendiendo RX Java. Creo que flatMap es una forma segura de garantizar que las cosas se procesen como esperabas.
user924272
24

La forma en que lo pienso es que usas flatMapcuando la función que querías poner dentro de map()devuelve un Observable. En cuyo caso aún podría intentar usarlo, map()pero no sería práctico. Déjame intentar explicar por qué.

Si en ese caso decidieras seguir map, obtendrías un Observable<Observable<Something>>. Por ejemplo, en su caso, si utilizamos una biblioteca RxGson imaginaria, que ha devuelto un Observable<String>de ella del toJson()método (en lugar de simplemente devolver una String) que se vería así:

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

En este punto, sería bastante complicado para subscribe()un observable. Dentro de él, obtendría un valor Observable<String>al que volvería a necesitar subscribe(). Lo cual no es práctico ni agradable de ver.

Entonces, para que sea útil, una idea es "aplanar" este observable de observables (puede comenzar a ver de dónde viene el nombre _flat_Map). RxJava proporciona algunas formas de aplanar los observables y, por simplicidad, supongamos que fusionar es lo que queremos. Merge básicamente toma un montón de observables y emite cada vez que alguno de ellos emite. (Mucha gente argumentaría que cambiar sería un valor predeterminado mejor. Pero si está emitiendo un solo valor, no importa de todos modos).

Entonces, modificando nuestro fragmento anterior obtendríamos:

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

Esto es mucho más útil, porque suscribiéndote a eso (o mapeando, filtrando o ...) solo obtienes el Stringvalor. (Además, merge()ten en cuenta que tal variante de no existe en RxJava, pero si entiendes la idea de la fusión, espero que también entiendas cómo funcionaría eso).

Básicamente, porque merge()probablemente esto solo debería ser útil cuando se logra un map()retorno de un observable y para que no tenga que escribir esto una y otra vez, flatMap()se creó como una abreviatura. Aplica la función de mapeo como lo map()haría normalmente , pero luego, en lugar de emitir los valores devueltos, también los "aplana" (o fusiona).

Ese es el caso de uso general. Es más útil en una base de código que usa Rx en todo el lugar y tiene muchos métodos que devuelven observables, que desea encadenar con otros métodos que devuelven observables.

En su caso de uso, también resulta útil, ya map()que solo puede transformar un valor emitido en onNext()otro valor emitido en onNext(). Pero no puede transformarlo en múltiples valores, ningún valor o un error. Y como akarnokd escribió en su respuesta (y ten en cuenta que es mucho más inteligente que yo, probablemente en general, pero al menos cuando se trata de RxJava) no deberías lanzar excepciones de tu parte map(). Entonces, en su lugar, puede usar flatMap()y

return Observable.just(value);

cuando todo va bien, pero

return Observable.error(exception);

cuando algo falla
Consulte su respuesta para obtener un fragmento completo: https://stackoverflow.com/a/30330772/1402641

Marcin Koziński
fuente
1
Esta es mi respuesta favorita. básicamente, termina anidando un observable EN un SI observable que es lo que devuelve su método.
filthy_wizard
21

La pregunta es ¿ Cuándo usas map vs flatMap en RxJava? . Y creo que una demostración simple es más específica.

Cuando desee convertir un elemento emitido a otro tipo, en su caso, la conversión de archivos a String, map y flatMap puede funcionar. Pero prefiero el operador de mapa porque es más claro.

Sin embargo, en algún lugar, flatMappuede hacer un trabajo mágico pero mapno puede. Por ejemplo, quiero obtener la información de un usuario, pero primero tengo que obtener su identificación cuando el usuario inicia sesión. Obviamente necesito dos solicitudes y están en orden.

Vamos a empezar.

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

Observable<UserInfo> fetchUserInfo(String userId);

Aquí hay dos métodos, uno para el inicio de sesión devuelto Responsey otro para obtener información del usuario.

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

Como puede ver, en la función se aplica flatMap, al principio obtengo la identificación del Responseusuario y luego obtengo la información del usuario. Cuando finalizan dos solicitudes, podemos hacer nuestro trabajo, como actualizar la IU o guardar datos en la base de datos.

Sin embargo, si lo usa, mapno puede escribir un código tan agradable. En una palabra, flatMappuede ayudarnos a serializar solicitudes.

CoXier
fuente
18

Aquí es un simple regla básica que utilizo como ayuda a decidir cuándo usar flatMap()más map()de Rx de Observable.

Una vez que decida que va a emplear una maptransformación, escribiría su código de transformación para devolver algún Objeto, ¿verdad?

Si lo que está devolviendo como resultado final de su transformación es:

  • un objeto no observable que usarías solomap() . Y map()envuelve ese objeto en un Observable y lo emite.

  • un Observableobjeto, entonces lo usaríasflatMap() . Y flatMap()desenvuelve el Observable, recoge el objeto devuelto, lo envuelve con su propio Observable y lo emite.

Digamos, por ejemplo, que tenemos un método titleCase (String inputParam) que devuelve el objeto Titled Cased String del parámetro de entrada. El tipo de retorno de este método puede ser Stringo Observable<String>.

  • Si el tipo de devolución titleCase(..)fuera simple String, entonces usaríasmap(s -> titleCase(s))

  • Si el tipo de retorno titleCase(..)fuera Observable<String>, entonces usaríasflatMap(s -> titleCase(s))

Espero que eso aclare.

karthiks
fuente
11

Solo quería agregar eso flatMap, realmente no necesita usar su propio Observable personalizado dentro de la función y puede confiar en los métodos / operadores estándar de fábrica:

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);
        }
    }
});

En general, debe evitar lanzar excepciones (Runtime-) de los métodos onXXX y las devoluciones de llamada si es posible, a pesar de que pusimos tantas salvaguardas como pudimos en RxJava.

akarnokd
fuente
Pero creo que el mapa es suficiente. Entonces flatMap y map es un hábito ¿verdad?
CoXier
6

En ese escenario, use el mapa, no necesita un nuevo Observable para él.

debe usar Exceptions.propagate, que es un contenedor para poder enviar esas excepciones marcadas al mecanismo de 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
        } 
    } 
});

Entonces deberías manejar este error en el suscriptor

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 }
};); 

Hay una excelente publicación para ello: http://blog.danlew.net/2015/12/08/error-handling-in-rxjava/

ndori
fuente
0

En algunos casos, podría terminar teniendo una cadena de observables, en donde su observable devolvería otro observable. 'flatmap' desenvuelve el segundo observable que está enterrado en el primero y le permite acceder directamente a los datos que el segundo observable está escupiendo mientras se suscribe.

Anoop Isaac
fuente
0

Mapa plano asigna observables a observables. Mapa asigna elementos a elementos.

Flatmap es más flexible, pero Map es más ligero y directo, por lo que depende un poco de su caso de uso.

Si está haciendo CUALQUIER COSA asíncrona (incluido el cambio de hilos), debe usar Flatmap, ya que Map no verificará si el consumidor está dispuesto (parte de la ligereza)

skr1p7k1dd
fuente