RxJava思想
-
文章概述:
- 本文围绕Rx编程思想(响应式编程)进行深入细致探讨;以获取服务器图片为例,通过传统方式与Rx方式对比进一步体现Rx 编程方式的魅力;借助卡片式编程思想,对Rx编程方式进行第一次优化;借助 Java泛型对Rx编程进一步优化;
Rx编程出现背景:改变思维来提升效率
-
通过事件流动,推进业务执行
-
从起点到终点,逻辑严密
- 下一层依赖上一层:体现在函数参数
-
链式调用只是里面的一环
-
样例:每一层逻辑上关联
- 起点(分发事件:点击登录)----------登录API-------请求服务器--------获取响应码----------> 终点(更新UI登录成功 消费事件)
-
RxJava 配合 Retrofit
-
业务逻辑:
- Retrofit通过OKHHTTP请求服务器拿到响应码,交给RxJava由RxJava处理数据
-
防抖:
- 一秒钟点击了20次,只响应一次
-
网络嵌套:拿到主数据再拿到item数据
-
doNext运用:异步与主线之间频繁切换
- 异步线程A拿到数据,切换至UI线程更新,再次切换到异步线程B,再拿到UI线程
对比说明Rx 编程优势:统一业务代码逻辑
- 主要内容:以获取服务器图片为例,通过传统方式与Rx方式对比进一步体现Rx 编程方式的魅力;
传统模式获取图片
-
实现效果:
-
传统编写思路:
-
弹出加载框
-
开启异步线程:此时有多种途径
- 封装方法....
- 全部写在一起
- new Thread
- 使用 线程池
-
将从服务器获取的图片转成Bitmap
-
从异步线程切换至UI线程更新UI
-
代码实现:
public void downloadImageAction(View view) { progressDialog = new ProgressDialog(this); progressDialog.setTitle("下载图片中..."); progressDialog.show(); // 异步线程处理耗时任务 new Thread(new Runnable() { @Override public void run() { try { 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 bitmap = BitmapFactory.decodeStream(inputStream); // 使用Handler 进行切换 Message message = handler.obtainMessage(); message.obj = bitmap; handler.sendMessage(message); } } catch (Exception e) { e.printStackTrace(); } } }).start(); } // 使用Handler处理问题 private final Handler handler = new Handler(new Handler.Callback() { @Override public boolean handleMessage(@NonNull Message msg) { Bitmap bitmap = (Bitmap) msg.obj; image.setImageBitmap(bitmap); if (progressDialog != null) progressDialog.dismiss(); return false; } });
-
-
传统方式弊端:
-
在具体实现(切换线程)时,因为思维不统一,导致实现方式不同
-
RxJava思路:采用观察者设计模式,实现响应式(Rx)编程
-
以事件流动推进业务执行
-
角色:
-
起点:被观察者(为其分配异步线程--->请求服务器)
// 起点 Observable.just(PATH) // 内部会分发 PATH Stirng // TODO 第二步
-
终点:观察者(为其分配UI线程--->更新UI)
//终点 .subscribe( new Observer<Bitmap>() { //订阅 @Override public void onSubscribe(Disposable d) { } //拿到事件:因为上一层是一个String类型的Path事件 @Override public void onNext(@NonNull Bitmap bitmap) { image.setImageBitmap(bitmap); } // 错误事件 @Override public void onError(Throwable e) { } // 完成事件 @Override public void onComplete() { } });
-
-
编写思路:框架在实际使用中是U型逻辑(终点--->起点--->终点--->……)
-
第一步:处理终点中拿到事件后的业务逻辑
//拿到事件:因为上一层是一个String类型的Path事件 @Override public void onNext(@NonNull String s) { image.setImageBitmap(bitmap); }
-
细节:onNext的参数问题
-
Rx 整体是以事件流动推进业务逻辑,如果上一层是String类型的事件(Path)那么它的下一层应该也是String类型的事件(参数为String类型)
-
但Rx 中根据业务进行事件的拦截
- A层(String事件),B层(Bitmap事件),逻辑为A层--->B层
- 那么就需要在A层到B层之间添加一个拦截器,进行事件转换
-
-
-
第二步:在起点与终点之间添加拦截器
-
为什么要添加拦截器:业务需求是拿到一个Bitmap而起点提供的是String类型的事件
-
拦截器为map(K,V):K为上层事件,V为下层事件
//上层事件为String类型,由系统自动推断;但此时拦截器并不知道下一层是什么事件,因此为Object .map(new Function<String, Object>() { })
-
终点要求Bitmap事件
//根据业务将map 中的value改为 Bitmap类型 .map(new Function<String, Object>() { })
-
终点完成事件(onNext报错,联动变化):注意由Rx思想决定,那么终点处的完成事件参数因为Bitmap
//由Rx思想决定,那么终点处的完成事件参数因为Bitmap @Override public void onNext(@NonNull Bitmap bitmap) { image.setImageBitmap(bitmap); }
-
整体事件流向:
-
-
-
第三步:在拦截器内添加网络请求
@Override public Bitmap apply(@NonNull String s) throws Exception { //处理网络请求:将String类型的Path事件处理为Bitmap实例 URL url = new URL(PATH); HttpURLConnection httpURLConnection = (HttpURLConnection) url.openConnection(); int responseCode = httpURLConnection.getResponseCode(); if(responseCode == httpURLConnection.HTTP_OK){ InputStream inputStream = httpURLConnection.getInputStream(); Bitmap bitmap = BitmapFactory.decodeStream(inputStream); return bitmap; } return null; }
- 此时不要使用Handler,因为拦截器已经将String类型事件转为Bitmap类型了,将Bitmap流向终点进行显示
-
第四步:分配线程
-
起点到此时拦截器结束,应当分配异步线程(因为需要请求服务器)
//给上边代码分配异步线程,用于请求服务器 .subscribeOn(Schedulers.io())
-
拦截器结束位置到终点处,应当分配UI主线程(因为需要更新UI)
//给下边的代码分配主线程,用于更新UI .observeOn(AndroidSchedulers.mainThread())
-
分配的主线程跟下面这个是一样的
// Thread.currentThread().getName(); == Android的主线程,这个跟RxJava切的android主线程是一样的
-
-
-
到此基础功能已经实现,为了使得用户友好,需要添加下列步骤
-
Rx 代码优化(一):卡片式编程
-
什么是卡片式编程:
- 因为Rx 响应式编程是依靠事件流动推进业务执行,那么我们可以在起点与终点之间添加卡片(拦截器)实现具体的业务功能
-
代码扩展:点击按钮后立即加载对话框,拿到图片并更新,随后关闭对话框
-
整体流程:
-
预处理:点击按钮后,立即加载对话框,开始准备事件分发
//在终点订阅开始处加载对话框(预处理操作) // 订阅开始:一订阅就要显示对话框 @Override public void onSubscribe(Disposable d) { // 第一步:事件分发前预准备 progressDialog = new ProgressDialog(Test.this); progressDialog.setTitle("开始下载"); progressDialog.show(); }
-
第一步:回到起点,开始分发事件
Observable.just(PATH)
-
第二步:拦截器工作将String事件转为Bitmap事件(附带网络请求,从服务器拿到数据)
-
第三步:抵达终点拿到事件处,更新UI
//拿到事件:因为上一层是一个String类型的Path事件 @Override public void onNext(@NonNull Bitmap bitmap) { image.setImageBitmap(bitmap); }
-
第四步:抵达终点完成事件完成处,此时事件整体结束(Rx 编程结束尾巴)
// 完成事件 @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; }
-
添加代码:在前面一个拦截器后添加
.map(new Function<Bitmap, Bitmap>() { @Override public Bitmap apply(@NonNull Bitmap bitmap) throws Exception { //开始添加水印 Paint paint = new Paint(); paint.setTextSize(88); paint.setColor(Color.GREEN); return drawTextToBitmap(bitmap,"水印",paint,88,88); } })
-
运行结果:从服务器获取图片并添加水印
-
-
还可以添加:及时记录日志等功能
-
Rx 代码优化(二):封装代码部分功能提升程序结构
- 封装线程分配
//为上游(起点到拦截器结束)分配异步线程,为下游(拦截器结束位置到终点结束)分配android主线程
private final static <UD> ObservableTransformer<UD,UD> opMixed(){
return new ObservableTransformer<UD, UD>() {
@NonNull
@Override
//分配线程
public ObservableSource<UD> apply(@NonNull Observable<UD> upstream) {
return upstream.subscribeOn(Schedulers.io()).
observeOn(AndroidSchedulers.mainThread())
//继续链式调用
.map(new Function<UD, UD>() {
@Override
public UD apply(@NonNull UD ud) throws Exception {
Log.d(TAG,"日志记录")
return ud;
}
})
//还可以加卡片(拦截器)
;
}
};
}
- 仅需在终点前调用封装好的库就行了
……
//是需要在终点前调用封装好的东西就行了
.compose(opMixed())
//终点
.subscribe(
Rx 编程完整代码:
package com.xiangxue.rxjavademo.downloadimg;
import android.app.ProgressDialog;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.os.Bundle;
import android.os.Handler;
import android.os.Message;
import android.util.Log;
import android.view.View;
import android.widget.ImageView;
import androidx.annotation.NonNull;
import androidx.appcompat.app.AppCompatActivity;
import com.xiangxue.rxjavademo.R;
import java.io.InputStream;
import java.net.HttpURLConnection;
import java.net.URL;
import io.reactivex.Observable;
import io.reactivex.ObservableSource;
import io.reactivex.ObservableTransformer;
import io.reactivex.Observer;
import io.reactivex.android.schedulers.AndroidSchedulers;
import io.reactivex.disposables.Disposable;
import io.reactivex.functions.Function;
import io.reactivex.schedulers.Schedulers;
public class Test extends AppCompatActivity {
// 网络图片的链接地址,String类型的Path事件
private final static String PATH = "http://pic1.win4000.com/wallpaper/c/53cdd1f7c1f21.jpg";
// 弹出加载框
private ProgressDialog progressDialog;
// ImageView控件,用来显示结果图像
private ImageView image;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_download);
image = findViewById(R.id.image);
// Thread.currentThread().getName(); == Android的主线程,这个跟RxJava切的android主线程是一样的
}
// 通过订阅将 起点 和 终点 关联起来
public void rxJavaDownloadImageAction(View view) {
// 起点
Observable.just(PATH) // 内部会分发 PATH Stirng // TODO 第二步
//流程中的卡片
.map(new Function<String, Bitmap>() {
@Override
public Bitmap apply(@NonNull String s) throws Exception {
//处理网络请求:将String类型的Path事件处理为Bitmap实例
URL url = new URL(PATH);
HttpURLConnection httpURLConnection = (HttpURLConnection) url.openConnection();
int responseCode = httpURLConnection.getResponseCode();
if(responseCode == httpURLConnection.HTTP_OK){
InputStream inputStream = httpURLConnection.getInputStream();
Bitmap bitmap = BitmapFactory.decodeStream(inputStream);
return bitmap;
}
return null;
}
})
//给上边代码分配异步线程,用于请求服务器
.subscribeOn(Schedulers.io())
//给下边的代码分配主线程,用于更新UI
.observeOn(AndroidSchedulers.mainThread())
//终点
.subscribe(
new Observer<Bitmap>() {
// 订阅开始:一订阅就要显示对话框
@Override
public void onSubscribe(Disposable d) {
// 第一步:事件分发前预准备
progressDialog = new ProgressDialog(Test.this);
progressDialog.setTitle("开始下载");
progressDialog.show();
}
//拿到事件:因为上一层是一个String类型的Path事件
@Override
public void onNext(@NonNull Bitmap bitmap) {
image.setImageBitmap(bitmap);
}
// 错误事件
@Override
public void onError(Throwable e) {
}
// 完成事件
@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;
}
}