1. 程式人生 > >Android MVP開發模式及Retrofit + RxJava封裝

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

程式碼已上傳到Github,因為介面都是模擬無法進行測試,明白大概的邏輯就行了!

歡迎瀏覽我的部落格——https://pushy.site

1. MVP模式

1.1 介紹

如果熟悉MVP模式架構的話,對下圖各元件的呼叫關係應該不陌生:

和其他傳統模式相比,MVP有以下的幾個特點:

  • View不再負責同步的邏輯,而是由Presenter來負責。
  • View需要提供操作介面的介面給Presenter進行呼叫。
  • 打破了View原來對於Model的依賴。

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

Model:處理業務邏輯,工作職責:載入遠端網路或者本地的資料。
View:檢視,工作職責:控制顯示資料的方式。
Presenter:中間者,工作職責:繫結Model、View。

1.2 結構

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

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