RxJava, Tips and Tricks

Yaroslav Heriatovych

RxJava

Background

Observable

Observable




Observables fill the gap by being the ideal implementation of access to asynchronous sequences of multiple items
single items multiple items
synchronous T getData() Iterable<T> getData()
asynchronous Future<T> getData() Observable<T> getData()

Primitives

 public interface Observer <T>  {
    void onCompleted();
    void onError(java.lang.Throwable throwable);
    void onNext(T t);
 }
public class Observable <T>  {
    public final static <T> Observable<T> create(OnSubscribe<T> f)
    public rx.Subscription subscribe(rx.Observer<? super T> observer)
    // ... 
}

public static interface OnSubscribe<T> extends Action1<Subscriber<? super T>> {}

public interface Subscription {
    public void unsubscribe();
    public boolean isUnsubscribed();
}
public abstract class Subscriber<T> implements Observer<T>, Subscription {...}

Create Observable

Observable<String> o = Observable.from("a", "b", "c");
Observable<String> o = Observable.just("one object"); 

Observable<String> o = Observable.create(new Observable.OnSubscribe<String>() {
    @Override                                                                  
    public void call(Subscriber<? super String> subscriber) {                  
        subscriber.onNext("Hello");                                            
        subscriber.onNext("World");                                            
        subscriber.onCompleted();                                              
    }                                                                          
});                                                                            

Transform Observable

o.skip(10).take(5)                        
        .map(new Func1<String, String>() {
            @Override                     
            public String call(String s) {
                return s.toUpperCase();   
            }                             
        })                                
        .subscribe(new Action1<String>() {
            @Override                     
            public void call(String s) {  
                Log.d("rx", s)            
            }                             
        });                               

Schedulers

o.skip(10).take(5)                                
        .observeOn(Schedulers.computation())      
        .map(new Func1<String, String>() {        
            @Override                             
            public String call(String s) {        
                return s.toUpperCase();           
            }                                     
        })                                        
        .observeOn(AndroidSchedulers.mainThread())
        .subscribe(new Action1<String>() {        
            @Override                             
            public void call(String s) {          
                Log.d("rx", s)                    
            }                                     
        });                                       

RxJava: Library, 

not Framework

Lambda

the new old thing

Used lambdas before it was cool

Observable<Integer> xs = Observable.just(1, 2, 3, 4, 5, 6, 7);
        xs.filter(new Func1<Integer, Boolean>() {
            @Override
            public Boolean call(Integer x) {
                return x % 2 == 0;
            }
        }).map(new Func1<Integer, Integer>() {
            @Override
            public Integer call(Integer x) {
                return x + 10;
            }
        }).subscribe(new Action1<Integer>() {
            @Override
            public void call(Integer x) {
                Rx.this.print(x);
            }
        });

Anonymous classes hide logic behind the noise 

More code - more bugs

Map

Filter

        Observable<Integer> xs = Observable.just(1, 2, 3, 4, 5, 6, 7);
        xs.filter(x -> x % 2 == 0)
                .map(x -> x + 10)
                .subscribe(x -> print(x));
  • Less noise
  • Clear logic
  • Concise 

Once more:

  • Java8 for android development
  • Native support in Android Studio
  • Magic

One more thing

It's safer

static class MyActivity extends Activity {              
    Handler handler = new Handler();                    
    @Override                                           
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);             
                                                        
        String str = "uamobile";                        
        handler.postDelayed(new Runnable() {            
            @Override                                   
            public void run() {                         
                Log.d("Rx", str);                       
            }                                           
        }, 10000);                                      
    }                                                   
}                                                       
class MyActivity$1 
extends java.lang.Object implements java.lang.Runnable{
final java.lang.String val$str;

final MyActivity this$0;

MyActivity$1(cMyActivity, java.lang.String);
  Code:
   0:	aload_0
   1:	aload_1
   2:	putfield	
   5:	aload_0
   6:	aload_2
   7:	putfield	
   10:	aload_0
   11:	invokespecial	
   14:	return

public void run();
  Code:
   0:	ldc	
   2:	aload_0
   3:	getfield	
   6:	invokestatic	
   9:	pop
   10:	return
}

