2022-02-25

手把手教你搭建【rxjava+retrofit+okhttp】网络框架

  废话不多说,直接上代码。

  第一步:添加依赖
    implementation 'io.reactivex.rxjava2:rxandroid:2.1.1'  //线程调度
    implementation 'com.squareup.retrofit2:adapter-rxjava2:2.9.0' //配合Rxjava使用
    implementation 'com.squareup.retrofit2:retrofit:2.9.0' //retrofit2.0
    implementation 'com.squareup.retrofit2:converter-gson:2.9.0' //ConverterFactory的Gson:
    implementation 'com.google.code.gson:gson:2.9.0'   //gson解析
    implementation 'com.squareup.okhttp3:okhttp:4.9.3' //核心库
    implementation 'com.squareup.okhttp3:logging-interceptor:4.9.3' //请求日志
    implementation 'com.squareup.retrofit2:converter-scalars:2.9.0' //增加返回值为字符串的支持
  第二步:AndroidManifest.xml中添加网络权限
    <uses-permission android:name="android.permission.INTERNET"/>
    <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>

  此外,还需解决network security policy的问题,该问题比较简单,请参考https://www.jianshu.com/writer#/notebooks/49491345/notes/98737436/preview

  第三步:代码实现

  3.1 新建base目录,同时在该目录下创建几个基础文件:
  3.1.1 【BaseListModel】-----返回结果为数组的实体类
  注意:字段名需要根据项目自行修改。

public class BaseListModel<T> implements Serializable {

    private int code;
    private List<T> data;
    private String message;

    public int getCode() {
        return code;
    }

    public void setCode(int code) {
        this.code = code;
    }

    public List<T> getData() {
        return data;
    }

    public void setData(List<T> data) {
        this.data = data;
    }

    public String getMessage() {
        return message;
    }

    public void setMessage(String message) {
        this.message = message;
    }
}

  3.1.2 【BaseModel】----返回结果为对象的实体类
  注意:字段名需要根据项目自行修改。

public class BaseModel<T> implements Serializable {
    public final static int CODE_SUCCESS = 0;

    private int code;
    private T data;
    private String desc;

    public int getCode() {
        return code;
    }

    public void setCode(int code) {
        this.code = code;
    }

    public T getData() {
        return data;
    }

    public void setData(T data) {
        this.data = data;
    }

    public String getDesc() {
        return desc;
    }

    public void setDesc(String desc) {
        this.desc = desc;
    }

}

  3.1.3 【BaseObserver】----请求状态封装

public abstract class BaseObserver<T> extends DisposableObserver<T> {
    protected BaseView view;
    /**
     * 解析数据失败
     */
    public static final int PARSE_ERROR = 1001;
    /**
     * 网络问题
     */
    public static final int BAD_NETWORK = 1002;
    /**
     * 连接错误
     */
    public static final int CONNECT_ERROR = 1003;
    /**
     * 连接超时
     */
    public static final int CONNECT_TIMEOUT = 1004;

    public BaseObserver(BaseView view){
        this.view = view;
    }

    @Override
    public void onStart(){
        if(view != null){
            view.hideLoading();
            view.showLoading("");
        }
    }

    @Override
    public void onNext(@NotNull T o) {
        try {
            int code;
            String message = "";
            if (o instanceof BaseModel) {
                BaseModel<?> model = (BaseModel<?>) o;
                code = model.getCode();
                message = model.getDesc();
            } else if (o instanceof BaseListModel) {
                BaseListModel<?> model = (BaseListModel<?>) o;
                code = model.getCode();
                message = model.getMessage();
            } else {
                code = BaseModel.CODE_SUCCESS;
            }
            if (code == BaseModel.CODE_SUCCESS) {
                onSuccess(o);
            }else {
                view.showError(message);
                onError();
            }
        } catch (Exception e) {
            e.printStackTrace();
            view.showError(e.toString());
            onError();
        }
    }

