Android MVP開發模式及Retrofit + RxJava封裝

代碼已上傳到Github,由於接口都是模擬沒法進行測試,明白大概的邏輯就好了!java

歡迎瀏覽個人博客——https://pushy.sitereact

1. MVP模式

1.1 介紹

若是熟悉MVP模式架構的話,對下圖各組件的調用關係應該不陌生:android

和其餘傳統模式相比,MVP有如下的幾個特色:git

  • View再也不負責同步的邏輯,而是由Presenter來負責。
  • View須要提供操做界面的接口給Presenter進行調用。
  • 打破了View原來對於Model的依賴。

那麼這三者的分工分別是什麼呢?github

Model:處理業務邏輯,工做職責:加載遠程網絡或者本地的數據。
View:視圖,工做職責:控制顯示數據的方式。
Presenter:中間者,工做職責:綁定Model、View。json

1.2 結構

咱們仿造GitHub中谷歌官方例子中的安卓架構藍圖來搭建Android中MVP架構模式的結構:api

TIM截圖20181025161729.png

下面,咱們詳細來講明MVP中各個組件的含義和調用方式:緩存

Contract

你可能會好奇,MVP中不是隻有三個組件嗎?爲何多了一個!沒錯,多出來的這個出現正是LoginContract,在MVP模式中,Presenter與View須要提供各自的接口供其餘組件調用。一般,咱們把Presenter和View的接口都定義在*Contract類中:服務器

public class LoginContract {

    interface View {
        void setLoading(boolean v);  // 顯示加載中
    }

    interface Presenter {
        void login();   // 登陸邏輯調用
    }
}

Model

在Android中,Model層主要的職責是用來加載數據。在這裏,咱們一般請求遠程網絡的數據或者加載本地緩存的數據:網絡

public class LoginModel {

    public void login() {
        /* 請求網絡數據 */
    }
}

Presenter

MVP中,Presenter主要用於綁定View和Model,並組織調用不一樣層提供的接口。因此在Presenter層必須持有View和Model對象。

因此咱們讓Presenter實現Contract.Presenter的接口,並提供構造函數注入View的實現類和Model對象:

public class LoginPresenter implements LoginContract.Presenter {

    private LoginContract.View view;
    private LoginModel model;
    
    public LoginPresenter(LoginContract.View view, LoginModel model) {
        this.view = view;
        this.model = model;
    }

    @Override
    public void login() {
        view.setLoading(true);  // 顯示加載中
        model.login();          // 向服務器請求登陸
    }
}

View

在Android中,Activity每每當成是View的實現類。所以咱們讓LoginActivity實現Contract.View接口:

public class LoginActivity extends AppCompatActivity implements LoginContract.View {

    private LoginPresenter presenter;
    
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        
        presenter = new LoginPresenter(this, new LoginModel());
    }

    @Override
    public void setLoading(boolean v) {
        /* 顯示加載中的UI操做 */
    }
}

而且,咱們在onCreate方法中實例化出LoginPresenter對象,並注入View的實現類(即當前這個Activity)和Model對象。

這樣,當用戶觸發按鈕的點擊事件時,咱們就能夠調用Presenter提供的接口來向遠程服務器進行登陸的請求:

@Override
public void onClick(View v) {
    presenter.login(name, password);
}

2. 封裝Retrofit + RxJava

下面,咱們來正式地講解Retrofit + RxJava的封裝過程,並將上面的MVP中各層具體邏輯替換。

首先,咱們先在app/build.gradle中添加retrofit2rxJava庫的相關依賴:

// rxJava相關依賴
implementation 'io.reactivex.rxjava2:rxjava:2.2.2'
implementation 'io.reactivex.rxjava2:rxandroid:2.1.0'
// retrofit2相關依賴和插件
implementation 'com.squareup.retrofit2:retrofit:2.4.0'
implementation 'com.squareup.retrofit2:converter-gson:2.4.0'
implementation 'com.squareup.retrofit2:adapter-rxjava2:2.4.0'

首先,咱們定義RetrofitServiceManager統一輩子成接口實例的管理類:

public class RetrofitServiceManager {

    private static final String BASE_URL = "https://api.example.com";

    private Retrofit mRetrofit;

    public RetrofitServiceManager() {
        // 初始化OkHttpClient對象,並配置相關的屬性
        OkHttpClient okHttpClient = new OkHttpClient.Builder()
                .connectTimeout(5, TimeUnit.SECONDS)  // 設置超時時間
                .build();
        mRetrofit = new Retrofit.Builder()
                .baseUrl(BASE_URL)
                .client(okHttpClient)
                .addConverterFactory(GsonConverterFactory.create()) // 支持Gson自動解析JSON
                .addCallAdapterFactory(RxJava2CallAdapterFactory.create())  // 支持RxJava
                .build();
    }

    private static class SingletonHolder{
        private static final RetrofitServiceManager INSTANCE = new RetrofitServiceManager();
    }

    public static RetrofitServiceManager getInstance() {
        // 返回一個單例對象
        return SingletonHolder.INSTANCE;
    }

    public <T> T create(Class<T> service) {
        // 返回Retrofit建立的接口代理類
        return mRetrofit.create(service);
    }

}

