RxJava 系列(一):什么是响应式编程(以RxJava为例)

1,137 阅读8分钟

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 编程方式的魅力;

传统模式获取图片

  • 实现效果:

    image-20220621192036883

  • 传统编写思路:

    • 弹出加载框

    • 开启异步线程:此时有多种途径

      • 封装方法....
      • 全部写在一起
      • 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);
           }
          
        • 整体事件流向:

          image-20220622223332983

    • 第三步:在拦截器内添加网络请求

       @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();
         }
        
  • 这种编程方式成为卡片式编程

    • 好处:后期若需要添加功能,仅需在起点与重点之间添加对应的拦截器,在其中进行处理即可

    • 图片示例:一开始的

      • 事件流动顺序

      image-20220622223332983

      • 运行结果:

        image-20220622231216269

    • 图片示例:此时需要添加个需求,将下载下来的图片添加水印后再展示

      • 事件流动顺序

        image-20220622225848759

      • 添加代码:图片上绘制文字 加水印

         // 图片上绘制文字 加水印
         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);
             }
         })
        
      • 运行结果:从服务器获取图片并添加水印

        image-20220622231334525

    • 还可以添加:及时记录日志等功能

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;
     }
 }