前言
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次"之類的邏輯。