下一步,咱們修改LoginModel裏的具體請求邏輯。在默認構造函數中經過RetrofitServiceManager建立LoginModelService的代理對象,並定義公共的login方法讓Presenter來調用:

public class LoginModel extends BaseModel {

    private LoginModelService service;

    public LoginModel() {
        this.service = RetrofitServiceManager.getInstance().create(LoginModelService.class);
    }

    public Observable<BaseResponse<String>> login(LoginBody body) {
        // 調用父類BaseModel的observe方法進行請求
        return observe(service.login(body));  
    }

    interface LoginModelService {

        @POST("/login")
        Observable<BaseResponse<String>> login(@Body LoginBody body);

    }

}

另外,咱們讓LoginModel繼承了BaseModel。在該類中,作了線程切換的操做,所以在請求時只須要簡單地嗲用父類的observe便可:

public class BaseModel {

    protected  <T> Observable<T> observe(Observable<T> observable){
        return observable
                .subscribeOn(Schedulers.io())
                .unsubscribeOn(Schedulers.io())
                .observeOn(AndroidSchedulers.mainThread());
    }
}

如今,在LoginPresenterlogin方法中,咱們就能夠同時操做viewmodel來控制登陸的UI和請求的邏輯了:

@Override
public void login(String name, String password) {
    LoginBody body = new LoginBody();
    body.name = name;
    body.password = password;

    view.setLoading(true);
    
    model.login(body)
        .subscribe(response -> {
            view.callback(true);  // 成功回調
            view.setLoading(false);
            
        }, throwable -> {
            view.callback(false); // 失敗回調
            view.setLoading(false);
        });
}

能夠看到,Presenter對於不一樣的請求成功或失敗的接口調用View提供的接口,展現給用戶登陸或者失敗的結果。所以咱們只須要在LoginActivity中定義不一樣結果的提示便可:

@Override
public void callback(boolean v) {
    if (v) {
        Toast.makeText(this, "登陸成功", Toast.LENGTH_LONG).show();
    } else {
        Toast.makeText(this, "登陸失敗", Toast.LENGTH_LONG).show();
    }
}

最後,咱們只須要完成如下登陸的UI視圖和調用Presenter提供接口的簡單邏輯,就能夠實現完整的登陸邏輯了:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical">

    <EditText
        android:id="@+id/et_name"
        android:layout_width="match_parent"
        android:layout_height="wrap_content" />

    <EditText
        android:id="@+id/et_password"
        android:layout_width="match_parent"
        android:layout_height="wrap_content" />

    <Button
        android:id="@+id/btn_submit"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:textAllCaps="false"
        android:text="登陸"/>

</LinearLayout>

在登陸按鈕的點擊事件邏輯中調用Presenter的login方法請求登陸:

@Override
public void onClick(View v) {
    switch (v.getId()) {
    case R.id.btn_submit:
        presenter
            .login(etName.getText().toString(), etPassword.getText().toString());
        break;
    }
}

3. 錯誤處理

假設服務器返回的基本數據爲:

// 成功返回的數據
{
    "code":200,
    "data": "Hello World",
    "message": ""
}

// 失敗返回的數據
{
    "code":401,
    "data": "",
    "message": "Unauthorized"
}

咱們針對這種返回數據,BaseResponse提供一個isSuccess方法來判斷的結果是否有錯:

public class BaseResponse<T> {

    public int code;

    public String message;

    public T data;

    /* 是否成功 */
    public boolean isSuccess() {
        return code == 200;
    }

}

而後修改LoginModellogin方法,經過Map的操做符來處理錯誤拋出異常,並進一步封裝返回的數據:

public Observable<BaseResponse<String>> login(LoginBody body) {
    return observe(service.login(body))
        .map(new PayLoad<>())
}

PayLoad類中,判斷請求數據是否成功,若是失敗,則拋出一個錯誤,不然返回成功的數據:

public class PayLoad<T> implements Function<BaseResponse<T>, BaseResponse<T>> {
    
    private static final String TAG = "PayLoad";

    @Override
    public BaseResponse<T> apply(BaseResponse<T> response) throws Exception {
        if (!response.isSuccess()) {
            /* 服務器端返回errno失敗 */
            throw new ServerException(response.code, response.message);
        }
        /* 成功獲取 */
        return response;
    }

}

Presenter中的訂閱回調方法中就能夠捕捉到ServerException異常:

model.login(body)
    .subscribe(response -> {
        view.callback(true);  // 成功回調
        view.setLoading(false);
    }, throwable -> {
        ServerException exception = (ServerException) throwable;
        view.errorCallback(exception);
        view.setLoading(false);
    });

同時,在Activity中也能夠根據服務端返回的不一樣狀態碼來向用戶展現不一樣的錯誤結果:

@Override
public void errorCallback(ServerException e) {
    switch (e.code) {
        case ServerError.NO_USER:
            Toast.makeText(this, "沒有該用戶", Toast.LENGTH_LONG).show();
        break;
        case ServerError.UNAUTHORIZED:
            Toast.makeText(this, "密碼錯誤", Toast.LENGTH_LONG).show();
        break;
    }
}
相關文章
相關標籤/搜索