Reactive Programming on Android with RxJava

Vincent Nien 2015/12/31

RX's power

滑鼠雙擊(多擊)設計

Imperative Programming

int x, y, z;
x = 1;
y = 2;

z = x + y; // 1 + 2
++y;
printf("%d", z);  // z = 3

現在有三個箱子x, y, z

x裡面放著一顆球,y裡面放著兩顆球

z的箱子放著x跟y這兩個箱子

問: z的箱子裡面現在有幾顆球?

What is Reactive

現在你在y箱子中多放一顆球

問: z的箱子裡面現在有幾顆球?

實際範例

Observable Pattern?

A style of programming based on two key ideas: continuous time-varying behaviors, and event-based reactivity

Functional Reactive Programming

http://blog.csdn.net/smzhangyang/article/details/47006663

http://wiki.jikexueyuan.com/project/android-weekly/issue-145/introduction-to-RP.html

響應式編程  -  一種關注在非同步資料流的編程方式

RX的特色

  • 函數式 - 避免複雜狀態,使用簡單的input/output來處理資料流
  • 簡潔 - Operators可以將複雜的邏輯變成簡單的代碼
  • 錯誤處理 - 強大的錯誤處理機制
  • 非同步處理 - 簡易的非同步執行序處理

Why Rx

"讓非同步的設計更加直覺" -- Sam Lee

"隨著程式邏輯變得複雜,依然能夠保持簡潔"-- 扔物線

"實現鏈式調用,邏輯清晰簡單" -- Coryphaei

Callback

FRP

http://www.infoq.com/cn/articles/functional-reactive-programming

RxJava

  • Observable
  • Transformation

Observable

Observable

        Observer<T> observer = new Observer<T>() {
            @Override
            public final void onCompleted() {
            }

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

            @Override
            public final void onNext(T args) {
            }

        };
        observable.flip().subscribe(observer);

Creating Observable

Observable<String> ob = Observable.just("Something");

Creating Observable

List<String>  aList = ...;
Observable<String> ob = Observable.from(aList);

Functional Language

object FunSets {
  type Set = Int => Boolean

  def contains(s: Set, elem: Int): Boolean = s(elem)

  def singletonSet(elem: Int): Set = (x: Int) => elem == x

  def union(s: Set, t: Set): Set = (x: Int) => contains(s, x) || contains(t, x)

  def intersect(s: Set, t: Set): Set = (x: Int) => contains(s, x) && contains(t, x)

  def diff(s: Set, t: Set): Set = (x: Int) => contains(s, x) && !contains(t, x)

  def filter(s: Set, p: Int => Boolean): Set = (x: Int) => contains(s, x) && p(x)

  val bound = 1000

  def forall(s: Set, p: Int => Boolean): Boolean = {
    def iter(a: Int): Boolean = {
      if (a > bound) true
      else if (contains(s, a)) filter(s, p)(a) && iter(a + 1)
      else iter(a + 1)
    }
    iter(-bound)
  }

Data Structure?

Observable
          .range(0, 5)
          .map(x -> toBinaryString(x*x))
          .subscribe(s -> println(s),
                  err ->  err.printStackTrace(),
                  () ->   println("done"));
0
1
100
1001
10000
done
Observable
  .range(1, 3)
  .flatMap(x -> Observable.just(x).repeat(x))
  .subscribe(System.out::println);
1
2
2
3
3
3
Observable
      .range(0, 10)
      .filter(x -> (x % 2) == 0)
      .subscribe(System.out::println);
0
2 
4 
6
8

來舉些例子吧

不偷資料的WhosCall

簡易流程

  1. 有人打電話進來
  2. junkcall.org 查詢號碼資料
  3. 顯示到浮動視窗上

一般寫法

public static List<JunkCall> queryJunkCall(String number) {
    List<JunkCall> list = new ArrayList<>();
    try {
        String content = getContent(URL.concat(number));
        List<String> data = parse(content);
        for(String desc : data) {
            list.add(JunkCall.create(number, desc));
        }
    } catch(Exception e) {
    }
    return list;
}

使用RxJava

public static Observable<JunkCall> queryJunkCall(String number) {
        return Observable.just(number)
                .map(new Func1<String, String>() {
                     @Override
                     public String call(String s) {
                        return URL_JUNKCALL.concat(s);
                     }
                })
                .flatMap(new Func1<String, Observable<String>>() {
                    @Override
                    public Observable<String> call(String s) {
                        return getContent(s);
                    }
                })
                .flatMap(new Func1<String, Observable<String>>() {
                    @Override
                    public Observable<String> call(String s) {
                        return parse(s);
                    }
                })
                .map(new Func1<String, JunkCall>() {
                    @Override
                    public JunkCall call(String desc) {
                        return JunkCall.create(number, desc)
                    }
                });
}

WTF...

With Lambda Expression / Method Reference