    @Override
    public void onError( Throwable e) {
        if (view != null){
            view.hideLoading();
        }

        if (e instanceof HttpException) {
            //HTTP错误
            onException(BAD_NETWORK);
        } else if (e instanceof ConnectException || e instanceof UnknownHostException) {
            //连接错误
            onException(CONNECT_ERROR);
        } else if (e instanceof InterruptedIOException) {
            //连接超时
            onException(CONNECT_TIMEOUT);
        } else if (e instanceof JsonParseException
                || e instanceof JSONException
                || e instanceof ParseException) {
            //解析错误
            onException(PARSE_ERROR);
        } else {
            if (e != null) {
               if(view != null) view.showError(e.toString());
            } else {
                if(view != null) view.showError("未知错误");
            }
            onError();
        }

    }

    /**
     * 异常情况
     * @param unknownError 异常情况对应的数值
     */
    private void onException(int unknownError) {
        String message = "";
        switch (unknownError) {
            case CONNECT_ERROR:
                message = "连接错误";
                break;

            case CONNECT_TIMEOUT:
                message = "连接超时";
                break;

            case BAD_NETWORK:
                message = "网络问题";
                break;

            case PARSE_ERROR:
                message = "解析数据失败";
                break;

            default:
                break;
        }
        view.showError(message);
        onError();
    }

    @Override
    public void onComplete() {
        //完成后隐藏加载框
        if (view != null) view.hideLoading();
    }

    public abstract void onSuccess(T o);

    public abstract void onError();
}

  3.1.4 【BaseView】----3种请求状态的接口

 public interface BaseView {

    /**
     * 显示dialog
     */
    void showLoading(String message);

    /**
     * 隐藏 dialog
     */

    void hideLoading();

    /**
     * 显示错误信息
     *
     * @param msg 消息内容
     */
    void showError(String msg);

}

  3.1.5 【BasePresenter】

public class BasePresenter<V extends BaseView> {

    private CompositeDisposable compositeDisposable;


    public V baseView;

    protected ApiServer apiServer = ApiRetrofit.getInstance().getApiServer();

    public BasePresenter(V baseView) {
        this.baseView = baseView;
    }

    public BasePresenter() {
    }

    /**
     * 解除绑定
     */
    public void detachView() {
        baseView = null;
        removeDisposable();
    }

    /**
     * 返回 view
     * @return 视图
     */
    public V getBaseView() {
        return baseView;
    }

    public void addDisposable(Observable<?> observable, BaseObserver observer) {
        if (compositeDisposable == null) {
            compositeDisposable = new CompositeDisposable();
        }
        compositeDisposable.add(observable.subscribeOn(Schedulers.io())
                .observeOn(AndroidSchedulers.mainThread())
                .subscribeWith(observer));
    }

    public void addDisposable(Observable<?> observable, DisposableObserver observer) {
        if (compositeDisposable == null) {
            compositeDisposable = new CompositeDisposable();
        }
        compositeDisposable.add(observable.subscribeOn(Schedulers.io())
                .observeOn(AndroidSchedulers.mainThread())
                .subscribeWith(observer));
    }

    public void removeDisposable() {
        if (compositeDisposable != null) {
            compositeDisposable.dispose();
        }
    }
}

  3.1.6 【BaseActivity】

public abstract class BaseActivity<P extends BasePresenter<? extends BaseView>> extends AppCompatActivity implements BaseView{

