用程式碼介紹 RxJava

前言

RxJava 也強迫自己用兩個月了,除了自己的專案外,工作的專案也拿來改寫原本很難看懂的邏輯。這篇建議就當作一篇引導文或是心得文看看,有興趣繼續研究的朋友,網路上已經有很多中/英文資源,不怕沒地方學只怕你不學。

安裝

要導入 RxJava 很簡單,gradle 下一行即可,沒用 gradle 也只要抓一個 rxjava.jar 檔案,再引入專案就可以了。
compile 'io.reactivex:rxjava:x.y.z'

不用綁定 JVM 版本,不限定是 Android 專案還是純 Java 專案,只要一個 jar 檔案就能使用 RxJava。

什麼是 RxJava

直接舉幾個例子,來說明什麼是 RxJava,又為什麼我要用 RxJava,看了程式碼應該比用文字描述來的有感覺。這些例子也是我在專案中反覆用到的程式碼來改寫的。

Android 中需要更新 GUI 的耗時工作

取代 AsyncTask 在 Android 上呼叫 耗時工作,呼叫前更新 GUI,呼叫完再更新 GUI。

對於耗時工作回傳的結果,在 RxJava 中還能做進一步處理,就像工廠的流水線,從頭到尾能看到做哪些處理。想想如果要用第一個耗時工作的結果,去呼叫第二的耗時工作,在 RxJava 就是單純再多一個站點(下面程式碼中的 map()),如果沒有 RxJava 這段程式碼要怎麼寫?寫出來的程式碼會不會很難看?
Observable.just("input")
    .doOnSubscribe(new Action0()
    {
        @Override
        public void call()
        {
            // 在觸發耗時工作前更新 GUI
            // 比如顯示進度條
        }
    })
    .subscribeOn(Schedulers.newThread()) // 設定耗時工作在新的 Thread 上執行
    .map(new Func1<String, String>()
    {
        @Override
        public String call(String input) 
        {
            // 執行耗時工作
            // 比如是一個 Web API 呼叫
            return new WebApi(input);
       }
    })
    .map(new Func1<String, User>()
    {
        @Override
        public User call(String input) 
        {
            // 處理耗時工作回傳的結果
            // 比如 Web API 回傳的 Json 字串轉成 POJO
            return new Gson().fromJson(input, User.class);
       }
    })
    .observeOn(AndroidSchedulers.mainThread()) // 設定非同步工作完成後回到 Android GUI Thread 上更新 GUI
    .subscribe(new Observer<User>() 
    {
        @Override
        public void onNext(User user) 
        {
            // 更新 GUI
            // 比如在 TextView 上顯示 User 的名稱
        }

        @Override
        public void onCompleted()
        {
           // 如果沒有發生例外
           // 比如關掉進度條
        }

        @Override
        public void onError(Throwable error)
        {
            // 如果發生例外
            // 比如關掉進度條後跳出警告視窗
        }
    }); 

移除會造成 Callback Hell 的程式風格

延續上個範例提到的,如果要用第一個耗時工作的結果(用帳號密碼呼叫 Web API 得到 token),去呼叫第二的耗時工作(用 token 呼叫得到使用者的訊息清單),最後印出訊息清單。

在 Java 中考慮到呼叫 Web API 是一個耗時工作,所以大部分設計的呼叫參數常會多帶一個 Handler(Observer Pattern),被呼叫端做完耗時工再後,再利用 Handler 讓呼叫端知道該做什麼,程式碼大致會長成這樣:
webApi.getToken("user", "password", new DefaultHandler()
{
    // 第一個 Web API 呼叫成功
    @Override
    public void onSuccess(String response)
    {
        webApi.getMessageList(response, new DefaultHandler()
        {   
            // 第二個 Web API 呼叫成功
            @Override
            public void onSuccess(List<String> response)
            {
                // 呼叫成功
                for (String message : response)
                {
                    // 印出每個訊息
                }
            }

            // 第二個 Web API 呼叫失敗
            @Override
            public void onFailure(Exception exception)
            {
                // 印出失敗訊息
            }
        });
    }

    // 第一個 Web API 呼叫失敗
    @Override
    public void onFailure(Exception exception)
    {
        // 印出失敗訊息
    }
});

有沒有看到程式碼一直往內凹,這個就是 Callback Hell,雖然程式碼還是可以動,但就是不夠簡單不夠整齊。如果今天採用 RxJava 來設計,RxJava 本來就是 Observer Pattern,前一個方法呼叫完成,才會進入下一個方法,程式碼大致會長成這樣:
Observable.just(webApi.getToken("user", "password"))
    .flatMap(new Func1<String, Observable<String>>()
    {
        // 第一個 Web API 呼叫成功
        @Override
        public Observable<String> call(String token)
        {
            try
            {
                // 第二個 Web API 呼叫成功
                return Observable.from(webApi.getMessageList(token));
            }
            catch (Exception e)
            {
                // 第二個 Web API 呼叫失敗
                // 將錯誤丟出來
                throw Exceptions.propagate(e);
            }
        }
    })
    .subscribe(new Observer<String>() 
    {
        @Override
        public void onNext(String message) 
        {
            // 印出每個訊息
        }

        @Override
        public void onCompleted()
        {
            // 如果沒有發生例外
        }

        @Override
        public void onError(Throwable error)
        {
            // 第一個 Web API 呼叫失敗
            // 第二個 Web API 呼叫失敗
        }
    });