One more thing

It's safer

static class MyActivity extends Activity {              
    Handler handler = new Handler();                    
    @Override                                           
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);             
                                                        
        String str = "uamobile";                        
        handler.postDelayed(() ->                       
                Log.d("Rx", str), 10000);               
    }                                                   
}                                                       
final class MyActivity$$Lambda$1 
    extends java.lang.Object 
    implements java.lang.Runnable{
public void run();
  Code:
   0:	aload_0
   1:	getfield	
   4:	invokestatic	
   7:	return

public static java.lang.Runnable 
    lambdaFactory$(java.lang.String);
  Code:
   0:	new	
   3:	dup
   4:	aload_0
   5:	invokespecial
   8:	areturn
}

One abstraction

to rule them all

final View.OnClickListener clickListener =  
        new View.OnClickListener() {        
            @Override                       
            public void onClick(View view) {
                //...                       
            }                               
        };                                  
new BroadcastReceiver() {                 
    @Override                             
    public void onReceive(Context context,
                          Intent intent) {
        //...                             
    }                                     
};                                        
com.squareup.okhttp.Callback okCallback =            
        new com.squareup.okhttp.Callback() {         
            @Override                                
            public void onFailure(Request request,   
                                  IOException e) {   
                //...                                
            }                                        
                                                     
            @Override                                
            public void onResponse(Response response)
                    throws IOException {             
                //...                                
            }                                        
        };                                           
Observable<Void> click
Observable<Intent> broadcasts
Observable<Response> responses
ViewObservable.text(editText)                     //Observable<OnTextChangeEvent>
        .map(event -> event.text)                 //Observable<CharSequence>     
        .filter(cs -> !TextUtils.isEmpty(cs))     //Observable<CharSequence>     
        .map(cs -> cs.toString().toUpperCase())   //Observable<String>           
        .take(5)                                  //Observable<String>           
        .subscribe(str -> handleString(str));     //Subscription                 
editText.addTextChangedListener(new TextWatcher() {                
    @Override                                                      
    public void beforeTextChanged(CharSequence charSequence,       
                                  int i, int i2, int i3) {}        
    @Override                                                      
    public void onTextChanged(CharSequence charSequence,           
                              int i, int i2, int i3) {}            
                                                                   
    int count = 5;                                                 
    @Override                                                      
    public void afterTextChanged(Editable editable) {              
        if (!TextUtils.isEmpty(editable)) {                        
            String transformed = editable.toString().toUpperCase();
            handleString(transformed);                             
            count--;                                               
            if (count == 0) {                                      
                editText.removeTextChangedListener(this);          
            }                                                      
        }                                                          
    }                                                              
});                                                                

Events as a stream

take(n)

Abstract over event producer

Observable<CharSequence> input = ViewObservable.text(editText)
                .map(event -> event.text);
input.filter(cs -> !TextUtils.isEmpty(cs))     
        .map(cs -> cs.toString().toUpperCase())
        .take(5)                               
        .subscribe(str -> handleString(str));  
Observable<CharSequence> input =                                  
        AndroidObservable.fromBroadcast(ctx, intentFilter)         
                .map(intent -> intent.getStringExtra("magic_str"));
Call call = okHttpClient.newCall(...);                                 
Observable<String> input = executeOkCall(call)                        
        .map(response -> {                                             
            try {                                                      
                return response.body().string();                       
            } catch (IOException e) {                                  
                throw new RuntimeException(e);                         
            }                                                          
        })                                                             
        .flatMap(bodyString -> Observable.from(bodyString.split("\n")))
        .observeOn(AndroidSchedulers.mainThread());                    

flatMap