    private LoadingDialog dialog;
    public Context context;
    protected P presenter;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        context = this;
        setContentView(getLayoutId());
        presenter = createPresenter();
        initView();
        initData();
    }

    /**
     * 关闭加载框
     */
    private void closeLoadingDialog() {
        if (dialog != null && dialog.isShowing()) {
            dialog.dismiss();
        }
    }

    /**
     * 显示加载框
     * @param message 加载框内容
     */
    private void showLoadingDialog(String message) {

        if (dialog == null) {
            dialog = new LoadingDialog(context);
        }
        dialog.setMessage(message);
        dialog.setCancelable(false);
        dialog.show();
    }

    /**
     * 吐司弹窗
     */
    public void showToast(String s) {
        Toast.makeText(context, s, Toast.LENGTH_SHORT).show();
    }


    @Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
        if (ev.getAction() == MotionEvent.ACTION_DOWN) {
            View v = getCurrentFocus();
            //当isShouldHideInput(v, ev)为true时,表示的是点击输入框区域,则需要显示键盘,
            //同时显示光标,反之,需要隐藏键盘、光标
            if (isShouldHideInput(v, ev)) {
                //处理EditText的光标隐藏、显示逻辑
                closeKeyboard((EditText) v, this);
            }
            return super.dispatchTouchEvent(ev);
        }
        // 必不可少,否则所有的组件都不会有TouchEvent了
        if (getWindow().superDispatchTouchEvent(ev)) {
            return true;
        }
        return onTouchEvent(ev);
    }

    /**
     * 关闭软键盘的方法(写在 KeyBoardUtils 的工具类里面)
     * @param mEditText 输入框
     * @param mContext  上下文
     */
    public static void closeKeyboard(EditText mEditText, Context mContext) {
        if (mEditText != null && mContext != null) {
            InputMethodManager imm = (InputMethodManager) mContext
                    .getSystemService(Context.INPUT_METHOD_SERVICE);
            imm.hideSoftInputFromWindow(mEditText.getWindowToken(), 0);
        }
    }

    /**
     * 判断点击的区域是否EditText之外
     * @param v 视图
     * @param event 手势事件
     * @return 返回true说明点击的是输入框区域外
     */
    public boolean isShouldHideInput(View v, MotionEvent event) {
        if ((v instanceof EditText)) {
            int[] leftTop = {0, 0};
            //获取输入框当前的location位置
            v.getLocationInWindow(leftTop);
            int left = leftTop[0];
            int top = leftTop[1];
            int bottom = top + v.getHeight();
            int right = left + v.getWidth();
            // 点击的是输入框区域,保留点击EditText的事件
            return !(event.getX() > left) || !(event.getX() < right)
                    || !(event.getY() > top) || !(event.getY() < bottom);
        }
        return false;
    }

    @Override
    public void showLoading(String message) {
        showLoadingDialog(message);
    }

    @Override
    public void hideLoading() {
        closeLoadingDialog();
    }

    @Override
    public void showError(String msg) {
        showToast(msg);
    }

    protected abstract int getLayoutId();

    public abstract void initData();

    public abstract void initView();

    protected abstract P createPresenter();
}

  3.2 新建api目录,在该目录下创建ApiService,ApiRetrofit文件
  3.2.1 【ApiService】----接口url都放在这里

public interface ApiService {
    String BASE_HOST = "https://mock.apipost.cn/app/mock/project/74ca9bd8-917d-4615-90f0-98e9e5001f38/";  //[ApiPost]mock测试地址

    //用户登录
    @POST("demo/network/login")
    Observable<BaseModel<LoginBean>> userLogin(@Body Map<String,Object> map);
}

  3.2.2 【ApiRetrofit】

public class ApiRetrofit {

    private final ApiService apiService;
    private static ApiRetrofit apiRetrofit;
    private static final String TAG = "ApiRetrofit";

    //构造函数
    public ApiRetrofit(){
        //网络日志
        HttpLoggingInterceptor httpLoggingInterceptor = new HttpLoggingInterceptor(s -> {
            try {
                String text = URLDecoder.decode(s, "utf-8");
                Log.e(TAG, text);
            }catch (UnsupportedEncodingException e){
                e.printStackTrace();
                Log.e(TAG, s);
            }
        });
        httpLoggingInterceptor.setLevel(HttpLoggingInterceptor.Level.BODY);

        //配置OkHttpClient
        OkHttpClient client = new OkHttpClient.Builder()
                .addInterceptor(httpLoggingInterceptor) //添加日志
                .connectTimeout(30, TimeUnit.SECONDS) //请求时限 30s
                .readTimeout(30, TimeUnit.SECONDS) //读取时限 30s
                .writeTimeout(30, TimeUnit.SECONDS) //写入时限 30s
                .build();

        //配置Retrofit
        Retrofit retrofit = new Retrofit.Builder()
                .baseUrl(BASE_HOST) //基础域名
                .addConverterFactory(GsonConverterFactory.create()) //增加返回值为Gson的支持(以实体类返回)
                .addConverterFactory(ScalarsConverterFactory.create()) //增加返回值为String的支持
                .addCallAdapterFactory(RxJava2CallAdapterFactory.create()) //怎加返回值为Observable<T>的支持
                .client(client) //绑定OkHttpClient
                .build();

        apiService = retrofit.create(ApiService.class); //创建api实例
    }

