代碼已上傳到Github,由於接口都是模擬沒法進行測試,明白大概的邏輯就好了!java
歡迎瀏覽個人博客——https://pushy.sitereact
若是熟悉MVP模式架構的話,對下圖各組件的調用關係應該不陌生:android
和其餘傳統模式相比,MVP有如下的幾個特色:git
那麼這三者的分工分別是什麼呢?github
Model:處理業務邏輯,工做職責:加載遠程網絡或者本地的數據。
View:視圖,工做職責:控制顯示數據的方式。
Presenter:中間者,工做職責:綁定Model、View。json
咱們仿造GitHub中谷歌官方例子中的安卓架構藍圖來搭建Android中MVP架構模式的結構:api
下面,咱們詳細來講明MVP中各個組件的含義和調用方式:緩存
你可能會好奇,MVP中不是隻有三個組件嗎?爲何多了一個!沒錯,多出來的這個出現正是LoginContract
,在MVP模式中,Presenter與View須要提供各自的接口供其餘組件調用。一般,咱們把Presenter和View的接口都定義在*Contract
類中:服務器
public class LoginContract { interface View { void setLoading(boolean v); // 顯示加載中 } interface Presenter { void login(); // 登陸邏輯調用 } }
在Android中,Model層主要的職責是用來加載數據。在這裏,咱們一般請求遠程網絡的數據或者加載本地緩存的數據:網絡
public class LoginModel { public void login() { /* 請求網絡數據 */ } }
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(); // 向服務器請求登陸 } }
在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); }
下面,咱們來正式地講解Retrofit + RxJava的封裝過程,並將上面的MVP中各層具體邏輯替換。
首先,咱們先在app/build.gradle
中添加retrofit2
和rxJava
庫的相關依賴:
// 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()); } }
如今,在LoginPresenter
的login
方法中,咱們就能夠同時操做view
和model
來控制登陸的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; } }
假設服務器返回的基本數據爲:
// 成功返回的數據 { "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; } }
而後修改LoginModel
的login
方法,經過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; } }