public Observable<String> fromOkCall1(Call call){       
    return Observable.create(subscriber -> {            
        try {                                           
            Response response = call.execute();         
            subscriber.onNext(response.body().string());
            subscriber.onCompleted();                   
        } catch (IOException e) {                       
            subscriber.onError(e);                      
        }                                               
    });                                                 
}                                                       
public Observable<String> fromOkCall2(Call call){                         
    return Observable.create(subscriber -> {                              
        call.enqueue(new Callback() {                                     
            @Override                                                     
            public void onFailure(Request request, IOException e) {       
                subscriber.onError(e);                                    
            }                                                             
                                                                          
            @Override                                                     
            public void onResponse(Response response) throws IOException {
                try {                                                     
                    subscriber.onNext(response.body().string());          
                    subscriber.onCompleted();                             
                } catch (IOException e) {                                 
                    subscriber.onError(e);                                
                }                                                         
            }                                                             
        });                                                               
    });                                                                   
}                                                                         

Lazyness

Observable<String> observable =                                 
        Observable.create(new Observable.OnSubscribe<String>() {
    @Override                                                   
    public void call(Subscriber<? super String> subscriber) {   
        subscriber.onNext("Hello");                             
        subscriber.onNext("uamobile");                          
        subscriber.onCompleted();                               
    }                                                           
});                                                             

Simple (cold) Observable

observable.subscribe(new Action1<String>() {
    @Override                               
    public void call(String s) {            
        handleString(s);                    
    }                                       
});                                         
Observable<String> lastNcRead =                                               
        mBriefcaseHelper.getBriefcaseObservable()                             
        .flatMap(list -> {                                                    
            Observable<Briefcase> briefcaseObservable = Observable.from(list);
            Observable<String> maxRead = briefcaseObservable                  
                    .ofType(NotificationReadBriefcase.class)                  
                    .map(b -> b.attrs.version)                                
                    .firstOrDefault("0");                                     
            Observable<String> maxClear = briefcaseObservable                 
                    .ofType(NotificationClearBriefcase.class)                 
                    .map(b -> b.attrs.version)                                
                    .firstOrDefault("0");                                     
            return Observable.zip(                                            
                    maxRead,                                                  
                    maxClear,                                                 
                    (a, b) -> a.compareTo(b) > 0 ? a : b                      
            );                                                                
        });                                                                   
                                                                              
Observable<Boolean> hasNotificationsObservable =                              
        mDatastore.getNotifications()                                
           .switchMap(notifications ->                                        
                        lastNcRead.flatMap(lastNC ->                          
                           Observable.from(notifications)                     
                              .takeWhile(n -> n.getId().compareTo(lastNC) > 0)
                              .isEmpty().map(empty -> !empty)                 
                        )                                                     
           ).observeOn(AndroidSchedulers.mainThread());                       
hasNotificationsObservable                                                
        .subscribe(hasNotifications -> view.setEnabled(hasNotifications));

Do nothing

Execute all above

Cold and lazy

Observable<Integer> cold =                        
        Observable.create(subscriber -> {         
            int result = new Random().nextInt(50);
            subscriber.onNext(result);            
            subscriber.onCompleted();             
        });                                       
                                                  
cold.subscribe(n -> print(n)); //13               
cold.subscribe(n -> print(n)); //42 =(        

Hot and shared

 ConnectableObservable<Integer> cold =             
         Observable.<Integer>create(subscriber -> {
             int result = new Random().nextInt(50);
             subscriber.onNext(result);            
             subscriber.onCompleted();             
         }).publish();                             
                                                   
 cold.subscribe(n -> print(n)); //40               
 cold.subscribe(n -> print(n)); //40               
 cold.connect();                                   

publish & connect

interface Response {                                            
    List<String> getData();                                     
    Observable<Response> next();                                
}                                                               
                                                                
public Observable<Response> apiCall(...) {...}                  
                                                                
public Observable<String> loadAll(Observable<Response> source) {
    if (source == null) {                                       
        return Observable.empty();                              
    } else {                                                    
        return source.flatMap(resp -> {                         
            List<String> data = resp.getData();                 
            Observable<Response> next = resp.next();            
            return Observable.concat(                           
                    Observable.from(data),                      
                    loadAll(next)                               
            );                                                  
        });                                                     
    }                                                           
}                                                               
Observable<String> all = loadAll(apiCall(...))
        .filter(str -> str.contains("#uamobile"))
        .take(20)                                
        .timeout(1, TimeUnit.MINUTES);           

Tip: Use infinite squences

Observable.empty()

concat

Tip: be careful with evaluation time