    public static Observable<JunkCall> queryJunkCall(String number) {
        return Observable.just(number)
                            .map(s -> URL_JUNKCALL.concat(s))
                            .flatMap(JunkCallParser::getContent)
                            .flatMap(JunkCallParser::parse)
                            .map(desc -> JunkCall.create(number, desc));
    }

Comparison

public static Observable<JunkCall> queryJunkCall
                (String number) {
    return Observable
            .just(number)
            .map(s -> URL_JUNKCALL.concat(s))
            .flatMap(JunkCallParser::getContent)
            .flatMap(JunkCallParser::parse)
            .map(desc -> JunkCall.create(number, desc));
}
public static List<JunkCall> queryJunkCall
           (String number) {
    List<JunkCall> list = new ArrayList<>();
    try {
        String content = getContent(URL.concat(number));
        List<String> data = parse(content);
        for(String desc : data) {
            list.add(JunkCall.create(number, desc));
        }
    } catch(Exception e) {
    }
    return list;
}

擴充功能

Everything is stream

Add Retry

public static List<JunkCall> queryJunkCallWithRetry(String number) {
    List<JunkCall> list = new ArrayList<>();
    int retry = 0;
    boolean success;
    do {
        try {
            String content = getContent(URL.concat(number));
            List<String> data = parse(content);
            for(String desc : data) {
                list.add(JunkCall.create(number, desc));
            }
            success = true;
        } catch(Exception e) {
            success = false;
        }
    } while(!success && retry++ < 1);
    return list;
}

Add Filter

public static List<JunkCall> queryJunkCallWithRetry(String number) {
    List<JunkCall> list = new ArrayList<>();
    int retry = 0;
    boolean success;
    do {
        try {
            String content = getContent(URL.concat(number));
            List<String> data = parse(content);
            for(String desc : data) {
                if(!TextUtils.isEmpty(desc)) {
                    list.add(JunkCall.create(number, desc));
                }
            }
            success = true;
        } catch(Exception e) {
            success = false;
        }
    } while(!success && retry++ < 1);
    return list;
}

http://www.slideshare.net/jingtw/in-in-der

http://wiki.jikexueyuan.com/project/android-weekly/issue-145/introduction-to-RP.html

Add Retry/Filter with RxJava

public static Observable<JunkCall> queryPhoneNumber(String number) {
        return Observable
                  .just(number)
                  .map(s -> URL_JUNKCALL.concat(s))
                  .flatMap(JunkCallParser::getContent)
                  .flatMap(JunkCallParser::parse)
                  .filter(s -> !TextUtils.isEmpty(s))
                  .map(desc -> JunkCall.create(number, desc))
                  .retry(1);
}

http://www.slideshare.net/jingtw/in-in-der

Comparison

public static Observable<JunkCall> 
            queryJunkCall(String number) {
    return Observable
      .just(number)
      .map(s -> URL_JUNKCALL.concat(s))
      .flatMap(JunkCallParser::getContent)
      .flatMap(JunkCallParser::parse)
      .filter(s -> !TextUtils.isEmpty(s))
      .map(desc -> JunkCall.create(number, desc))
      .retry(1);
}
public static List<JunkCall> 
        queryJunkCallWithRetry(String number) {
    List<JunkCall> list = new ArrayList<>();
    int retry = 0;
    boolean success;
    do {
        try {
            String content = getContent(URL.concat(number));
            List<String> data = parse(content);
            for(String desc : data) {
                if(!TextUtils.isEmpty(desc)) {
                    list.add(JunkCall.create(number, desc));
                }
            }
            success = true;
        } catch(Exception e) {
            success = false;
        }
    } while(!success && retry++ < 1);
    return list;
}

http://www.infoq.com/cn/articles/functional-reactive-programming

public static Observable<JunkCall> 
            queryJunkCall(String number) {
    return Observable
      .just(number)
      .map(s -> URL_JUNKCALL.concat(s))
      .flatMap(JunkCallParser::getContent)
      .flatMap(JunkCallParser::parse)
      .filter(s -> !TextUtils.isEmpty(s))
      .map(desc -> JunkCall.create(number, desc))
      .retry(1);
}

將查詢結果顯示到UI

實現非同步串接stream

一般寫法