    //双重检查,实现单例
    public static ApiRetrofit getInstance(){
        if(apiRetrofit == null){
            //使用同步锁,当有线程在执行时,保证其它线程不会进入该方法体.
            synchronized (Object.class){
                if(apiRetrofit == null){
                    apiRetrofit = new ApiRetrofit();
                }
            }
        }
        return apiRetrofit;
    }

    public ApiService getApiService(){ return apiService; }

}

  3.3 新建model目录,用于存放请求结果实体类。
  我们测试一个登录接口。所以在model下生成一个LoginBean的实体类。

public class LoginBean implements Serializable {

    private String username;
    private String email;
    private String address;
    private String token;

    public String getUsername() {
        return username;
    }

    public void setUsername(String username) {
        this.username = username;
    }

    public String getEmail() {
        return email;
    }

    public void setEmail(String email) {
        this.email = email;
    }

    public String getAddress() {
        return address;
    }

    public void setAddress(String address) {
        this.address = address;
    }

    public String getToken() {
        return token;
    }

    public void setToken(String token) {
        this.token = token;
    }
}

  3.4 新建widgets目录,用于存放自定义的控件。
  我们在这里加一个加载组件LoadingDialog。注意:组件中会用到资源文件,请到我的github取,地址在最下面。


public class LoadingDialog extends ProgressDialog {
    TextView mLoadingTextView;
    View mView;

    /**
     * 这里的Context 必须用actiivty 不能用applicationContext
     *
     * @param context 上下文
     */
    public LoadingDialog(Context context) {
        this(context, "");
    }

    public LoadingDialog(Context context, CharSequence msg) {
        super(context, R.style.LoadingDialogStyle);
        mView = View.inflate(context, R.layout.dialog_loading, null);
        mLoadingTextView = (TextView) mView.findViewById(R.id.mLoadingTextView);
        if (!TextUtils.isEmpty(msg)) {
            mLoadingTextView.setText(msg);
            mLoadingTextView.setVisibility(View.VISIBLE);
        } else {
            mLoadingTextView.setVisibility(View.GONE);
        }
        this.setCanceledOnTouchOutside(false);
        this.setCancelable(true);
    }


    @Override
    public void show() {
        try {
            if (this.isShowing()) {
                this.dismiss();
            } else {
                super.show();
            }
            //setContentView()一定要在show之后调用
            this.setContentView(mView);
        } catch (WindowManager.BadTokenException exception) {
        }
    }


    public void setMessage(String message) {
        if (!TextUtils.isEmpty(message)) {
            if (mLoadingTextView.getVisibility() != View.VISIBLE) {
                mLoadingTextView.setVisibility(View.VISIBLE);
            }
            mLoadingTextView.setText(message);
        }else {
            if (mLoadingTextView.getVisibility() == View.VISIBLE) {
                mLoadingTextView.setVisibility(View.GONE);
            }
        }
    }

    public void setMessage(@StringRes int message) {
        if (mLoadingTextView.getVisibility() != View.VISIBLE) {
            mLoadingTextView.setVisibility(View.VISIBLE);
        }
        mLoadingTextView.setText(message);
    }

}

  3.5 新建presenter目录,在该目录下新建LoginPresenter文件。

public class LoginPresenter extends BasePresenter<LoginView> {
    LoginView loginView;

    //通过构造函数传递LoginView
    public LoginPresenter(LoginView loginView){this.loginView = loginView;}

    public void login(Map<String,Object> map){
        BaseObserver<BaseModel<LoginBean>> baseObserver = new BaseObserver<BaseModel<LoginBean>>(loginView) {
            @Override
            public void onSuccess(BaseModel<LoginBean> o) {
                loginView.onLoginSuccess(o.getData());
            }

            @Override
            public void onError() {
                loginView.onLoginFailed();
            }
        };
        addDisposable(apiService.userLogin(map),baseObserver);
    }
}

  3.6 最后我们来修改MainActivity文件和它的布局文件
  3.6.1【MainActivity】

