大戰 RxJava2 和 Java8 Stream [ Android RxJava2 ] (這到底是什麼) 第四部分
又是新的一天,如果學點新東西,這一天一定會很酷炫。
小夥伴們一切順利啊,這是我們的 RxJava2 Android 系列的第四部分 [ 第一部分, 第二部分, 第三部分 ]。 好訊息是我們已經做好準備,可以開始使用 Rx 了。在使用 RxJava2 Android Observable 之前,我會先用 Java8 的 Stream 來做響應式程式設計。我認為我們應該瞭解 Java8,而且通過使用 Java8 的 Stream API 讓我感覺學習 RxJava2 Android 的過程更簡單。
動機:
動機跟我在 第一部分 和大家分享過的一樣。在我開始學習 RxJava2 Android 的時候,我並不知道自己會在什麼地方,以何種方式使用到它。
現在我們已經學會了一些預備知識,但當時我什麼都不懂。因此我開始學習如何根據資料或物件建立 Observable 。然後知道了當 Observable 的資料發生變化時,應該呼叫哪些介面(或者可以叫做“回撥”)。這在理論上很好,但是當我付諸實踐的時候,卻 GG 了。我發現很多理論上應該成立的模式在我去用的時候完全不起作用。對我來說最大的問題,是不能用響應或者函式式響應的思維思考問題。我熟悉指令式程式設計和麵向物件程式設計,由於先入為主,所以對我來說理解響應式會有些難。我一直在問這些問題:我該在哪裡實現?我應該怎麼實現?如果你能堅持看完這篇文章,我可以 100% 保證你會知道怎樣把命令式程式碼轉換成 Rx 程式碼,雖然寫出來的 Rx 程式碼不是最好的,但至少你知道該從哪裡入手了。
回顧:
我想回顧之前三篇文章中我們提到過的所有概念 [ 第一部分、第二部分、 第三部分 ]。因為現在我們要用到這些概念了。在 第一部分 我們學習了觀察者模式; 在 第二部分 學習了拉模式和推模式、命令式和響應式;在 第三部分 我們學習了函式式介面(Functional Interfaces)、 介面預設方法(Default Methods)、高階函式(Higher Order Functions)、函式的副作用(Side Effects in Functions)、純函式(Pure Functions)、Lambda 表示式和函數語言程式設計。我在下面寫了一些定義(很無聊的東西)。如果你清楚這些定義,可以跳到下一部分。
函式式介面是隻有一個抽象方法的介面。
在 Java8 我們可以在介面中定義方法,這種方法叫做“預設方法”。
至少有一個引數是函式的函式和返回型別為函式的函式稱為高階函式。
純函式的返回值僅僅由引數決定,不會產生可見的副作用(比如修改一些影響程式狀態的值。——譯註)。
Lambda 表示式在計算機程式設計中又叫做匿名函式,是一種在宣告和執行的時候不會跟識別符號繫結的函式或者子程式。
簡介:
今天我們將向 RxJava 的學習宣戰。我確定在最後我們會取得勝利。
作戰策略:
Java8 Stream(這使得我們快速開始,我們將從 Android 開發者的角度來看)
Java8 Stream 向 Rx Observable 轉變
RxJava2 Android 示例
技巧,怎樣把命令式程式碼轉為 RxJava2 Android 程式碼
是時候根據我們的策略發動進攻了,兄弟們上。
1. Java8 Stream:
現在我用 IntelliJ 這個 IDE 來寫 Java8 的 Stream。你可能會想為什麼我去使用在 Android 不支援的 Java8 的 Stream。對於這樣想的同志,我來解釋一下。主要有兩個原因。首先,我知道幾年後 Java8 將成為 Android 開發的一等公民。所以你應該瞭解關於 Stream 的 API,並且在面試中你可能被問到。而且,Java8 的 Stream 和 Rx Observable 在概念上很像。所以,為什麼不一次性把這兩個東西一起學了呢?其次,我感覺很多像我一樣能力低下、懶惰並且不容易掌握概念的同志也可以在幾分鐘內瞭解這個概念。再次強調,我向你們 100% 地保證。通過學習 Java8 的 Stream 可以讓你很快地學會 Rx。好,我們開始了。
Stream:
支援在元素形成的流上進行函式式操作(比如在集合上進行的 map-reduce 變換)的類(docs.oracle)。
第一個問題:在英語中 Stream 是什麼意思?
答案:一條很窄的小河,或者源源不斷流動的液體、空氣、氣體。在程式設計的時候把資料轉化成“流”的形式,比如我有一個字串,但是我想把它變成“流”來使用的話我需要幹些什麼,我需要建立一個機制,使這個字串滿足“源源不斷流動的液體、空氣、氣體 {或者資料}”的定義。問題是,我們為什麼想要自己的資料變成“流”呢,下面是個簡單的例子。
就像下面這幅圖中畫的那樣,我有一杯混合著大大小小石子的藍色的水。
現在按照我們關於“流”的定義,我用下圖中的方法將水轉化成“流”。
為了讓水變成水流,我把水從一個杯子倒進另一個杯子 裡。現在我想去掉水中的大石子,所以我造了一個可以幫我濾掉大石子的過濾器。“大石子過濾器”如下圖所示。
現在,將這個過濾器作用在水流上,這會得到不包含大石子的水。如下圖所示。
哈哈哈。 接下來,我想從水中清除掉所有石子。已經有一個過濾大石子的過濾器了,我們需要造一個新的來過濾小石子。“小石子過濾器”如下圖所示。
像下圖這樣,將兩個過濾器同時作用於水流上。
哇哦~ 我已經感覺到你們領悟了我說的在程式設計中使用流所帶來的好處是什麼了。接下來,我想把水的顏色從藍色變成黑色。為了達到這個目的,我需要造一個像下圖這樣的“水顏色轉換器(mapper)”。
像下圖這樣使用這個轉換器。
把水轉換成水流後,我們做了很多事情。我先用一個過濾器去掉了大石子,然後用另一個過濾器去掉了小石子, 最後用一個轉換器(map)把水的顏色從藍色變成黑色。
當我將資料轉換成流時,我將在程式設計中得到同樣的好處。現在,我將把這個例子轉換成程式碼。我要顯示的程式碼是真正的程式碼。可能示例程式碼不能工作,但我將要使用的操作符和 API 是真實的,我們將在後面的例項中使用。所以,同志們不要把關注點放在編譯上。通過這個例子,我有一種感覺,我們將很容易地把握這些概念。在這個例子中,重要的一點是,我使用 Java8 的 Stream API 而不是 Rx API。我不想讓事情變困難,但稍後我也會使用 Rx。
影象中的水 & 程式碼中的水:
public static void main(String [] args){
Water water = new Water("water",10, "big stone", 1 , "small stone", 3);
// 含有一個大石子和三個小石子的十升水
for (String s : water) {
System.out.println(s);
}
}複製程式碼
輸出:
water
water
big stone
water
water
small stone
water
small stone
small stone
water
water
water
water
water
影象中的水流 & 程式碼中的水流:
public static void main(String[] args) {
Water water = new Water("water", 10, "big stone", 1, "small stone", 3);
// 10 litre water with 1 big and 3 small stones.
water.stream();
}
//輸出和上面那個一樣複製程式碼
影象中的“大石子過濾器” & 程式碼中的“大石子過濾器”:
同志們這裡需要注意下!
在 Java8 Stream 中有個叫做 Predicate(謂詞,可以判斷真假,詳情見離散數學中的相關定義——譯註)的函式式介面。所以,如果我想進行過濾的話,可以用這個函式式介面實現流的過濾功能。現在,我給大家展示在我們的程式碼中如何建立“大石子過濾器”。
private static Predicate<String> BigStoneFilter = new Predicate<String>() {
@Override
public boolean test(String s) {
return !s.equals("big stone");
}
};複製程式碼
正如我們在 第三部分 所學到的,任何函式式介面都可以轉換成 Lambda 表示式。把上面的程式碼轉換成 Lambda 表示式:
private static Predicate<String> BigStoneFilter = s -> !s.equals("big stone");複製程式碼
影象和程式碼中的作用在水流上的“大石子過濾器”:
public static void main(String[] args) {
Water water = new Water("water", 10, "big stone", 1, "small stone", 3);
water.stream().filter(BigStoneFilter)
.forEach(s-> System.out.println(s));
}
private static Predicate<String> BigStoneFilter = s -> !s.equals("big stone");複製程式碼
這裡我使用了 forEach 方法,暫時把這當作流上的 for 迴圈。用在這裡僅僅是為了輸出。除去沒有這個方法,我們也已經實現了我們在影象中表示的內容。是時候看看輸出了:
water
water
water
water
small stone
water
small stone
small stone
water
water
water
water
water
沒有大石子了,這意味著我們成功過濾了水。
影象中的“小石子過濾器” & 程式碼中的“小石子過濾器”:
private static Predicate<String> SmallStoneFilter = s -> !s.equals("small stone");複製程式碼
在影象和程式碼中使用“小石子過濾器”:
public static void main(String[] args) {
Water water = new Water("water", 10, "big stone", 1, "small stone", 3);
water.stream()
.filter(BigStoneFilter)
.filter(SmallStoneFilter)
.forEach(s-> System.out.println(s));
}
private static Predicate<String> BigStoneFilter = s -> !s.equals("big stone");
private static Predicate<String> SmallStoneFilter = s -> !s.equals("small stone");複製程式碼
我不打算解釋 SmallStoneFilter,它的實現和 BigStoneFilter 是一樣一樣的。這裡我只展示輸出。
water
water
water
water
water
water
water
water
water
water
影象中的“水顏色轉換器” 和 程式碼中的“水顏色轉換器”
同志們這裡需要注意!
在 Java8 Stream 中有個叫做 Function 的函式式介面。所以,當我想進行轉換的時候,需要把這個函式式介面送到流的轉換(map)函式裡面。現在,我給大家展示在我們的程式碼中如何建立“水顏色轉換器”。
private static Function<String, String > convertWaterColour = new Function<String, String>() {
@Override
public String apply(String s) {
return s+" black";
}
};複製程式碼
這是一個函式式介面,所以我可以把它轉換為 Lambda :
private static Function<String, String > convertWaterColour = s -> s+" black";複製程式碼
簡單來說,泛型中的第一個 String 代表我從水中得到什麼,第二個 String 表示我會返回什麼。 為了更好地掰扯清楚,我寫了個把 Integer 轉化成 String 的轉換器。
private static Function<Integer, String > convertIntegerIntoString = i -> i+" ";複製程式碼
回到我們原來的例子。
為水流新增顏色轉換器的影象和程式碼:
public static void main(String[] args) {
Water water = new Water("water", 10, "big stone", 1, "small stone", 3);
water.stream()
.filter(BigStoneFilter)
.filter(SmallStoneFilter)
.map(convertWaterColour)
.forEach(s -> System.out.println(s));
}
private static Predicate<String> BigStoneFilter = s -> !s.equals("big stone");
private static Predicate<String> SmallStoneFilter = s -> !s.equals("small stone");
private static Function<String, String> convertWaterColour = s -> s + " black";複製程式碼
輸出:
water black
water black
water black
water black
water black
water black
water black
water black
water black
water black
完活!現在我們再次回顧一些內容。
filter(過濾器): Stream 有一個只接受 Predicate 這個函式式介面的方法。我們可以在 Predicate 裡寫作用在資料上的邏輯程式碼。
map(對映):Stream 有一個只接受 Function 這個函式式介面的方法。我們可以在 Function 裡寫按照我們的要求轉換資料的邏輯程式碼。
在進入下個環節之前,我想解釋一個曾經困惑我很久的東西。當我們在任意資料上使用 stream() 的時候,背後是怎樣工作的。所以我要舉一個例子。我有一個整數列表。我想在控制檯上顯示它們。
public static void main(String [] args){
List<Integer> list = new ArrayList<>();
list.add(1);
list.add(2);
list.add(3);
list.add(4);
}複製程式碼
使用指令式程式設計來列印資料:
public static void main(String [] args){
List<Integer> list = new ArrayList<>();
list.add(1);
list.add(2);
list.add(3);
list.add(4);
for (Integer integer : list) {
System.out.println(integer);
}
}複製程式碼
使用 Stream 或 Rx 的方式來列印資料:
public static void main(String [] args){
List<Integer> list = new ArrayList<>();
list.add(1);
list.add(2);
list.add(3);
list.add(4);
list.stream().forEach(integer -> System.out.println(integer));
}複製程式碼
對於以上兩段程式碼,它們的不同點在哪呢?
簡單來說,在第一段程式碼中我自己管理 for 迴圈:
for (Integer integer : list) {
System.out.println(integer);
}複製程式碼
但是在第二段程式碼中,流(或者稍後後要展示的 Rx 中的 Observable)進行迴圈:
list.stream().forEach(integer -> System.out.println(integer));複製程式碼
我認為很多事情都說清楚了,是時候用 Rx 來寫個真實的例子了。在這個例子中,我會同時使用流式編碼(stream code)和響應式編碼(Rx code),這樣大家可以更容易地掌握這倆的概念。
2. Java8 Stream to Rx Observable:
有一個存有 “Hello World” 的列表。 在圖片中,把它視作字串。在程式碼中把它看作列表,這樣比較好解釋。
Java8 的 Stream 程式碼:
public static void main(String [] args){
List<String> list = new ArrayList<>();
list.add("H");
list.add("e");
list.add("l");
list.add("l");
list.add("o");
list.add(" ");
list.add("W");
list.add("o");
list.add("r");
list.add("l");
list.add("d");
list.stream(); // Java8
}複製程式碼
Android 中的程式碼:
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
List<String> list = new ArrayList<>();
list.add("H");
list.add("e");
list.add("l");
list.add("l");
list.add("o");
list.add(" ");
list.add("W");
list.add("o");
list.add("r");
list.add("l");
list.add("d");
Observable.fromIterable(list);
}
}複製程式碼
在這裡展示了 Java8 程式碼和 Android 程式碼。從現在開始,我只給出程式碼中的響應式(Reactive)部分而不給出完整的一個類。完整程式碼分享在文章的最後了。上面的程式碼將變成這樣:
Again above example:
list.stream(); // Java8
Observable.fromIterable(list); // Android複製程式碼
這兩者會有相同的結果,這樣來輸出整個列表:
list.stream()
.forEach(s-> System.out.print(s)); // Java8
Observable.fromIterable(list)
.forEach(s-> Log.i("Android",s)); // Android
Java8 的輸出:
Hello World
Android 的輸出:
03-12 15:55:33.561 6094-6094/async.waleed.rx I/Android: H
03-12 15:55:33.561 6094-6094/async.waleed.rx I/Android: e
03-12 15:55:33.561 6094-6094/async.waleed.rx I/Android: l
03-12 15:55:33.561 6094-6094/async.waleed.rx I/Android: l
03-12 15:55:33.561 6094-6094/async.waleed.rx I/Android: o
03-12 15:55:33.561 6094-6094/async.waleed.rx I/Android:
03-12 15:55:33.561 6094-6094/async.waleed.rx I/Android: W
03-12 15:55:33.561 6094-6094/async.waleed.rx I/Android: o
03-12 15:55:33.561 6094-6094/async.waleed.rx I/Android: r
03-12 15:55:33.561 6094-6094/async.waleed.rx I/Android: l
03-12 15:55:33.561 6094-6094/async.waleed.rx I/Android: d複製程式碼
是時候來比較下這倆了。
list.stream().forEach(s-> System.out.print(s)); // Java8
Observable.fromIterable(list).forEach(s-> Log.i("Android",s)); // Android複製程式碼
在 Java8 中我想要一個東西變成流的形式,我會用 Stream 的 API,但是在 Android 裡,我先把那個東西轉換成 Observable 然後獲取到資料流。
接下來,我們將用 ’l‘ 作為過濾器來處理 Hello World,就像下面這樣:
list.stream()
.filter(s -> !s.equals("l"))
.forEach(s-> System.out.print(s)); //Java8
Observable.fromIterable(list)
.filter(s->!s.equals("l"))
.forEach(s-> Log.i("Android",s)); // Android
輸出 in Java8:
Heo Word
輸出 In Android:
03-12 16:05:58.558 10236-10236/async.waleed.rx I/Android: H
03-12 16:05:58.558 10236-10236/async.waleed.rx I/Android: e
03-12 16:05:58.558 10236-10236/async.waleed.rx I/Android: o
03-12 16:05:58.558 10236-10236/async.waleed.rx I/Android:
03-12 16:05:58.558 10236-10236/async.waleed.rx I/Android: W
03-12 16:05:58.558 10236-10236/async.waleed.rx I/Android: o
03-12 16:05:58.558 10236-10236/async.waleed.rx I/Android: r
03-12 16:05:58.558 10236-10236/async.waleed.rx I/Android: d複製程式碼
好。是時候對 Java8 的 Stream API 說再見了。
3. RxJava2 的 Android 示例:
有一個整數陣列,我想讓陣列中的每個成員變成自身的平方。
如圖所示:
Android 程式碼:
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
Integer[] data = {1,2,3,4};
Observable.fromArray(data)
.map(value->value*value)
.forEach(value-> Log.i("Android",value+""));
}複製程式碼
輸出:
03-12 16:13:32.432 14918-14918/async.waleed.rx I/Android: 1
03-12 16:13:32.432 14918-14918/async.waleed.rx I/Android: 4
03-12 16:13:32.432 14918-14918/async.waleed.rx I/Android: 9
03-12 16:13:32.432 14918-14918/async.waleed.rx I/Android: 16
.map(value->value*value)複製程式碼
這波很穩,我們之前已經用到過相同的概念了。把一個函式式介面傳進 map,這個函式簡單地將輸入的數平方後返回。
.forEach(value-> Log.i("Android",value+""));複製程式碼
稍有常識的人都知道,我們只能在 log 中列印字串。在上面的程式碼中,我在整數值的後面新增 +""
來把他們轉換成字串。
哇哦!我們可以在這個例子中再用一次 map。你們都知道我需要把整數轉換成字串以便列印到 Logcat,但是我現在打算為 map 再寫一個函式式介面來完成轉換。這意味著我們不需要在資料後面新增 +""
了,如下所示:
Observable.fromArray(data)
.map(value->value*value)
.map(value-> Integer.toString(value))
.forEach(string-> Log.i("Android",string));複製程式碼
4. 如何把命令式程式碼轉化成 RxJava2 Android 程式碼:
這裡我打算使用一段現實存在於某 APP 的程式碼,我將使用 Rx Observable 把它轉化成響應式(Reactive)程式碼。這樣你很容易就知道怎樣開始在自己的專案中使用 Rx 了。重要的東西可能不是很容易理解,但你應該開始動手,這樣才會感覺良好。所以,像我在示例程式碼中提到的那樣去使用它們,我會在下一篇文章中詳細解釋。嘗試多去練練手。
示例:
我在一個專案中使用了 OnBoarding 介面,根據 UI 設計需要在每個 OnBoarding 介面上顯示點點,如下圖所示:
如果你觀察得很仔細的話,可以看到我需要將選定的介面對應的點設定成黑色。
指令式程式設計的程式碼:
private void setDots(int position) {
for (int i = 0; i < mCircleImageViews.length; i++) {
if (i == position)
mCircleImageViews[i].setImageResource(R.drawable.white_circle_solid_on_boarding);
else
mCircleImageViews[i].setImageResource(R.drawable.white_circle_outline_on_boarding);
}
}複製程式碼
響應式程式碼(Rx)的程式碼:
public void setDots(int position) {
Observable.fromIterable(circleImageViews)
.subscribe(imageView ->
imageView.setImageResource(R.drawable.white_circle_outline_on_boarding));
circleImageViews.get(position)
.setImageResource(R.drawable.white_circle_solid_on_boarding);
}複製程式碼
在 setDots 函式中,我簡單地遍歷每個 ImageView 並且把它們設定成白色的空心圈,之後將選定的 ImageView 重新設定為實心圈。
或者,
public void setDots(int position) {
Observable.range(0, circleImageViews.size())
.filter(i->i!=position)
.subscribe(i->circleImageViews.get(i).setImageResource(R.drawable.white_circle_outline_on_boarding)));
circleImageViews.get(position)
.setImageResource(R.drawable.white_circle_solid_on_boarding);
}複製程式碼
在這個 setDots 函式中,我把除選定的 ImageView 之外的所有 ImageView 設定為白色空心圈。
之後,將選中的 ImageView 設定為實心圈。
4. 幾個關於把命令式程式碼轉換成響應式程式碼的技巧:
為了讓大家可以在現有的程式碼上輕鬆開始使用 Rx,我寫了幾個小技巧。
- 如果程式碼中有迴圈的話,用 Observable 替換
for (int i = 0; i < 10; i++) {
}
==>
Observable.range(0,10);複製程式碼
- 如果程式碼中有 if 語句的話,用 Rx 中的 filter 替換
for (int i = 0; i < 10; i++) {
if(i%2==0){
Log.i("Android", "Even");
}
}
==>
Observable.range(0,10)
.filter(i->i%2==0)
.subscribe(value->Log.i("Android","Event :"+value));複製程式碼
- 如果需要把一些資料轉換為另一種格式,可以用 map 實現
public class User {
String username;
boolean status;
public User(String username, boolean status) {
this.username = username;
this.status = status;
}
}
List<User> users = new ArrayList<>();
users.add(new User("A",false));
users.add(new User("B",true));
users.add(new User("C",true));
users.add(new User("D",false));
users.add(new User("E",false));
for (User user : users) {
if(user.status){
user.username = user.username+ "Online";
}else {
user.username = user.username+ "Offline";
}
}複製程式碼
在 Rx 中,有很多方法實現上述程式碼。
使用兩個流:
Observable.fromIterable(users)
.filter(user -> user.status)
.map(user -> user.username + " Online")
.subscribe(user -> Log.i("Android", user.toString()));
Observable.fromIterable(users)
.filter(user -> !user.status)
.map(user -> user.username + " Offline")
.subscribe(user -> Log.i("Android", user.toString()));複製程式碼
在 map 中使用 if else :
Observable.fromIterable(users)
.map(user -> {
if (user.status) {
user.username = user.username + " Online";
} else {
user.username = user.username + " Offline";
}
return user;
})
.subscribe(user -> Log.i("Android", user.toString()));複製程式碼
- 如果程式碼中有巢狀的迴圈:
for (int i = 0; i < 10; i++) {
for (int j = 0; j < 10; j++) {
System.out.print("j ");
}
System.out.println("i");
}
==>
Observable.range(0, 10)
.doAfterNext(i-> System.out.println("i"))
.flatMap(integer -> Observable.range(0, 10))
.doOnNext(i -> System.out.print("j "))
.subscribe();複製程式碼
這裡用到了 flatmap 這個新的操作符。先僅僅嘗試像示例程式碼中那樣使用,我會在下篇文章中解釋。
總結:
同志們幹得好!今天我們學 Rx Android 學得很開心。我們從圖畫開始,然後使用了 Java8 的流(Stream)。之後將 Java8 的流轉換到 RxJava 2 Android 的 Observable。再之後,我們看到了實際專案中的示例並且展示了在現有的專案中如何開始使用 Rx。最後,我展示了一些轉換到 Rx 的技巧:把迴圈用 forEach 替換,把 if 換成 filter,用 map 進行資料轉化,用 flatmap 代替巢狀的迴圈。下篇文章: Dialogue between Rx Observable and a Developer (Me) [ Android RxJava2 ] ( What the hell is this ) Part5.
希望你們開心,同志們再見!
程式碼:
- Water Stream Example(示例:水流)
- HelloWorldStream using Java8 Stream API(示例:Java8 Stream 初體驗)
- HelloWorldStream using Rx Java2 Android(示例:RxJava2 Android 初體驗) | project level gradle | app level gradle
- ArrayOfIntegers using Rx Java2 Android(示例:用 RxJava2 Android 操作整數陣列) | project level gradle | app level gradle
對於其他所有示例,您可以使用文章中的片段。
掘金翻譯計劃 是一個翻譯優質網際網路技術文章的社群,文章來源為 掘金 上的英文分享文章。內容覆蓋 Android、iOS、React、前端、後端、產品、設計 等領域,想要檢視更多優質譯文請持續關注 掘金翻譯計劃。