    new Thread(new Runnable() {
        public void run() {
            List<JunkCall> data = JunkCallParser.queryJunkCall(number);
            final List<String> descriptions = new ArrayList<>();
            // distinct
            Set<String> set = new HashSet<>();
            for(JunkCall call : data) {
                set.add(call.description());
            }
            for(String s : set) {
                descriptions.add(s);
            }
            runOnUiThread(new Runnable() {
                public void run() {
                    mWindow.setResult(JunkCallService.this, 
                        descrptions, number);
                }
            });
        }
    }).start();

With RxJava

JunkCallParser
   .queryJunkCall(number)
   .distinct(JunkCall::description)
   .map(JunkCall::description)
   .toList()
   .subscribeOn(Schedulers.io())
   .observeOn(AndroidSchedulers.mainThread())
   .subscribe(junkCalls -> {
       mWindow.setResult(JunkCallService.this, 
                            junkCalls, number);
     }
   });

Comparison

JunkCallParser
    .queryJunkCall(number)
    .distinct(JunkCall::description)
    .map(JunkCall::description)
    .toList()
    .subscribeOn(Schedulers.io())
    .observeOn(AndroidSchedulers.mainThread())
    .subscribe(junkCalls -> 
            mWindow.setResult(JunkCallService.this, 
                       junkCalls, number);
    );
new Thread(new Runnable() {
    public void run() {
        List<JunkCall> data =
            JunkCallParser.queryJunkCall(number);
        final List<String> descriptions = 
            new ArrayList<>();
        // distinct
        Set<String> set = new HashSet<>();
        for(JunkCall call : data) {
            set.add(call.description());
        }
        for(String s : set) {
            descriptions.add(s);
        }
        runOnUiThread(new Runnable() {
            public void run() {
                mWindow.setResult(JunkCallService.this, 
                    descrptions, number);
            }
        });
    }
}).start();

Result

Say Hello to Callback Hell

/* API */
void getFromServer(String key, Action1<String> callback);
void getFromDB(String key, Action1<String> callback);

/* Code */
btnClick.setOnClickListener(new View.OnClickListener() {
    public void onClick(View view) {
        getFromDB("myid", new Action1<String>() {
            public void call(String s) {
                getFromServer(s, new Action1<String>() {
                    public void call(final String s) {
                     runOnUiThread(new Runnable() {
                            public void run() {
                                Toast.makeText(context, s, LENGTH_LONG).show();
                            }
                        });
     /* ... a lot of }) ... */

http://misgod.github.io/Slide-FRP-Android/

Say Goodbye to Callback Hell

/* API */
Observable<String> getFromServer(String key);
Observable<String> getFromDB(String key);

/* Code */
ViewObservable
  .clicks(btnClick)
  .map(x -> "myid")
  .observeOn(Schedulers.io())
  .flatMap(this::getFromDB)
  .flatMap(this::getFromServer)
  .observeOn(AndroidSchedulers.mainThread())
  .subscribe(x -> Toast.makeText(context, x, LENGTH_LONG).show());  

Use rx.Observable

http://misgod.github.io/Slide-FRP-Android/

All In One search

Observable<SearchResult> g = googleSearch.search(keyword).retry(3);
Observable<SearchResult> b = bingSearch.search(keyword).retry(3);
Observable<SearchResult> y = yahooSearch.search(keyword).retry(3);

Observable
          .merge(g, b, y)
          .distinct(site -> site.url)
          .observeOn(AndroidSchedulers.mainThread())
          .subscribe(
                     site -> appendDataForUI(),
                     err -> errorhandle(err));

http://misgod.github.io/Slide-FRP-Android/

Requirements

  1. A search engine includes google/yahoo/bing search results.
  2. Should search different engines in parallel
  3. Retry 3 times when search fail
  4. remove redundant url

A simple EventBus

PublishSubject<Object> subject = PublishSubject.create(); //Global Singleton

http://misgod.github.io/Slide-FRP-Android/

//...In Class A...
subject.filter(x ->  x instanceof DataUpdateAction)
       .subscribe( x -> ... doSomething ...);

//...In Class B...
subject.filter(x ->  x instanceof DeleteAction)
       .subscribe( x -> ... doSomething ...);

//...In Class C...
subject.filter(x ->  x instanceof RefreshAction)
       .subscribe( x -> ... doSomething ...);
subject.onNext(aDataUpdateAction);
subject.onNext(aDataUpdateAction);
subject.onNext(aRefreshAction);

Conclusion

  1. 全世界都在往Functional Language發展 
  2. Everything is a stream
  3. Try it in your next project

Q & A

Reactive Programming on Android with RxJava

By Vincent SH

Reactive Programming on Android with RxJava

  • 1,396