Observable<MyHeavyData> dataObservable =                           
        loadDataFromNetwork()                                      
        .timeout(10, TimeUnit.SECONDS)
        .onErrorResumeNext(Observable.just(loadDataFromStorage()));
Observable<MyHeavyData> dataObservable =                           
        loadDataFromNetwork()                                      
        .timeout(10, TimeUnit.SECONDS)
        .onErrorResumeNext(ex -> Observable.just(loadDataFromStorage()));

onErrorResumeNext

timeout

Cancelation

Proactive

Observable<Integer> items = Observable.create(subscriber -> {
    int i = 0;                                               
    while (true) {                                           
        if(subscriber.isUnsubscribed()) return;              
        subscriber.onNext(i++);                              
    }                                                        
});                                                          
                                                             
items.take(10).subscribe(x -> print(x));                     

Reactive

final LocalBroadcastManager localBroadcastManager = LocalBroadcastManager.getInstance(ctx);
                                                                                           
Observable<Intent> broadcasts = Observable.create(subscriber -> {                          
    final BroadcastReceiver broadcastReceiver = new BroadcastReceiver() {                  
        @Override                                                                          
        public void onReceive(Context context, Intent intent) {                            
            subscriber.onNext(intent);                                                     
        }                                                                                  
    };                                                                                     
                                                                                           
    final Subscription subscription = Subscriptions.create(                  
        () -> localBroadcastManager.unregisterReceiver(broadcastReceiver)
    );                                                                                       
                                                                                           
    subscriber.add(subscription);                                                          
    localBroadcastManager.registerReceiver(broadcastReceiver, intentFilter);               
});                                                                                        

OkHttp example

public Observable<String> fromOkCall3(Call call){                         
    return Observable.create(subscriber -> {                              
        subscriber.add(Subscriptions.create(() -> call.cancel()));        
        call.enqueue(new Callback() {                                     
            @Override                                                     
            public void onFailure(Request request, IOException e) {       
                subscriber.onError(e);                                    
            }                                                             
                                                                          
            @Override                                                     
            public void onResponse(Response response) throws IOException {
                try {                                                     
                    subscriber.onNext(response.body().string());          
                    subscriber.onCompleted();                             
                } catch (IOException e) {                                 
                    subscriber.onError(e);                                
                }                                                         
            }                                                             
        });                                                               
    });                                                                   
}                                                                         

Functional Collections

Right here, in your java

Observables can be used to operate

over simple collections

Collection as Observable

  • Build-in operators
  • No intermediate allocations
  • Functional style

How to use

  1. Create Observable from Iterable
    Observable.from(list)

  2. Transform it using build-in operators
    .filter(...), .map(...), .flatMap(...)

  3. Transform to BlockingObservable and get resut
    .toBlocking.toList().single()
    .toBlocking.single()
    .toBlocking.toIterable()

     

class GameSession {                                                            
    String user;                                                               
    int score;                                                                 
}                                                                              
                                                                               
List<Pair<String, Integer>> leaderboard =                                      
   Observable.from(gameSessions)                                               
      .filter(session -> session.score != 0)                                   
      .groupBy(session -> session.user)                                        
      .flatMap(groupedSessions ->                                              
                  groupedSessions.map(session -> session.score)                
                     .reduce(0, (n1, n2) -> n1 + n2)                           
                     .map(totalScore ->                                        
                             Pair.create(groupedSessions.getKey(), totalScore))
      )                                                                        
      .take(10)                                                                
      .toSortedList((pair1, pair2) -> pair2.second.compareTo(pair1.second))    
      .toBlocking()                                                            
      .single();                                                               

Example: Leaderboard

groupBy

reduce

