Slide 1

Slide 1 text

RXJAVA PROGRAMAÇÃO REATIVA FUNCIONAL UBIRATAN SOARES JULHO / 2016

Slide 2

Slide 2 text

MOTIVAÇÃO Java8 trouxe uma API de Streams, que permite operações comuns sobre coleções como Map, Filter, Reduce e Collect Java9 trará uma API de ReactiveStreams, adotadando muitos conceitos presentes em RxJava diretamente no JDK Funcional Programming ajuda a escrever código mais robusto e conciso de maneira geral

Slide 3

Slide 3 text

ERIK MEIJER “Do ponto de vista de fluxo de dados, qual a diferença entre consultar o DB e processar as posições do ponteiro do mouse ???”

Slide 4

Slide 4 text

VELHOS PROBLEMAS SEMPRE NA MODA Execução orquestrada de código de forma assíncrona Execução e sincronização de processamento concorrente Tratamento de erros Escalabilidade

Slide 5

Slide 5 text

UMA NOVA FORMA DE PENSAR E se ao invés de obter dados de forma síncrona, os dados chegassem até mim de forma assíncrona ? Reativo : algo que reage a um estímulo !!! Fundamento matemático : teoria da categorias !!!

Slide 6

Slide 6 text

REACTIVE MANIFESTO As demandas atuais tipicamente pedem aplicações Responsivas Resilientes Orientadas a eventos Escaláveis

Slide 7

Slide 7 text

HANS DOCKTER “Programar é arte de encontrar as abstrações corretas”

Slide 8

Slide 8 text

RELEITURA DE CONCEITOS Pensamento em termos de fluxo de dados : eventos discretos e fluxo desses eventos É possível reagir a eventos e combinar os mesmos O estado do sistema deve mudar conforme a passagem de eventos no tempo Eventos no fluxo são imutáveis Sistema que idealmente nunca bloqueia (I/O, cálculos, etc)

Slide 9

Slide 9 text

DADOS E SINCRONICIDADE Um valor Múltiplos valores Síncrona T getData( ) Iterable getData( ) Assíncrona Future getData( ) Observable getData( )

Slide 10

Slide 10 text

RELEITURA DE CONTRATOS Iterable Observer Obter o próximo T next( ) onNext( T ) Sinalizar erro throws Exception( ) onError( Thowable ) Saber se terminou hasNext( ) onComplete( ) PULLED WAY PUSHED WAY

Slide 11

Slide 11 text

CONCEITOS BÁSICOS OBSERVABLE OBSERVER OPERATOR SCHEDULER

Slide 12

Slide 12 text

OBSERVABLE Representa o fluxo de dados (ou eventos, ou itens emitidos) Por padrão, executa de forma sequencial (não concorrente) Repassa cada item emitido para um observador (callback) Pode ser associado ao conceito de source da Streams API do java8, porém seguindo push model quanto aos dados

Slide 13

Slide 13 text

CRIANDO OBSERVABLES (I) Observable source = Observable.just("GOOGLE", "APPLE", "MICROSOFT"); source.subscribe(new Action1() { @Override public void call(String s) { System.out.println(s) } }); GOOGLE APPLE MICROSOFT PROCESS FINISHED WITH EXIT CODE 0

Slide 14

Slide 14 text