當要串的 Web API 越多的時候,帶 Handler 產生的巢狀架構,會讓程式碼越來越複雜不容易看懂,而用 RxJava 寫法的程式碼只會變長。

執行固定次數的動作

有時想要執行某個動作10次,一般都是要寫個 for 迴圈,現在不用羨慕 Ruby/Python 有很簡單的寫法,現在我們也有了。
Observable.range(0, 10).subscribe(new Action1<Integer>() // 0, 1, 2, 3...9
{
    @Override
    public void call(Integer item)
    {
        // 執行動作
        System.out.println(string);
    }
});

幹,好像也沒有比 for 迴圈少打幾個字,這個就必須要你有學好 lamda 了。
Observable.range(0, 10).subscribe(item -> System.out.println(item)); // 0, 1, 2, 3...9

資料集合處理

寫後台寫邏輯常常會需要處理資料集合,今天如果我們有個字串集合,要做以下處理(依序):
  • 只取出有帶 "boy" 的字串
  • 取出字串中的數字
  • 將數字轉成英文(1變成 "one")
  • 只取最前面兩個符合條件的英文
廢話不多說,直接上程式碼最有感覺。
public class HelloRxJava
{
    public static void main(String[] args)
    {
        List<String> list = new ArrayList<String>(Arrays.asList("boy_4", "boy_1", "girl_1", "boy_2", "boy_3", "girl_2"));


        // 傳統寫法:
        int take = 2;
        int i = 1;
        for (String string : list)
        {
            if (isBoy(string))
            {
                if (take < i)
                {
                    return; // Jump!
                }

                System.out.println(numberToEnglishNumber(getNumber(string)));
                i++;
            }
        }


        // RxJava 沒有 lamda 的寫法:
        Observable.from(list)
            .filter(new Func1<String, Boolean>()
            {
                @Override
                public Boolean call(String string) 
                {
                    return isBoy(string);
                }
            })
            .map(new Func1<String, String>()
            {
                @Override
                public String call(String string) 
                {
                    return getNumber(string);
                }
            })
            .map(new Func1<String, String>()
            {
                @Override
                public String call(String string) 
                {
                    return numberToEnglishNumber(string);
                }
            })
            .take(2)
            .subscribe(new Action1<String>()
            {
                @Override
                public void call(String string) 
                {
                    System.out.println(string);
                }
            });


        // RxJava 有 lamda 的寫法:
        Observable.from(list)
            .filter(string -> isBoy(string) == true)
            .map(string -> getNumber(string))
            .map(string -> numberToEnglishNumber(string))
            .take(2)
            .subscribe(string -> System.out.println(string));
    }




    /**
     * 將阿拉伯數字轉為英文字
     */
    private static String numberToEnglishNumber(String string)
    {
        if (string.equals("1")) return "one";

        if (string.equals("2")) return "two";

        if (string.equals("3")) return "three";

        return "oops!";
    }

    /**
     * 取得字串裡的數字
     */
    private static String getNumber(String string)
    {
        return string.substring(string.indexOf("_") + 1);
    }

    /**
     * 字串是否包含 boy
     */
    private static boolean isBoy(String string)
    {
        return string.startsWith("boy");
    }
}

為求範例簡潔,一些邏輯都抽取成 private 方法。可以發現傳統寫法雖然不長,但有巢狀 if 的關係,需要花一點時間才能了解程式碼的意圖,而用 RxJava 的寫法,基本上根本就是把剛剛條列的動作依序執行,而用 lamda 的方式寫的程式碼更是很容易就能看出程式碼的意圖。

小結

本文說明了:
  • 無論是單純的 Java 環境,還是在 Android 上,只要一個 jar 就能開始用 RxJava。
  • RxJava 雖然讓程式碼變長了,但讓程式碼的意圖變得比較好理解。
  • 如果你會用 lamda,你的程式碼就不會那麼長。
  • RxJava 提供的多種運算子,讓我們很容易就能做出"取出最前面兩個元素"、"Retry 10次"之類的邏輯。
我有把我學習過程中寫的範例碼都放上 github,有興趣的人可以拉下來跑,基本上都是一些釐清觀念的 Code。另外推薦這篇 RxJava 文檔中文版,除了介紹 RxJava 的概念外,也針對各種運算子有很詳細的說明。