List<GameSession> gameSessions = null;                                     
List<Pair<String, Integer>> leaderboard =                                  
   Observable.from(gameSessions)                                           
      .filter(session -> session.score != 0)                               
      .groupBy(session -> session.user)                                    
      .flatMap(groupedSessions ->                                          
                  groupedSessions.publish(shared -> {                      
                    Observable<Integer> total =                            
                            shared.map(session -> session.score)           
                                  .reduce(0, (n1, n2) -> n1 + n2);         
                    Observable<Integer> count = shared.count();            
                    return total.zipWith(count, (t, c) -> t/c);            
                  })                                                       
                  .map(totalScore ->                                       
                         Pair.create(groupedSessions.getKey(), totalScore))
      )                                                                    
      .take(10)                                                            
      .toSortedList((pair1, pair2) -> pair2.second.compareTo(pair1.second))
      .toBlocking()                                                        
      .single();                                                           

Example: Leaderboard

(average)

zip

Async Patterns

loadDataFromNetwork()                 
        .timeout(10, TimeUnit.SECONDS)

Timeout

build-in

                                              
        .timeout(10, TimeUnit.SECONDS, loadDataFromStorageObservable());
loadDataFromNetwork()                 
        .timeout(10, TimeUnit.SECONDS)
        .retry()                      

Retry

build-in

        .retry(5)
        .retry((attempt, throwable) ->                                   
                attempt < 5 && throwable instanceof UnknownHostException)
        .retryWhen(throwables ->                                          
                throwables.zipWith(Observable.range(1, 5), (ex, i) -> i)  
                .map(i -> (int) Math.pow(2, i))                           
                .flatMap(time -> Observable.timer(time, TimeUnit.SECONDS))
                .concatWith(throwables.flatMap(t -> Observable.error(t))) 
        );                                                                

retryWhen

ViewObservable.text(editText)                                   
        .map(textChangeEvent -> textChangeEvent.text)           
        .debounce(500, TimeUnit.MILLISECONDS)                   
        .switchMap(text -> loadSuggestions(text))               
        .observeOn(AndroidSchedulers.mainThread())              
        .subscribe(suggestions -> showSuggestions(suggestions));

Search

debounce and autocomplete 

debounce

switchMap

ArrayAdapter<Message> messagesAdapter = ...;               
Observable<Message> messagesStream = getMessagesStream();   
messagesStream                                              
        .observeOn(AndroidSchedulers.mainThread())          
        .subscribe(message -> messagesAdapter.add(message));

Buffering

 

messagesStream                                       
        .buffer(10) //collect lists by 10            
        .observeOn(AndroidSchedulers.mainThread())   
        .subscribe((List<Message> messageList) ->    
                messagesAdapter.addAll(messageList));

Ineficient!

messagesStream                                                
        .buffer(2, TimeUnit.SECONDS) //collect every 2 seconds
        .observeOn(AndroidSchedulers.mainThread())            
        .subscribe((List<Message> messageList) ->             
                messagesAdapter.addAll(messageList));         

buffer

buffer

Buffering

 

messagesStream                                                      
        .publish(sharedMessagesObs -> {                             
            Observable<Message> boundary =                          
                    sharedMessagesObs.debounce(1, TimeUnit.SECONDS);
            return sharedMessagesObs                                
                    .buffer(boundary);                              
        })//wait until there are no items for 2 seconds             
          //then propagate them together                            
        .observeOn(AndroidSchedulers.mainThread())                  
        .subscribe((List<Message> messageList) ->                   
                messagesAdapter.addAll(messageList));               
 Observable<List<TimeLineItem>> timeline = ...;                           
                                                                           
 Observable<Boolean> busyList = Observable.create(subscriber -> {          
     mTimelineListView.setOnTouchListener((v, event) -> {                  
         int action = event.getActionMasked();                             
         subscriber.onNext(action == MotionEvent.ACTION_DOWN               
                        || action == MotionEvent.ACTION_MOVE);             
         v.onTouchEvent(event);                                            
         return true;                                                      
     });                                                                   
 });                                                                       
                                                                           
 Observable<List<TimeLineItem>> delayedTimeline = Observable.combineLatest(
         timeline,                                                         
         busyTimeline.distinctUntilChanged(),                              
         (items, isBusy) -> isBusy ? null : items                          
 ).filter(items -> items != null);                                         

Delayed list update

combineLatest

Questions?

Links

RxJava, Tips and Tricks

By Yaroslav Heriatovych

RxJava, Tips and Tricks

Some tips and trick about using RxJava on Android platform.

  • 8,850