CRIANDO OBSERVABLES (II) Observable source = Observable.just("GOOGLE", "APPLE", “MICROSOFT"); source.subscribe((String)company -> System.out.println(company)); Observable.just("GOOGLE", "APPLE", "MICROSOFT") .subscribe(System.out::println);

Slide 15

Slide 15 text

CRIANDO OBSERVABLES (III) Observable.fromCallable(() -> “RxJava is Awesome”) .subscribe(System.out::println); List names = Arrays.asList("Banana", "Apple", "Orange"); Observable.from(names).subscribe(System.out::println);

Slide 16

Slide 16 text

CRIANDO OBSERVABLES (IV) Observable.create( new Observable.OnSubscribe() { @Override public void call( final Subscriber super SomeEventType> subscriber) { . . . // Wrap some callback and emits new events } }) ); BEW ARE, THINK TW ICE

Slide 17

Slide 17 text

CRIANDO OBSERVABLES (V) Observable.interval(1, TimeUnit.SECONDS) .map(time -> "AT SECOND " + time) .subscribe(System.out::println); AT SECOND 0 AT SECOND 1 AT SECOND 2 AT SECOND 3 … WARNING : estamos roubando aqui, se você executar esse código, não verá esse log !!!!

Slide 18

Slide 18 text

OBSERVER / SUBSCRIBER Consome o fluxo de dados Respeita o contrato no estilo pushed data É o ponto no qual os erros flutuam : um erro que aconteça durante uma operação interrompe a sequência de emissões Callbacks sinalizam o término da sequência Adições ao Observer Pattern do GOF

Slide 19

Slide 19 text

OBSERVER public interface Observer { } void onCompleted(); void onError(Throwable e); void onNext(T data);

Slide 20

Slide 20 text

source.subscribe(new Observer() { @Override public void onCompleted() { … } @Override public void onError(Throwable e) { … } @Override public void onNext(Item item) { … } } ); source.subscribe(new Subscriber() { @Override public void onCompleted() { … } @Override public void onError(Throwable e) { … } @Override public void onNext(Item item) { … } } ); ?????????

Slide 21

Slide 21 text

No content

Slide 22

Slide 22 text

Observable.fromCallable(() -> “Better subscribing with actions”) .subscribe( System.out::println, throwable -> someErrorAction(), () -> done() ); COM ACTIONS

Slide 23

Slide 23 text

PIPELINE DE OPERAÇÕES Observable.from(…) .flatMap(…) .filter(…) .map(…) .observeOn(…) .subscribeOn(…) .subscribe(…); upstream sequence downstream sequence

Slide 24

Slide 24 text

Observable.zip( restApi.getAvaliableItems(), restApi.getRecommendedItems(clientId), new ItemsResultsZipper()) .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .subscribe(new Observer>() { @Override public void onCompleted() { … } @Override public void onError(Throwable e) { … } @Override public void onNext(List items) { … } }); podemos fazer melhor ?

Slide 25

Slide 25 text

OPERADORES Funções que permitem manipular a sequência de eventos observáveis, sejam os itens emitidos, seja a própria sequência (ou múltiplas delas) Reactive Extensions define um enorme catálogo de operadores quanto à semântica, em categorias bem definidas

Slide 26

Slide 26 text

CATEGORIA OPERADORES CRIAÇÃO just( ), from( ), range( ), interval( ), defer( ) … COMBINAÇÃO zip( ), merge( ), combineLatest( ), concat( ) … TRANSFORMAÇÃO map( ), flatMap( ), concatMap( ) … FILTRAGEM filter( ), take( ), skip( ), debounce( ) … MUITO MAIS ! cache( ), replay( ), retry( ), retryWhen( ) …

Slide 27

Slide 27 text

MARBLE DIAGRAMS A B C D E onNext( ) é chamado cinco vezes

Slide 28

Slide 28 text

MARBLE DIAGRAMS A B C 1 2 3 4 5 x I onError( ) onCompleted( )

Slide 29

Slide 29 text

MAP 1 2 3 4 5 A B C D E MAP { INT X -> CHAR Y }

Slide 30

Slide 30 text

FILTER 1 2 3 4 5 2 4 FILTER { INT X -> INT Y}

Slide 31

Slide 31 text

MERGE 1 3 5 2 4 1 2 3 4 5 MERGE I I I

Slide 32

Slide 32 text

CONCAT 1 3 5 2 4 1 3 5 2 4 CONCAT I I I

Slide 33

Slide 33 text

AMB 1 3 5 2 4 1 3 5 AMB I I I

Slide 34

Slide 34 text

INTERVAL INTERVAL {} 1 2 3 4 5

Slide 35

Slide 35 text

rxmarbles.com Disponível como app Android

Slide 36

Slide 36 text

SCHEDULER Escalonador de trabalho entre thread pools distintas Abstração em volta de Executors ( java.util.concurrent ) Qualquer Observable pode ser produzido em um Scheduler e observado em outro Mecanismo fundamental para aplicações móveis

Slide 37

Slide 37 text

Observable.range(1, 2) .map(i -> i * 2) .observeOn(Schedulers.io()) .doOnNext(i -> System.out.println( "Emitting " + i + " on thread " + threadName())) .observeOn(Schedulers.computation()) .map(i -> i * 10) .subscribe(i -> System.out.println( "Received " + i + " on thread " + threadName())); Emitting 2 on thread RxCachedThreadScheduler-1 Received 20 on thread RxComputationThreadPool-3 Emitting 4 on thread RxCachedThreadScheduler-1 Received 40 on thread RxComputationThreadPool-3 sleep(3000);

Slide 38

Slide 38 text

Observable.just("Google", "Apple", "Microsoft", "IBM") .subscribleOn(Schedulers.computation()) .subscribe(s -> System.out.println( "Received " + s + " on thread " + threadName())); Received Google on thread RxComputationThreadPool-1 Received Apple on thread RxComputationThreadPool-1 Received Microsoft on thread RxComputationThreadPool-1 Received IBM on thread RxComputationThreadPool-1 sleep(3000);

Slide 39

Slide 39 text

NUÂNCIAS SOBRE SCHEDULERS (I) Schedulers.io( ) encapsula um thread pool de tamanho variável Schedulers.computation( ) encapsula um thread pool de tamanho fixo Operam sobre deamon threads São extensíveis para encapsular threads importantes, como por exemplo a UI thread do Android ou do JavaFX Alguns factory methods de Observable já associam um scheduler à sequência (como interval( ) )

Slide 40

Slide 40 text

NUÂNCIAS SOBRE SCHEDULERS (II) subscribeOn( ) instrui o framework sobre em qual thread os itens as emissões iniciam para o consumidor observeOn( ) instrui o framework sobre quais schedulers podem operar nas etapas intermediárias do pipeline de operações subscribeOn( ) em geral é usado uma vez no, início do pipeline observeOn( ) é utilizado conforme a necesside de concorrência nas etapas do pipeline

Slide 41

Slide 41 text

TRATAMENTO DE ERROS Evento terminal destrói a sequência. Ponto. Recuperação via operadores onErrorResumeNext( ), onErrorReturn( ) e outros Sugestão : implemente um wrapper com o retorno de onError( throwable), isso ajudará no stacktrace em casos de erro Sugestão : verificar semântica de onExceptionResumeNext( )

Slide 42

Slide 42 text

CONCEITOS AVANÇADOS

Slide 43

Slide 43 text

FLATMAP "Transform the items emitted by an Observable into Observables, then flatten the emissions from those into a single Observable The FlatMap operator transforms an Observable by applying a function that you specify to each item emitted by the source Observable, where that function returns an Observable that itself emits items. FlatMap then merges the emissions of these resulting Observables, emitting these merged results as its own sequence. This method is useful, for example, when you have an Observable that emits a series of items that themselves have Observable members or are in other ways transformable into Observables, so that you can create a new Observable that emits the complete collection of items emitted by the sub-Observables of these items. "

Slide 44

Slide 44 text

No content

Slide 45

Slide 45 text

FLATTENING (MERGE) MAPPING (PROVIDED FUNCTION)

Slide 46

Slide 46 text

List companies = Arrays.asList("Google", "Apple", "Microsoft"); Google Apple Microsoft Larry Steve Bill flatMap( ) desmontou 2 sequências de items e juntou individualmente os itens de cada sequência em uma única sequência final mapeamento Observable> obs = Observable.just(companies, leaders); Observable flat = obs.flatMap(strings -> Observable.from(strings)); flat.subscribe(System.out::println); List leaders = Arrays.asList("Larry", "Steve", "Bill");

Slide 47

Slide 47 text

MAIS SOBRE FLATMAP flatmap( ) transforma em sequência em outra através da função de mapeamento flatmap( ) é um dos mecanismos para encadeamento de operações assíncronas flatmap( ) não garante a ordem dos itens emitidos na sequência gerada final após o merging; se a ordem dos itens for importante, usar concatMap( )

Slide 48

Slide 48 text

ESTUDO DE CASO : STAR WARS REST API https://swapi.co/ Consumir com Retrofit2 Consulta simples em um endpoint : lista de personagens Consulta encadeada em 2 endpoints : dado um personagem aleatório, em quais filmes ele aparece?

Slide 49

Slide 49 text

No content

Slide 50

Slide 50 text

public class People {
 
 public String name;
 public String gender; public List films; } public class PeopleResults {
 public List results; }

Slide 51

Slide 51 text

public class Movie {
 
 public String title;
 public String director; public String releaseDate; }

Slide 52

Slide 52 text

public interface StarWarsAPI { @GET(“people“) Observable people(); @GET(“film/{film_id}“) Observable movieById( @Path("film_id") String id, ); } public StarWarsAPI createAPI() {
 
 Retrofit retrofit = new Retrofit.Builder()
 .baseUrl(API_URL)
 .addConverterFactory(GsonConverterFactory.create())
 .addCallAdapterFactory(RxJavaCallAdapterFactory.create())
 .build();
 
 return retrofit.create(StarWarsAPI.class); }

Slide 53

Slide 53 text

starWarsAPI.people() .subscribeOn(Schedulers.io()) .flatMap(payload -> Observable.from(payload.results)) .subscribe(
 System.out::println,
 Throwable::printStackTrace,
 () -> System.out::println("Done -> All People")
 ); PEOPLE{NAME='LUKE SKYWALKER', GENDER='MALE'} PEOPLE{NAME='C-3PO', GENDER='N/A'} PEOPLE{NAME='R2-D2', GENDER='N/A'} PEOPLE{NAME='DARTH VADER', GENDER=‘MALE'} [ … ] PEOPLE{NAME='OBI-WAN KENOBI', GENDER='MALE'} DONE -> ALL PEOPLE // List

Slide 54

Slide 54 text

starWarsApi.people() .subscribeOn(Schedulers.io()) .flatMap(payload -> selectRandomPeople(payload.results)) .doOnNext(System.out::println) .flatMap(people -> Observable.from(people.films)) .flatMap(filmUrl -> { String filmId = ResourceIdExtractor.idFromUrl(filmUrl); return api.movieById(filmId) .subscribeOn(Schedulers.io());
 }) .subscribe(System.out::println); PEOPLE{NAME='OWEN LARS', GENDER=‘MALE'} MOVIE{TITLE='ATTACK OF THE CLONES', EPISODE=2, DIRECTOR='GEORGE LUCAS', RELEASEDAY='2002-05-16'} MOVIE{TITLE='A NEW HOPE', EPISODE=4, DIRECTOR='GEORGE LUCAS', RELEASEDAY='1977-05-25'} MOVIE{TITLE='REVENGE OF THE SITH', EPISODE=3, DIRECTOR='GEORGE LUCAS', RELEASEDAY='2005-05-19'}

Slide 55

Slide 55 text

OBSERVABLES ESPECIAIS (I) Observable.empty( ) é uma sequêcia que finaliza sem emitir itens, com sucesso Observable.error( ) aceita um Throwable e emite uma sequência com o estado terminal de erro Observable.never( ) é uma sequência que não emite nenhum item, nunca Observable.toBlocking( ) é um mecanismo para executar a emissão de itens de modo síncrono

Slide 56

Slide 56 text

Single é um Observable que emite um único item Completable é uma sequência que não emite nenhum item, mas emite os eventos terminais Retrofit2 pode proxyar um Single (2.0.0 final em diante) Completable é um Observable (muito!) mais leve para interessados no sucesso ou fracasso de uma sequência OBSERVABLES ESPECIAIS (II)

Slide 57

Slide 57 text

Single single = Observable.range(1, 10).first().toSingle(); single.subscribe(System.out::println); // 1 Observable source = Observable.just("Lets See"); Completable.fromObservable(source)
 .subscribe(() -> System.out.println(“YES!")); // YES Observable inevitable = Observable.error(new Exception("Ouch!")); Completable.fromObservable(inevitable)
 .subscribe(
 throwable -> System.out.println("FAIL"),
 () -> System.out.println("DONE”) ); // FAIL

Slide 58

Slide 58 text

TRANSFORMERS Aplicáveis através do operador compose( ) Manipulam a sequência como um todo, ideais para evitar repetições de código em etapas comuns do pipeline Casos comuns : setup de schedulers, composição de filtros, etc

Slide 59

Slide 59 text

restApi.callMethod01() .subscribleOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .subscribe(…); restApi.callMethod02() .subscribleOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .subscribe(…); DRY ?

Slide 60

Slide 60 text

public static Observable.Transformer setupSchedulers() { return observable -> observable.subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()); } restApi.callMethod01() .compose(Transformers.setupSchedulers() ) .subscribe(…); restApi.callMethod02() .compose(Transformers.setupSchedulers() ) .subscribe(…);

Slide 61

Slide 61 text

SUBSCRIPTION Subscription representa o resultado de um Observer / Subscriber ser associado a um Observable Permitem deixar de acompanhar a sequência “por fora”, eg, de forma explícita via unsubscribe( ) Mecanismo possível para evitar memory leaks no Android Múltiplas sequências podem ser agrupadas via CompositeSubscription

Slide 62

Slide 62 text

Subscription first = Observable.interval(1, TimeUnit.SECONDS)
 .subscribe(System.out::print);
 
 Subscription second = Observable.range(1, 100000)
 .subscribe(System.out::print);
 
 CompositeSubscription subs = new CompositeSubscription();
 subs.add(first);
 subs.add(second); // . . . 
 first.unsubscribe(); subs.add(third); // . . . 
 if(subs.hasSubscriptions()) subs.unsubscribe(); Desinscreve-se de todos as sequências participantes

Slide 63

Slide 63 text

SUBJECTS Subject é alguém que implementa tanto Observable quanto Observer Também aceitam múltiplos Subscribers Quatro variantes : Async, Behavior, Publish e Replay Ideais para particionar uma sequência em vários fluxos de eventos diferentes Bastante flexíveis, porém traiçoeiros

Slide 64

Slide 64 text

PUBLISH SUBJECT SUBSCRIBE SUBSCRIBE

Slide 65

Slide 65 text

PublishSubject subject = PublishSubject.create(); From first : 1 From second : ODD From first : 2 From second : EVEN subject.subscribe(
 integer -> System.out.println("From first : " + integer),
 throwable -> reportError(),
 () -> done()
 ); subject.map(number -> number / 2 != 0 ? "ODD" : "EVEN")
 .subscribe(s -> System.out.println("From second : " + s)); Observable.range(1, 2).subscribe(subject);

Slide 66

Slide 66 text

HOT AND COLD OBSERVABLES Hot Observables começam a emitir itens no momento em que são criados Cold Observables começam a emitir itens no momento em que são observados Hot Observables são passíveis de perda de emissões Cold Observables podem produzir side-effects Alguns operadores transformam a temperatura da sequência

Slide 67

Slide 67 text

BACKPRESSURE Cenário no qual a sequência observável (produtor) emite mais itens do que o observador (consumidor) consegue processar Menos provável em sequências de itens de natureza estritamente computacional Mais provável em sequências sendo geradas e consumidas por IO

Slide 68

Slide 68 text

LIDANDO COM BACKPRESSURE Dois tipos de cenário, de acordo com a natureza dos itens emitidos Perda de emissões implica em perda de informação Perda de emissões não implica perda de informação

Slide 69

Slide 69 text

BACKPRESSURE COM PERDA DE DADOS Estratégia : diminuir a quantidade de dados que chegam até o consumidor segundo alguma heurística de perda Diversos operadores disponíveis : sample( ), debounce( ), take( ), throttleFirst( ), etc

Slide 70

Slide 70 text

BACKPRESSURE SEM PERDA DE DADOS Duas estratégias imediatas (i) Bufferizar dados para processamento posterior, com operadores como cache( ), buffer( ), window( ) etc (ii) Informar a upstream sequence que é preciso diminuir o ritmo na emissão de itens, usando Producer API

Slide 71

Slide 71 text

JAVA8 STREAMS List partners = Arrays.asList(
 "Caelum", “Google", "7Comm", "Porto", "Oxigenio",
 "Intel", "IBM", "Globalcode", "Samsung", "Novatec",
 "Paypal", "Twitter", "Facebook", "USP", "Impacta" );
 
 Optional result = partners.parallelStream()
 .filter(s -> s.length() > 3)
 .map(String::length)
 .distinct()
 .reduce((a, b) -> a + b);
 
 if(result.isPresent()) System.out.println(result.get()); // 36

Slide 72

Slide 72 text

RXJAVA VS JAVA8 STREAMS (I) Streams são desenhadas para processamento computacional, in-memory e com fácil paralelização Streams trazem Collectors API para melhor semântica de agrupamentos Streams pode adaptar sources através de Spliterators API Streams operam estritamente em pull mode, eg, não baseado em callbacks Não há backpressure

Slide 73

Slide 73 text

RXJAVA VS JAVA8 STREAMS (II) RxJava opera essencialmente em push mode, eg, baseado em callbacks RxJava contém a noção de tempo na API, permitindo inclusive sincronização de eventos no tempo RxJava oferece controle granular sobre concorrência via Schedulers RxJava é desenhada tanto para uso de processamento computacional quanto para IO (onde brilha muito!) RxJava tende a ser mais burocrática e/ou pesada que Streams para processamento computacional tipo map/filter/collect/reduce

Slide 74

Slide 74 text

E NO DIA A DIA ?? Mas afinal …

Slide 75

Slide 75 text

RX-LIFECYCLE public class ActivityWithObsersable extends RxActivity { @Override public void onResume() { super.onResume(); someObservable .compose(bindToLifecycle()) .subscribe(); } }

Slide 76

Slide 76 text

RX-BINDING @Override public boolean onCreateOptionsMenu(Menu menu) { getMenuInflater().inflate(R.menu.menu_search_by_terms, menu);
 MenuItem search = menu.findItem(R.id.search);
 SearchView searchView = (SearchView) MenuItemCompat.getActionView(search);
 
 RxSearchView.queryTextChangeEvents(searchView)
 .throttleLast(100, TimeUnit.MILLISECONDS)
 .debounce(300, TimeUnit.MILLISECONDS)
 .observeOn(AndroidSchedulers.mainThread())
 .subscribe(this::proceedWithQuery);
 
 return true;
 }

Slide 77

Slide 77 text

¯\_(ϑ)_/¯

Slide 78

Slide 78 text

No content

Slide 79

Slide 79 text

PROBLEMAS COM RXJAVA Debugging pode ser difícil (stacktraces gigantescos) Verbosidade (Java 6,7) Uso descuidado pode levar a problema de leaks memória Curva de aprendizado : muitos conceitos, em geral complexos, que exigem atenção aos detalhes

Slide 80

Slide 80 text

CONCLUSÕES RxJava é (muito!) superior do que opções do framework do Android para concorrência (eg AsyncTask) RxJava permite execução orquestrada de código assíncrono e/ ou concorrente de forma declarativa RxJava permite operacionais funcionais sobre dados de maneira retrocompatível RxJava é BATTLE-TESTED RxJava ajuda a resolver melhor problemas difíceis

Slide 81

Slide 81 text

REFERÊNCIAS (I) "Functional Reactive Programming with RxJava" by Ben Christensen https://youtu.be/_t06LRX0DV0 “Learning RxJava (for Android) by example“ by Kaushik Goupal https://youtu.be/k3D0cWyNno4 “Demystifying RxJava Subscribers" by Jake Wharton https://youtu.be/NVKmyK6sd-Q “What does it mean to be Reactive ?” by Erik Meijer https://youtu.be/sTSQlYX5DU0

Slide 82

Slide 82 text

REFERÊNCIAS (II) "Grokking RxJava Series” by Dan Lew http://blog.danlew.net/2014/09/15/grokking-rxjava-part-1/ “The Introduction to Reactive Programming you`ve been missing” by André Staltz https://gist.github.com/staltz/868e7e9bc2a7b8c1f754 Oficial RxJava Wiki by NetFlix https://github.com/ReactiveX/RxJava/wiki Advanced RxJava Blog by David Karnok akarnokd.blogspot.com

Slide 83

Slide 83 text

REFERÊNCIAS (III) GradleLambda : https://github.com/evant/gradle-retrolambda RxAndroid : https://github.com/ReactiveX/RxAndroid RxLifecycle : https://github.com/trello/RxLifecycle RxBinding : https://github.com/JakeWharton/RxBinding RxRelay : https://github.com/JakeWharton/RxRelay Frodo : https://github.com/android10/frodo

Slide 84

Slide 84 text

UBIRATAN SOARES Computer Scientist by ICMC/USP Software Engineer @ Luiza Labs Google Developer Expert for Android Intel Software Innovator for Android Teacher, speaker, etc, etc

Slide 85

Slide 85 text

OBRIGADO THAT`S ALL FOLKS !!! @ubiratanfsoares speakerdeck.com/ubiratansoares ubiratansoares.github.io