public class MainActivity extends BaseActivity<LoginPresenter> implements LoginView {

    EditText account;
    EditText password;
    TextView confirmedLogin;
    LoginPresenter presenter;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
    }

    @Override
    protected int getLayoutId() {
        return R.layout.activity_main;
    }

    @Override
    public void initData() {

    }

    @Override
    public void initView() {
        account = findViewById(R.id.account);
        password = findViewById(R.id.password);
        confirmedLogin = findViewById(R.id.confirmed_btn);

        confirmedLogin.setOnClickListener(onLoginHandler);
    }

    /**
     * 点击登录按钮事件
     */
    private final View.OnClickListener onLoginHandler = new View.OnClickListener() {
        @Override
        public void onClick(View view) {
            if(account.getText().toString().isEmpty()){
                Toast.makeText(context, "请输入账号", Toast.LENGTH_SHORT).show();
            }else if(password.getText().toString().isEmpty()){
                Toast.makeText(context, "请输入密码", Toast.LENGTH_SHORT).show();
            }else {
                Map<String,Object> map = new HashMap<>();
                map.put("username",account.getText().toString());
                map.put("password",password.getText().toString());
                presenter.login(map);
            }
        }
    };

    @Override
    protected LoginPresenter createPresenter() {
        presenter = new LoginPresenter(this);
        return presenter;
    }


    @Override
    public void onLoginSuccess(LoginBean data) {
        Toast.makeText(context, "登录成功", Toast.LENGTH_SHORT).show();
    }

    @Override
    public void onLoginFailed() {

    }
}

  3.6.2【activity_main.xml】

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <EditText
        android:id="@+id/account"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_marginHorizontal="30dp"
        android:layout_marginBottom="20dp"
        android:hint="@string/please_input_phone"
        android:inputType="text"
        android:autofillHints=""
        app:layout_constraintBottom_toTopOf="@id/password" />

    <EditText
        android:id="@+id/password"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_marginHorizontal="30dp"
        android:layout_marginBottom="50dp"
        android:hint="@string/please_input_pwd"
        android:inputType="text"
        app:layout_constraintBottom_toTopOf="@id/confirmed_btn"/>

    <TextView
        android:id="@+id/confirmed_btn"
        android:layout_width="match_parent"
        android:layout_height="50dp"
        android:text="@string/confirmed_login"
        android:textColor="@color/white"
        android:background="@color/black"
        android:layout_marginHorizontal="30dp"
        android:gravity="center"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

</androidx.constraintlayout.widget.ConstraintLayout>

  到此为止,rxjava+retrofit+okhttp的网络架构就全部搭建好了。最后附上github地址https://github.com/shijia2118/demo

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,258评论 6 498
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,335评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 162,225评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,126评论 1 292
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,140评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,098评论 1 295
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,018评论 3 417
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,857评论 0 273
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,298评论 1 310
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,518评论 2 332
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,678评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,400评论 5 343
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,993评论 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,638评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,801评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,661评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,558评论 2 352

推荐阅读更多精彩内容

  • “not permitted by network security policy”的四种解决方法 一、在appl...
    潇洒的然然阅读 222评论 0 0
  • 李红武焦点学习分享第60天,约练9次。 今早和肖老师、刘老师一起约练,我们学习中级P44页的小步推演。第一次我当咨...
    dcfac43304da阅读 106评论 0 0
  • 2022年2月25日,中原焦点任丘第二期,张雪平坚持366+47天分享 社会建构论认为,真实只存在于个人的生活脉络...
    XA独钓寒江阅读 177评论 0 0
  • 定制开发软件的优点和好处 一、定制软件VS现成软件 1、现成软...
    小可爱i1536阅读 184评论 0 0
  • 本人很懒,笔记这些都写在有道云;此外,感觉kafka的资料是真的多,且官网关于命令是真的很详细了,把自己常用的命令...
    SweetMojito阅读 394评论 0 0