核心思想:
RxJava學習和使用最重要的是掌握它的核心思想,它在 github 主頁的介紹是:
"a library for composing asynchronous and event-based programs using observable sequences for the Java VM"
我理解是:一個運行在 Java 虛擬機上的可序列觀察組合、異步和基于事件的庫芥玉。所以能推導 Rx 定義為:序列觀察組合異步事件姨拥。
我們能得到幾個關鍵詞:
- 序列:對順序敏感(事件發(fā)送)
- 觀察:響應式編程
- 組合:可處理多個事件
- 異步:可調度線程
- 事件:可以理解為起點發(fā)送的事件,也可以理解為每個中間環(huán)節(jié)的操作
根據(jù)使用經(jīng)驗來總結其核心思想:在起點注冊觀察后分發(fā)一個事件,流式操作中間步驟克伊,終點消費事件解除觀察捏浊。
因為它的操作符很多懂衩,遍歷學習操作符性價比很小,我們根據(jù)其核心思想金踪,以輸出的方式來學習 RxJava浊洞。
普通應用:
業(yè)務要求1:有一個圖片url地址,下載圖片并且將圖片顯示在 ImageView胡岔。
思路:事件為下載并且展示圖片到 ImageView法希,事件起點為分發(fā)(放置)一個 url 給下游中間步驟1(傳送帶),下游中間步驟1拿到的為String而步驟二設置圖片顯示需要的數(shù)據(jù)為 Bitmap靶瘸,所以此步驟需要 Map 操作符來將url轉換為 Bitmap并且應該 為異步操作苫亦,然后將 Bitmap分發(fā)給下游中間步驟 2尖淘,步驟 2 將 Bitmap更新到 ImageView,消費掉事件著觉,事件結束村生。
流程圖為:
上代碼:
// 打印logcat日志的標簽
String TAG = DownloadActivity.class.getSimpleName();
// 網(wǎng)絡圖片的鏈接地址
String PATH = "http://pic1.win4000.com/wallpaper/c/53cdd1f7c1f21.jpg";
// 彈出加載框
ProgressDialog progressDialog;
Observable.just(PATH) // 內部會分發(fā) PATH Stirng // TODO 第二步
// TODO 第二步
.map(new Function<String, Bitmap>() {
@Override
public Bitmap apply(String s) throws Exception {
URL url = new URL(PATH);
HttpURLConnection httpURLConnection = (HttpURLConnection) url.openConnection();
httpURLConnection.setConnectTimeout(5000);
int responseCode = httpURLConnection.getResponseCode(); // 才開始 request
if (responseCode == HttpURLConnection.HTTP_OK) {
InputStream inputStream = httpURLConnection.getInputStream();
Bitmap bitmap = BitmapFactory.decodeStream(inputStream);
return bitmap;
}
return null;
}
})
// 日志記錄
.map(new Function<Bitmap, Bitmap>() {
@Override
public Bitmap apply(Bitmap bitmap) throws Exception {
Log.d(TAG, "apply: 是這個時候下載了圖片啊:" + System.currentTimeMillis());
return bitmap;
}
})
.subscribeOn(Schedulers.io()) // 給上面代碼分配異步線程
.observeOn(AndroidSchedulers.mainThread()) // 切換主線程
// 訂閱 起點 和 終點 訂閱起來
.subscribe(
// 終點
new Observer<Bitmap>() {
// 訂閱開始
@Override
public void onSubscribe(Disposable d) {
// 預備 開始 要分發(fā) 創(chuàng)建并展示 loading
// TODO 第一步
progressDialog = new ProgressDialog(DownloadActivity.this);
progressDialog.setTitle("download run");
progressDialog.show();
}
// TODO 第四步
// 消費事件
@Override
public void onNext(Bitmap bitmap) {
image.setImageBitmap(bitmap);
}
// 錯誤事件
@Override
public void onError(Throwable e) {
}
// TODO 第五步
// 完成事件
@Override
public void onComplete() {
if (progressDialog != null)
progressDialog.dismiss();
}
});
實現(xiàn)完上邊需求穿香,產(chǎn)品小姐姐說需要一個加水印的功能袍榆,沒問題宾尚,我們就在上邊代碼基礎上來做改造蝠咆。
思路:在上述步驟1九巡,拿到 Bitmap 后然后我們將步驟2 改為為 Bitmap 添加水印勤篮,然后步驟3 來完成圖片的展示和事件的消費逼龟。
修改后的流程圖為:
上代碼:
// 打印logcat日志的標簽
String TAG = DownloadActivity.class.getSimpleName();
// 網(wǎng)絡圖片的鏈接地址
String PATH = "http://pic1.win4000.com/wallpaper/c/53cdd1f7c1f21.jpg";
// 彈出加載框
ProgressDialog progressDialog;
Observable.just(PATH) // 內部會分發(fā) PATH Stirng // TODO 第二步
// TODO 第二步
.map(new Function<String, Bitmap>() {
@Override
public Bitmap apply(String s) throws Exception {
URL url = new URL(PATH);
HttpURLConnection httpURLConnection = (HttpURLConnection) url.openConnection();
httpURLConnection.setConnectTimeout(5000);
int responseCode = httpURLConnection.getResponseCode(); // 才開始 request
if (responseCode == HttpURLConnection.HTTP_OK) {
InputStream inputStream = httpURLConnection.getInputStream();
Bitmap bitmap = BitmapFactory.decodeStream(inputStream);
return bitmap;
}
return null;
}
})
// 此處為新增水印操作代碼
.map(new Function<Bitmap, Bitmap>() {
@Override
public Bitmap apply(Bitmap bitmap) throws Exception {
Paint paint = new Paint();
paint.setTextSize(88);
paint.setColor(Color.RED);
return drawTextToBitmap(bitmap, "產(chǎn)品要得水印",paint, 88 , 88);
}
})
// 日志記錄
.map(new Function<Bitmap, Bitmap>() {
@Override
public Bitmap apply(Bitmap bitmap) throws Exception {
Log.d(TAG, "apply: 是這個時候下載了圖片啊:" + System.currentTimeMillis());
return bitmap;
}
})
.subscribeOn(Schedulers.io()) // 給上面代碼分配異步線程
.observeOn(AndroidSchedulers.mainThread()) // 切換主線程
// 訂閱 起點 和 終點 訂閱起來
.subscribe(
// 終點
new Observer<Bitmap>() {
// 訂閱開始
@Override
public void onSubscribe(Disposable d) {
// 預備 開始 要分發(fā) 創(chuàng)建并展示 loading
// TODO 第一步
progressDialog = new ProgressDialog(DownloadActivity.this);
progressDialog.setTitle("download run");
progressDialog.show();
}
// TODO 第四步
// 消費事件
@Override
public void onNext(Bitmap bitmap) {
image.setImageBitmap(bitmap);
}
// 錯誤事件
@Override
public void onError(Throwable e) {
}
// TODO 第五步
// 完成事件
@Override
public void onComplete() {
if (progressDialog != null)
progressDialog.dismiss();
}
});
// 圖片上繪制文字 加水印
private final Bitmap drawTextToBitmap(Bitmap bitmap, String text, Paint paint, int paddingLeft, int paddingTop) {
Bitmap.Config bitmapConfig = bitmap.getConfig();
paint.setDither(true); // 獲取跟清晰的圖像采樣
paint.setFilterBitmap(true);// 過濾一些
if (bitmapConfig == null) {
bitmapConfig = Bitmap.Config.ARGB_8888;
}
bitmap = bitmap.copy(bitmapConfig, true);
Canvas canvas = new Canvas(bitmap);
canvas.drawText(text, paddingLeft, paddingTop, paint);
return bitmap;
}
防抖:
防抖的意思就是說規(guī)定時間內接收到多次事件只觸發(fā)一次亿胸。放置惡意轟炸典徘,或者重復觸發(fā)問題蟀苛。
業(yè)務需求:如下圖,兩秒內項目按鈕點擊只完成一次獲取所有項目分類逮诲,并且根據(jù)項目 id 獲取所有項目下的所有列表數(shù)據(jù)帜平。
一般思路:利用 RxBinding 處理防抖動,然后步驟1梅鹦,網(wǎng)絡獲取所有項目數(shù)據(jù)裆甩,數(shù)據(jù)類型為[ProjectBean,ProjectBean,ProjectBean,ProjectBean,ProjectBean...],步驟 2,根據(jù)步驟 1 獲取的ProjectBeanList 遍歷齐唆,分別根據(jù)每個 ProjectBean id 去獲取項目列表數(shù)據(jù)嗤栓。步驟 3,分別獲取每個項目的列表數(shù)據(jù) [ProjectItem,ProjectItem,ProjectItem,ProjectItem,ProjectItem..]去更新 UI消費掉該事件箍邮。
流程圖如下:
上代碼:(此處為偽代碼實現(xiàn))
// 注意:(項目分類)查詢的id茉帅,通過此id再去查詢(項目列表數(shù)據(jù))
// 對那個控件防抖動?
Button bt_anti_shake = findViewById(R.id.bt_anti_shake);
RxView.clicks(bt_anti_shake)
.throttleFirst(2000, TimeUnit.MILLISECONDS) // 2秒鐘之內 響應你一次
.subscribe(new Consumer<Object>() {
@Override
public void accept(Object o) throws Exception {
api.getProject() // 查詢主數(shù)據(jù)
.compose(RxUtils.rxud()) //線程切換封裝
.subscribe(new Consumer<ProjectBean>() {
@Override
public void accept(ProjectBean projectBean) throws Exception {
for (ProjectBean.DataBean dataBean : projectBean.getData()) { // 10
// 查詢item數(shù)據(jù)
api.getProjectItem(1, dataBean.getId())
.compose(RxUtils.rxud())
.subscribe(new Consumer<ProjectItem>() {
@Override
public void accept(ProjectItem projectItem) throws Exception {
Log.d(TAG, "accept: " + projectItem); // 可以UI操作
}
});
}
}
});
}
});
網(wǎng)絡嵌套:
上邊防抖代碼可讀性很差锭弊,why堪澎?因為上邊代碼有兩次嵌套縮進,第一次為實現(xiàn)防抖動廷蓉,回調縮進了一次全封,項目列表數(shù)據(jù)獲取也在主數(shù)據(jù)獲取的回調嵌套中。
解決迷之嵌套問題桃犬,我們需要了解 RxJava 的另一個操作符:
flatmap:可以將一個觀察集合數(shù)據(jù)的觀察者展開為多個觀察一條數(shù)據(jù)的觀察者刹悴。這也是他和 map 的最主要區(qū)別,當集合中只有一條數(shù)據(jù)時攒暇,它的作用將和 map 一樣土匀。回顧上邊普通應用的需求實現(xiàn)都是從頭到尾一個觀察者形用,觀察著一條數(shù)據(jù)向下游流動就轧。
改造版代碼:(同樣為偽代碼)
// 對那個控件防抖動证杭?
Button bt_anti_shake = findViewById(R.id.bt_anti_shake);
RxView.clicks(bt_anti_shake)
.throttleFirst(2000, TimeUnit.MILLISECONDS) // 2秒鐘之內 響應你一次
// 我只給下面 切換 異步
.observeOn(Schedulers.io())
.flatMap(new Function<Object, ObservableSource<ProjectBean>>() {
@Override
public ObservableSource<ProjectBean> apply(Object o) throws Exception {
return api.getProject(); // 主數(shù)據(jù)
}
})
.flatMap(new Function<ProjectBean, ObservableSource<ProjectBean.DataBean>>() {
@Override
public ObservableSource<ProjectBean.DataBean> apply(ProjectBean projectBean) throws Exception {
return Observable.fromIterable(projectBean.getData()); // 注冊多個觀察者
}
})
.flatMap(new Function<ProjectBean.DataBean, ObservableSource<ProjectItem>>() {
@Override
public ObservableSource<ProjectItem> apply(ProjectBean.DataBean dataBean) throws Exception {
return api.getProjectItem(1, dataBean.getId());
}
})
.observeOn(AndroidSchedulers.mainThread()) // 給下面切換 主線程
.subscribe(new Consumer<ProjectItem>() {
@Override
public void accept(ProjectItem projectItem) throws Exception {
// 如果我要更新UI 會報錯2 不會報錯1
Log.d(TAG, "accept: " + projectItem);
}
});
連續(xù)操作:
上邊需求發(fā)現(xiàn)沒?有一個共同規(guī)律妒御,就是都是一個事件開始解愤,然后事件結束。如果實現(xiàn)一個時間開始乎莉,一個時間結束送讲,然后再次開啟一個事件,然后再次結束第二個事件怎么辦惋啃?是和上述代碼并列哼鬓,再次創(chuàng)建一個觀察者?NO边灭!看下邊操作符异希。
doNoNext:可以用于,多個連續(xù)事件绒瘦,執(zhí)行過程中称簿,事件都需要按順序消費掉前邊事件然后繼續(xù)執(zhí)行后邊事件。
業(yè)務需求:用戶注冊完成后更新UI 跳轉登錄頁面并且自動登錄后跳轉首頁椭坚。
實現(xiàn)思路:用戶點擊 reigstBtn獲取注冊結果予跌,拿到注冊結果 UI 線程去跳轉登錄頁面,然后工作線程去根據(jù) reigstResponse 去請求登錄數(shù)據(jù)善茎,拿到登錄信息去跳轉首頁,消費掉這個事件频轿。注意:此時消費掉的事件并非最初事件垂涯。
業(yè)務流程比較簡單,直接代碼實現(xiàn):(偽代碼)
/**
* 一行代碼 實現(xiàn)需求
* 需求:
* 還有彈出加載
* * 1.請求服務器注冊操作
* * 2.注冊完成之后航邢,更新注冊UI
* * 3.馬上去登錄服務器操作
* * 4.登錄完成之后耕赘,更新登錄的UI
*/
MyRetrofit.createRetrofit().create(IReqeustNetwor.class)
.registerAction(new RegisterRequest()) // todo 1.請求服務器注冊操作 // todo 2
.subscribeOn(Schedulers.io()) // 給上面 異步
.observeOn(AndroidSchedulers.mainThread()) // 給下面分配主線程
.doOnNext(new Consumer<RegisterResponse>() { // todo 3
@Override
public void accept(RegisterResponse registerResponse) throws Exception {
// todo 2.注冊完成之后,更新注冊UI
}
})
// todo 3.馬上去登錄服務器操作
.observeOn(Schedulers.io()) // 給下面分配了異步線程
.flatMap(new Function<RegisterResponse, ObservableSource<LoginResponse>>() { // todo 4
@Override
public ObservableSource<LoginResponse> apply(RegisterResponse registerResponse) throws Exception {
Observable<LoginResponse> loginResponseObservable = MyRetrofit.createRetrofit().create(IReqeustNetwor.class)
.loginAction(new LoginReqeust());
return loginResponseObservable;
}
})
.observeOn(AndroidSchedulers.mainThread()) // 給下面 執(zhí)行主線程
.subscribe(new Observer<LoginResponse>() {
// 一定是主線程膳殷,為什么操骡,因為 subscribe 馬上調用onSubscribe
@Override
public void onSubscribe(Disposable d) {
// TODO 1
progressDialog = new ProgressDialog(RequestActivity.this);
progressDialog.show();
// UI 操作
disposable = d;
}
@Override
public void onNext(LoginResponse loginResponse) { // todo 5
// TODO 4.登錄完成之后,更新登錄的UI
}
@Override
public void onError(Throwable e) {
}
// todo 6
@Override
public void onComplete() {
// over
if (progressDialog != null) {
progressDialog.dismiss();
}
}
});
????
并行操作:
??????????????????????????再有一個例子赚窃,右下邊需求:
Map<String, String> map = CZController.paramHelp.getBasicParam();
map.put("catalogId", catalogId);
map.put("courseId", courseId);
Observable<CZResult<String>> obLiveData =
ApiWrapper.getService().getLiveRoomV2(map)
.subscribeOn(Schedulers.io());
Observable<CZResult<String>> obUserInfo =
ApiWrapper.getService().getLiveEmployeeInfo(map)
.subscribeOn(Schedulers.io());
Observable.merge(obLiveData, obUserInfo)
.observeOn(AndroidSchedulers.mainThread())
.subscribe(new CZSubscriberD<CZResult<String>>() {
@Override
public void onCZNext(String json) {
}
});
}
遞歸:
還是一個需求來看册招,掃描某個路徑下的所有文件,找出格式為 .doc 的文件并輸出路徑勒极。
直接看代碼:
/**
* 獲取某文件夾下所有 doc 格式的文件路徑
*/
private void getFiles(File file) {
if(!file.isDirectory()) return;
Observable.just(file)
.observeOn(Schedulers.io())
.map(new Function<File, List<File>>() {
@Override
public List<File> apply(File file) throws Exception {
return Arrays.asList(file.listFiles());
}
})
.flatMap(new Function<List<File>, ObservableSource<File>>() {
@Override
public ObservableSource<File> apply(List<File> files) throws Exception {
return Observable.fromIterable(files);
}
})
.observeOn(AndroidSchedulers.mainThread())
.subscribe(new Consumer<File>() {
@Override
public void accept(File file) throws Exception {
//TODO 判斷 file 類型是掰,如果是文件夾則遞歸,如果是需要的格式則繼續(xù)辱匿,否則結束
if (file.isDirectory()) {
getFiles(file);
} else if (file.getName().endsWith(".doc")) {
Log.d("Tag1", "path:" + file.getPath());
} else {
Log.d("Tag1", "其他類型文件");
}
}
});
}
哈哈哈键痛,咋樣炫彩,如果你真拿上邊代碼去應用,就會發(fā)現(xiàn)一個問題絮短,卡的一筆江兢。
為什么呢,flatmap 就會創(chuàng)建 n 個 Observable丁频,然后外層遞歸更可怕 n 的 n 次方個Observable,所以遞歸中最好都不要有對象的創(chuàng)建杉允,哈哈哈。
看一下下邊代碼:
/**
* Created by droidwolf on 2017/11/14.
* https://my.oschina.net/droidwolf
* 轉載請注明
*/
public class FileTreeWalker implements Iterable<File> {
private ArrayDeque<File> mDirectories =null;
private ArrayDeque<File> mFiles = null;
public FileTreeWalker walk(File path) {
if (mDirectories != null && !mDirectories.isEmpty()) {
mDirectories.clear();
}
if (mFiles != null && !mFiles.isEmpty()) {
mFiles.clear();
}
walkDir(path);
return this;
}
private void walkDir(File path) {
if (path == null || !path.exists() || !path.isDirectory()) {
return;
}
final File[] files = path.listFiles();
if (files == null || files.length == 0) {
return;
}
if(mDirectories==null) mDirectories = new ArrayDeque<File>(256);
if(mFiles==null) mFiles = new ArrayDeque<File>(512);
for (File f : files) {
if (f.isDirectory()) {
mDirectories.push(f);
} else {
mFiles.addLast(f);
}
}
}
@Override
public Iterator<File> iterator() {
return mIterator;
}
private final Iterator<File> mIterator=new Iterator<File>() {
@Override
public boolean hasNext() {
return (mFiles!=null &&!mFiles.isEmpty()) || (mDirectories!=null&& !mDirectories.isEmpty());
}
@Override
public File next() {
if (mFiles != null && !mFiles.isEmpty()) {
final File f = mFiles.pollFirst();
return f;
} else if (mDirectories != null && !mDirectories.isEmpty()) {
final File dir = mDirectories.pop();
walkDir(dir);
return dir;
}
return null;
}
};
}
// rxjava調用FileTreeWalker
Observable.fromIterable(new FileTreeWalker().walk(path))
.subscribeOn(Schedulers.io())
.filter(file -> {
//限府。夺颤。。
return false;
})
.observeOn(AndroidSchedulers.mainThread())
.subscribe(fileNext -> {
//胁勺。世澜。。
});
//當然也可以在java8中使用FileTreeWalker
StreamSupport.stream(Spliterators.spliteratorUnknownSize(new FileTreeWalker().walk(new File("C:\\")).iterator(),Spliterator.NONNULL),true )
.forEach(file->{
if(file.isDirectory()) {
System.out.println(file.getName());
}else {
System.out.println("\t"+file.getName());
}
});
小結
帶著本文第一節(jié)對“核心思想”的理解署穗,來思考一下這幾個業(yè)務實現(xiàn)寥裂。發(fā)現(xiàn)每個業(yè)務都重點關注事件的開始和結束也就是事件的傳入數(shù)據(jù)類型和最終消費事件需要的數(shù)據(jù)類型,中間步驟的所有轉換都有操作符去支持我們案疲。
上邊圖片下載問題可以舉個相似栗子:小明肚子餓了封恰,想吃東西利用核心思想來實現(xiàn)一下:
此事件完整實現(xiàn)需要先確定事件開始(大腦餓了信號類型),和事件結束(食物類型)中間具體需要步驟都可以有操作符來實現(xiàn)褐啡。
網(wǎng)絡嵌套需求舉個相似栗子:小明餓了诺舔,想吃 10 種東西:
連續(xù)操作需求舉一個相似栗子:小明餓了,吃完飯然后去上學备畦。
關注開始與結束