問題的引出
對於一個基本的Android項目,我們在初學的時候的做法通常都是直接在xml中繪製界面,然後在對應的Activity中做一些響應操作。這樣做在一些demo演示的時候倒是沒什麼問題,但是一旦項目稍微有點規模,就會導致Activity變得臃腫。而對於一個成百上千行的Activity,當我們需要對界面進行修改的時候,會很難快速定位到對應修改的地方,並且由於所有的操作都在activity中,耦合度很高,當修改某一處的時候,可能會涉及到多個方面,從而需要修改多處。
解決這個問題的方法就是將在activity中的操作進行分離,同時對於整個項目也進行功能模塊的分離。這樣的話,當我們需要修改某處的時候,只用修改對應的部分即可。因此就有了mvp模式了,雖然mvp模式也存在很多缺點,巨多的接口,但是對於一箇中小型項目,這點缺點還是可以忽略的。
MVP
而MVP模式指的就是Model,View,Presenter這三者。也就是說MVP模式將集中在Activity中的操作拆分成了三個部分,由這三個部分共同完成一個操作。
其中,model是獲取數據的部分,它只負責獲取數據,而不進行其他操作。View則是界面顯示的部分,同樣的,它只負責展示界面,不進行其他操作,由Activity/Fragment擔任。Presenter是連接model和View的部分,它是主要負責進行邏輯的處理。
因此,一個完整的事件流程理應是這樣:在View中產生某個事件,然後由View轉交給Presenter,Presenter進行邏輯的處理後通知model獲取數據,在Model獲取完數據後回調給Presenter,然後presenter將數據進行處理後交給View去顯示。
在這個過程中,可以看出View與Model是完全沒有聯繫的,它們之間的數據通過Presenter進行互通。另外,Model獲取到的數據也是通過回調接口來提供給Presenter的。這樣,就可以將Model獨立出來,減少數據的耦合。
所以他們的關係也應當如上圖所示,View與Presenter互相持有,可以互相通信。而Model只被Presenter持有,是單向通信的。
小結
對於MVP而言,數據的獲取和界面的顯示是沒有關係的。也就是說,當我們需求進行變化的時候,就可以根據需求而只修改其中的一部分,而不用全部修改。
另外,由於邏輯與界面的顯示是分離的,當我們測試的時候就可以不用對Activity進行註釋修改添加,而是可以直接新建一個Presenter繼承原Presenter,然後進行修改,極大的方便了我們測試。
至於MVP的實現,也不是一成不變的,而是根據自己的習慣或者需求進行一些變動。畢竟MVP模式本身的引出就是爲了解耦,分割各個模塊的。
實現MVP
對於一個應用程序,我們應當保持其界面的一致性,因此這裏先定義了三個接口,分別對應MVP的三個部分的行爲準則,這是三個接口中,定義的是每組MVP都應當擁有的功能,也就是說每組MVP都應當實現這些對應的方法。對於MVP,當然應該至少有三個類的實現,分別對應MVP的每個部分,稱爲一組MVP。
既然說到了這裏,就有必要說一下分包的實現了。有些人習慣建立三個包Model、View、Presenter,然後將對應的文件分別放在這三個包中。還有些人習慣按照功能分包,然後將每個功能對應的MVP放在一起。這裏我就是採用的後者,按照功能分包,同時加入一個Contract來協同每組MVP的行爲。
包結構
這裏,添加了一個Module,這個Module是專門用於放置MVP基礎的搭建的,叫做common。然後還有一個Module,叫做app,是具體的示例程序對應的Module。
具體實現
首先是View。對於View而言,這裏只添加了兩個方法,進度條的顯示和隱藏。當進行某項操作的時候,應當顯示進度條,然後操作結束的時候隱藏,這樣才能使得應用更加友好。
public interface View {
/**
* 顯示進度條,用於提示正在加載
*/
void showProgress();
/**
* 隱藏進度條
*/
void hideProgress();
}
接下來是model的接口,由於這裏model只是負責獲取數據的部分,它是獨立的。因而沒有什麼公共的方法,但是爲了後面的model的統一,這裏還是創建了一個model接口,只是其中並沒有方法而已。
public interface Model {
}
最後就差Presenter了,而Presenter是model和view的連接者,它是需要同時持有model和view。但是對於View而言,它的生命週期很是複雜,我們需要當view創建的時候連接二者,而view銷燬的時候解開連接。否則的話當view銷燬,但是還被Presenter持有的時候,view就不會被回收,從而造成內存的泄露。那麼presenter就應當擁有解除關聯和判斷是否還在關聯的方法。
public interface Presenter {
/**
* 解除關聯
*/
void detach();
/**
* 判斷View是否與當前的Presenter建立連接
*
* @return true爲已建立連接
*/
boolean isAttach();
/**
* 檢查是否與View建立連接
*/
void checkAttach();
}
對於每個model,view,presenter,都應當實現以上的接口。而對於這種共有的方法,每個對應的類都去實現一遍顯然是不合適的,最好的做法是使用一個基類來實現這些方法,其餘的類只要繼承這個基類即可擁有這些方法了。同時也會因爲這些方法都是在基類中實現的,從而會使得每個類的行爲表現得具有一致性。
要知道的是,上面只是定義了三個接口,只是代表每組MVP的行爲而已。實際上還並沒有實現MVP的搭建,那麼下面,將會通過Base基類搭建一個MVP模式基礎。
Model
public class BaseModel implements Model {
}
因爲model沒有共有的方法,所以它的基類也是一個空的類。可能從這裏看的話會顯得多餘,畢竟只是一個空的類,那麼就完全沒必要去創建它。而實際上也的確沒必要去創建這個BaseModel,畢竟接口Model本身就是爲了實現model的一致性了。而之所以這裏又重新創建了這個Base類,是因爲後面將會基於該mvp框架搭建RxJava+Retrofit實現的MVP,而引入BaseModel的話,不僅能夠使得兩種MVP的使用具有一致性,同時也可以在BaseModel中添加model可能需要的共同的方法。
那麼接下來看Presenter的實現。
Presenter
public abstract class BasePresenter<V extends View, M extends Model> implements Presenter {
protected V mView;
protected M mModel;
public BasePresenter(V view) {
mView = view;
mModel = createModel();
}
@Override
public void detach() {
mView = null;
}
@Override
public boolean isAttach() {
return mView != null;
}
@Override
public void checkAttach() {
if (!isAttach()){
throw new RuntimeException("當前Presenter未與View建立連接");
}
}
/**
* 創建Presenter對應的Model
*
* @return Model
*/
protected abstract M createModel();
}
從上面可以看到,這裏presenter實現了presenter接口的共有方法,同時還定義了兩個變量,mView 和mModel ,也就是mvp中的另外兩者。並且只提供了一個構造方法,在這個構造方法中完成和View的綁定。這裏就可以知道,我們沒在presenter的接口中定義綁定的方法,是因爲將綁定的過程放到了構造方法,也就是說當Presenter存在的時候就需要綁定View。
另外,presenter中並沒有直接引用model,而是提供了一個抽象方法來生成model。因爲對於一個presenter而言,model並不是必須存在的,只有當需要獲取數據的時候纔會需要model,因此這裏將model的創建放在了一個抽象方法中,當我們不需要的時候直接返回null即可。
接下來是View的基類了,因爲view是由Activity和Fragment擔任的,所以這裏將會分成兩個BaseView。
View
public abstract class BaseActivity<P extends Presenter> extends AppCompatActivity implements View {
protected P mPresenter;
protected Context mContext;
private Dialog mProgressDialog;
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(getContentView());
mPresenter = createPresenter();
initView();
mContext = this;
mProgressDialog = DialogUtils.getDefaultDialog(mContext);
}
@Override
protected void onDestroy() {
mPresenter.detach();
super.onDestroy();
}
@Override
public void showProgress() {
if (mProgressDialog != null && !mProgressDialog.isShowing()) {
mProgressDialog.show();
}
}
@Override
public void hideProgress() {
if (mProgressDialog.isShowing() && mProgressDialog != null) {
mProgressDialog.dismiss();
}
}
/**
* 返回Content佈局id
*
* @return 佈局文件id
*/
protected abstract @LayoutRes int getContentView();
/**
* View的初始化工作
*/
protected abstract void initView();
/**
* 創建Presenter
*
* @return 與Activity關聯的Presenter
*/
protected abstract P createPresenter();
}
上面是使用Activity作爲View而實現的基礎View。在其中的onCreate中創建presenter和進度條,並且在onDestroy中解除了和Presenter的綁定。另外除了實現基本的顯示隱藏進度條外,還抽象出了三個方法。分別是contentView的佈局xml的ID和初始化的方法以及創建Presenter的方法。這樣的話,當具體的View繼承該基類的時候只需要實現這三個方法即可,而不用去考慮其生命週期的變化了。
此外,爲了對進度條的統一管理,將生成進度條的方法放在了DialogUtils中,這樣當需要修改進度條的樣式的時候將會更加方便。
public class DialogUtils {
/**
* 用於生成默認的正在加載的進度條,該進度條用於普通加載時顯示
*
* @return 對話框
*/
public static Dialog getDefaultDialog(Context context) {
Dialog dialog = new ProgressDialog(context);
dialog.setCancelable(true);
dialog.setCanceledOnTouchOutside(false);
return dialog;
}
}
對於Fragment擔任的View與Activity也是一樣,唯一的區別就是他們生命週期的不同,從而在不同的方法中進行Presenter的綁定和解除。
public abstract class BaseFragment<P extends Presenter> extends Fragment implements com.pgaofeng.common.mvp.View {
protected P mPresenter;
private Dialog mDialog;
protected Context mContext;
@Nullable
@Override
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
View view = inflater.inflate(getContentView(), container, false);
initView(view);
mPresenter = createPresenter();
mContext = getContext();
mDialog = DialogUtils.getDefaultDialog(mContext);
return view;
}
@Override
public void onDestroyView() {
mPresenter.detach();
super.onDestroyView();
}
@Override
public void showProgress() {
if (mDialog != null && !mDialog.isShowing()) {
mDialog.show();
}
}
@Override
public void hideProgress() {
if (mDialog != null && mDialog.isShowing()) {
mDialog.dismiss();
}
}
/**
* 獲取content佈局id
*
* @return 佈局id
*/
protected abstract @LayoutRes int getContentView();
/**
* 初始化View
*
* @param view contentView
*/
protected abstract void initView(View view);
/**
* 創建View對應的Presenter
*
* @return Presenter
*/
protected abstract P createPresenter();
}
至此,對於基礎MVP的封裝已經完成。也就是說,MVP中的三者已經通過這幾個Base類連接起來了,因此我們的封裝到這裏已經算是完成了。只是完成了MVP的封裝又怎麼能滿足我們的要求呢,接下來當然是要具體的通過實際案例來體驗一下封裝的MVP如何使用了。
案例(使用方式)
下面將通過一個小的案例來演示如何使用MVP。這裏的案例就是通過點擊一個按鈕實現獲取數據,然後由textView展示。
先看一下xml的佈局。
xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
tools:context=".main.view.MainActivity">
<TextView
android:id="@+id/tv_myText"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Hello World!" />
<Button
android:id="@+id/btn_update"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="更新" />
</LinearLayout>
對於一個功能,MVP的各個部分都應當有相應的具體的行爲,而行爲應當是使用接口來提取的,但是若每組MVP都提出三個接口的話,會使得文件更加的多了。因此,這裏在使用MVP的時候,使用了Contract來合併了每組的mvp接口。
Contract
public interface MainContract {
interface View {
/**
* 更新TextView的text
*
* @param text 內容
*/
void updateText(String text);
/**
* 顯示Toast
*
* @param message 消息內容
*/
void showToast(String message);
}
interface Presenter {
/**
* 更新TextView的內容
*/
void updateTextViewText();
}
interface Model {
/**
* 獲取TextView 的內容
*
* @param param 請求參數
* @param callBack 請求回調
* @param handler 用於更新主線程UI
*/
void getTextString(String param, ModelCallBack callBack, Handler handler);
}
}
從上面可以看到的是,Contract接口組合了每一組MVP的所有接口,這些接口都應當是它們的行爲準則。在Modle中,獲取數據的方法中傳入了一個回調參數,這個參數是在Model獲取到數據後回調的,由於Presenter和Model是單向通信,因此只能使用回調的方式來將結果傳回Presenter。
View
public class MainActivity extends BaseActivity<MainPresenter> implements MainContract.View {
TextView mTextView;
Button mButton;
@Override
protected int getContentView() {
return R.layout.activity_main;
}
@Override
protected void initView() {
mTextView = findViewById(R.id.tv_myText);
mButton = findViewById(R.id.btn_update);
mButton.setOnClickListener(v -> {
mPresenter.updateTextViewText();
});
}
@Override
protected MainPresenter createPresenter() {
return new MainPresenter(this,new Handler());
}
@Override
public void updateText(String text) {
mTextView.setText(text);
}
@Override
public void showToast(String message) {
Toast.makeText(this, message, Toast.LENGTH_SHORT).show();
}
}
MainActivity實現了View的接口,同時繼承的是前面封裝的BaseActivity。可以看到的是,Activity已經很簡潔了,它其中的一些方法除了必要的一些外就只剩下操作界面UI的了,這與我們的期望是一致的,View只負責顯示UI即可。
另外,引入的泛型是MainPresenter,也就是說該View持有的Presenter就是MainPresenter。這樣的話,就可以根據用戶的需求去主動地調用Presenter的方法了,這裏是通過按鈕的點擊來觸發的更新text操作。
Presenter
public class MainPresenter extends BasePresenter<MainActivity, MainModel> implements MainContract.Presenter {
public MainPresenter(MainActivity view, Handler handler) {
super(view);
this.mHandler = handler;
}
private String[] params = {"success", "fail"};
private Handler mHandler;
private Random random = new Random(System.currentTimeMillis());
@Override
protected MainModel createModel() {
return new MainModel();
}
@Override
public void updateTextViewText() {
mView.showProgress();
// 模擬獲取數據的幾種狀態,這裏通過參數來決定是否獲取數據成功
int index = random.nextInt(2);
mModel.getTextString(params[index], new ModelCallBack() {
@Override
public void success(BaseData<?> baseData) {
checkAttach();
String s = (String) baseData.getData();
if (!TextUtils.isEmpty(s)) {
mView.updateText(s);
}
mView.hideProgress();
mView.showToast(baseData.getMessage());
}
@Override
public void fail(Throwable throwable) {
checkAttach();
mView.showToast("獲取數據失敗!");
mView.hideProgress();
}
}, mHandler);
}
}
而MainPresenter中只實現了一個方法,也就是Contract中的presenter中定義的方法。這裏模擬的是獲取數據,但是由於我們並沒有真實的數據可以獲取,這裏就使用隨機來模擬了兩種情況,成功和失敗這兩種情況。
同時在構造方法中傳入了一個Handler,之所以使用handler,是因爲通常獲取數據是一個耗時的操作,因此應該在子線程中進行。而界面的操作則必須在UI線程中進行,因此這裏使用Handler來操作UI。而從上面可以看出在Presenter中還是調用了Model的獲取數據的,其中由於獲取數據可能是個耗時,因此Model的方法參數傳入的除了參數外還有一個回調和Handler,然後在Model獲取完數據後將會調用這個方法將結果傳回給Presenter。
而之所以把Handler也傳遞給了Model,是因爲這裏分配職責的時候,把線程的切換交給了Model來處理。也就是說,Model如何獲取數據以及在什麼線程獲取都是由它自己決定,而Presenter只需要它在UI線程中將結果回調回來即可。
CallBack
public interface ModelCallBack {
/**
* 請求成功的回調
*
* @param baseData 返回值的基礎bean
*/
void success(BaseData<?> baseData);
/**
* 請求失敗的回調
*
* @param throwable 錯誤異常信息
*/
void fail(Throwable throwable);
}
這裏定義回調接口的時候,已經限定了獲取成功的回調參數。也就是說,Model必須要將數據轉換成我們所需的格式再回調回來。
BaseData
public class BaseData<T> {
/**
* 返回碼
*/
private int code;
/**
* 返回提示消息
*/
private String message;
/**
* 返回的實際數據內容
*/
private T data;
...
// getter,setter
}
這裏可以看到,BaseData封裝了三個參數,code,Message和Data。其中code和message是對於此次獲取的數據的一個描述,其中code應當是與後臺協議後的返回碼,而message則是對於code的一些描述消息。data使用的是泛型,是真正存儲數據的地方。
那麼繼續看Model。
Model
public class MainModel extends BaseModel implements MainContract.Model {
@Override
public void getTextString(final String param, final ModelCallBack callBack, Handler handler) {
if (callBack == null) {
throw new RuntimeException("回調不應爲空");
}
new Thread(() -> {
try {
// 模擬在子線程中獲取數據
Thread.sleep(2000);
// 獲取數據結束結束後切換主線程回調
handler.post(() -> {
BaseData<String> baseData = new BaseData<>();
switch (param) {
case "success":
baseData.setCode(0);
baseData.setMessage("獲取成功");
baseData.setData("我是獲取到的消息內容");
callBack.success(baseData);
break;
case "fail":
callBack.fail(new RuntimeException("人爲異常!"));
break;
default:
break;
}
});
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
}
}
Model只有一個方法,就是獲取數據的那個方法。其中開闢了一個子線程,然後在線程中sleep了2秒作爲獲取數據的耗時。然後通過handler在UI線程把數據傳遞給了Presenter,最後Presenter會通知View去展示這個數據。
總結
MVP模式主要的作用就是解耦,完全是爲了降低耦合度發展而來的一種編碼模式。將界面的展示和數據的獲取進行分離,會使得整個項目的結構更加清晰。這樣不僅能在需求修改的時候及時定位到目標,還能更加方便得進行功能模塊測試。而這樣的分包分模塊更是方便團隊協作,以及新人對項目的接手難度。
- 附: 代碼上傳github,點擊查看代碼
- 對本文的改進:基於RxJava+Retrofit的MVP模式