前言
作為菜鳥一只,學(xué)習(xí)的新知識都要記下來歧强,以便日后復(fù)習(xí)鲤看。
本文側(cè)重點在于介紹 Android MVP 的優(yōu)劣朝卒,通過 Google 官方的to-do-mvp 系列項目了解官方是如何使用 MVP 的博烂,并通過自己動手寫一個小小的 MVP-demo 來加深對該模式的理解香椎。
不廢話了,下面進入正文禽篱。
MVC
談到 MVP畜伐,就不能不提它的“前身”- MVC,但為了更好的了解躺率,我們還需要向上追溯到 三層架構(gòu) :
- 界面層:與用戶交互的界面
- 業(yè)務(wù)邏輯層:界面層和數(shù)據(jù)訪問層的橋梁玛界,實現(xiàn)業(yè)務(wù)邏輯。
- 數(shù)據(jù)訪問層:和數(shù)據(jù)庫打交道悼吱,類似DAO慎框。
而 MVC 實際上更多只涉及前兩層,目的在于解除業(yè)務(wù)邏輯和視圖之間的耦合后添”靠荩可以說不只是 Android ,甚至在整個軟件開發(fā)中都是使用最廣的系統(tǒng)架構(gòu)之一遇西。
MVC 將整個結(jié)構(gòu)分為三個組件--Model馅精、View、Controller
- Model(模型):Model 是應(yīng)用程序的數(shù)據(jù)源粱檀,同時包括對業(yè)務(wù)邏輯的封裝洲敢。它接受 Controller 的請求并完成相應(yīng)的業(yè)務(wù)處理,并將處理后的數(shù)據(jù)通過 View 顯示給用戶茄蚯。數(shù)據(jù)源可以是Web压彭、本地數(shù)據(jù)庫(sqlite)等。
- View(視圖):該組件直接與用戶交互渗常,并負(fù)責(zé)用戶如何查看我們的應(yīng)用程序壮不。View 可以直接與 Model 進行交互,在MVC中凳谦,XML(也可以說是 Activity )被視為視圖。
- Controller(控制器):這是MVC模式的重要部分衡未,Controller是操作尸执、編輯、使用 Model 并通過 View 顯示給用戶的組件缓醋。Controller 負(fù)責(zé)收集所有數(shù)據(jù)如失,在Model 和 View 之間充當(dāng)中間人。Activity/Fragment 被認(rèn)為是Android 的 Controller 送粱。
一句話概括 MVC 的工作機理就是:當(dāng) User 觸發(fā)事件時褪贵,View 發(fā)送指令到Controller,之后 Controller 通知 Model 更新數(shù)據(jù),之后將結(jié)果顯示到 View 中脆丁。
過程很理想是吧世舰?但是在 Android 卻并不怎么令人滿意,我們來看看在 Android 中是個什么情況槽卫。
首先 布局.xml 毫無疑問是 View 吧跟压,然后一些 java bean 之類的就是 Model,而 Controller 則是 Activity/Fragment 咯歼培,但是理想很豐滿震蒋,現(xiàn)實很骨感,作為 View 而言躲庄,xml 顯然是不能勝任的查剖,它只能展示最基礎(chǔ)的靜態(tài)界面,比如當(dāng)我們動態(tài)隱藏顯示一個界面時候噪窘,我們必須在 Activity/Fragment 中去實現(xiàn)笋庄,這也就導(dǎo)致了 Activity/Fragment 既是 Controller 但是又承擔(dān)了一部分 View 的工作。
可以說 Android 中的 MVC 只做到了 M-V效览,因為所有一切都和 Activity 緊密相連无切。
MVC 在 Android 中的表現(xiàn)大致如下:
上述結(jié)果就是,Activity 中的代碼輕輕松松上千行丐枉。如果只是我們自己寫哆键,自己維護的話,上千行似乎并非不能接受瘦锹。但是一但需要你去看別人的上千行代碼籍嘹,想想就很難受。(更別說需要研讀 Android 破萬行的源碼了弯院。辱士。)
為了解決這個重大問題,我們需要將 Activity 承擔(dān)的工作拆分听绳,Activity 只控制 View颂碘,另外新建一個 Controller ,以此避免 Activity 越來越大椅挣,難以維護头岔。
于是就衍生出了 MVP。
MVP
MVP 作為 MVC 的衍生鼠证,將 Controller 和 View 從 Activity 中分割開開筒捺。對于 Android 來說边败,MVP 的 Model 和 MVC 中的 Model 是一樣的,而 Activity/Fragment 不再是 Controller,而是純粹的 View,所有關(guān)于用戶事件的處理都通過 Presenter。
我們可以看到,最明顯的差別就是 Model 和 View 不再相連,取而代之的是 Presenter 在二者之間充當(dāng)橋梁载城,分別與 Model 和 View 雙向通信。
工作流程大致為:
- View 接受用戶的交互請求
- View 將事件傳遞到 Presenter
- Presenter 操作 Model 進行數(shù)據(jù)處理
- Model 處理完成后戚宦,通知 Presenter 處理已完成
- Presenter 根據(jù)處理后的數(shù)據(jù)更新 View 的顯示
至于 Presenter 如何與 Model个曙、View 交互的,還記得設(shè)計模式中提到的 面向接口編程 的思想么受楼?沒錯垦搬,這里我們也是采取接口的形式,比如 Activity/Fragment 實現(xiàn)已經(jīng)定義好的接口艳汽,在對應(yīng)的 Presenter 中通過接口調(diào)用方法猴贰。
下面我們就看一看 Google 為我們提供的 MVP 示例中是如何編程的。
如果讀者對 面向接口編程 的思想不了解河狐,那么建議先去Google 一下米绕,有基本的了解之后,再繼續(xù)閱讀馋艺,不然只能是徒增痛苦栅干。。捐祠。碱鳞。
Google todo-mvp 項目介紹
Google 在 GayHub 上推出了一個項目 Android Architecture Blueprints,用來展示 Android 使用各種各樣的 MVP 架構(gòu)踱蛀,雖然Google 表示其中的示例只是用來做參考窿给,并不是要做標(biāo)準(zhǔn),但是作為 Google 腦殘粉率拒,相信 Google 出品崩泡,必屬精品 。
項目中設(shè)計多種架構(gòu)猬膨,但作為菜雞一枚角撞,還是只從最基礎(chǔ)的 todo-mvp 入手,分析如何實現(xiàn) MVP 架構(gòu)勃痴。
總體結(jié)構(gòu)
以 StatisticsContract 為例(其他類似):
基類
//在 Fragment 的 onResume()中調(diào)用方法谒所,作用是 presenter 開始獲取數(shù)據(jù)并調(diào)用 view 中方法改變界面顯示
public interface BasePresenter {
void start();
}
//在 presenter 實現(xiàn)類中調(diào)用方法,作用是將 presenter 實例傳入 view 中
public interface BaseView<T> {
void setPresenter(T presenter);
}
“契約類”-XXXContract
public interface StatisticsContract {
interface View extends BaseView<Presenter> {
void setProgressIndicator(boolean active);
void showStatistics(int numberOfIncompleteTasks, int numberOfCompletedTasks);
void showLoadingStatisticsError();
boolean isActive();
}
interface Presenter extends BasePresenter {
}
}
Google 似乎很喜歡這種寫法召耘,編寫一個契約類來管理 View 和 Presenter 的所有接口百炬,這種方式使得我們很清楚的知道這二者有哪些功能褐隆,方便維護污它。
Activity 的作用
在子模塊中扮演著 模塊管理者 的角色,負(fù)責(zé)創(chuàng)建 Presenter 實例以及 創(chuàng)建 View(Fragment),并將二者聯(lián)系起來:
@Override
protected void onCreate(Bundle savedInstanceState) {
//衫贬。德澈。。固惯。
StatisticsFragment statisticsFragment = (StatisticsFragment) getSupportFragmentManager()
.findFragmentById(R.id.contentFrame);
if (statisticsFragment == null) {
statisticsFragment = StatisticsFragment.newInstance();
ActivityUtils.addFragmentToActivity(getSupportFragmentManager(),statisticsFragment, R.id.contentFrame);
}
new StatisticsPresenter(
Injection.provideTasksRepository(getApplicationContext()), statisticsFragment);
//梆造。。葬毫。镇辉。
}
在創(chuàng)建 StatisticsPresenter 時,我們傳入了 statisticsFragment贴捡,再看看 StatisticsPresenter 的構(gòu)造函數(shù):
public StatisticsPresenter(@NonNull TasksRepository tasksRepository,
@NonNull StatisticsContract.View statisticsView) {
mTasksRepository = checkNotNull(tasksRepository, "tasksRepository cannot be null");
mStatisticsView = checkNotNull(statisticsView, "StatisticsView cannot be null!");
mStatisticsView.setPresenter(this);
}
也就是說 這時 StatisticsPresenter 獲取到了 statisticsFragment 的引用忽肛,且其實現(xiàn)了 view 接口,那么就可以調(diào)用 view 的方法了烂斋。
View <--> Presenter
分析一下 View 如何與 Presenter 雙向通信屹逛,上源碼:
public class StatisticsFragment extends Fragment implements StatisticsContract.View {
private TextView mStatisticsTV;
private StatisticsContract.Presenter mPresenter;
public static StatisticsFragment newInstance() {
return new StatisticsFragment();
}
@Override
public void setPresenter(@NonNull StatisticsContract.Presenter presenter) {
mPresenter = checkNotNull(presenter);
}
@Override
public void onResume() {
super.onResume();
mPresenter.start();
}
//。汛骂。罕模。。
}
可以看到帘瞭,F(xiàn)ragment 作為 View 淑掌,同時在 setPresenter 方法中得到 Presenter 實例(結(jié)合 Presenter 的構(gòu)造方法),從而可以調(diào)用 Presenter 中的方法图张。
而上面我們也提到過在 presenter 的構(gòu)造方法中獲取到了 fragment 也就是view 的引用锋拖。于是 二者就可以互相通信了。
Model <--> Presenter
分析完 View-Presenter祸轮,再來看看 Model 如何與 Presenter 雙向交互兽埃,源碼:
public class StatisticsPresenter implements StatisticsContract.Presenter {
private final TasksRepository mTasksRepository;
public StatisticsPresenter(@NonNull TasksRepository tasksRepository,
@NonNull StatisticsContract.View statisticsView) {
mTasksRepository = checkNotNull(tasksRepository, "tasksRepository cannot be null");
mStatisticsView = checkNotNull(statisticsView, "StatisticsView cannot be null!");
mStatisticsView.setPresenter(this);
}
@Override
public void start() {
loadStatistics();
}
private void loadStatistics() {
mStatisticsView.setProgressIndicator(true);
// The network request might be handled in a different thread so make sure Espresso knows
// that the app is busy until the response is handled.
EspressoIdlingResource.increment(); // App is busy until further notice
mTasksRepository.getTasks(new TasksDataSource.LoadTasksCallback() {
@Override
public void onTasksLoaded(List<Task> tasks) {
int activeTasks = 0;
int completedTasks = 0;
//.....
mStatisticsView.setProgressIndicator(false);
mStatisticsView.showStatistics(activeTasks, completedTasks);
}
@Override
public void onDataNotAvailable() {
// The view may not be able to handle UI updates anymore
if (!mStatisticsView.isActive()) {
return;
}
mStatisticsView.showLoadingStatisticsError();
}
});
}
}
上述代碼中的 TasksRepository 即為 Model,仍然是在構(gòu)造函數(shù)中獲取到其引用适袜,同時在 loadStatistics() 方法中調(diào)用 mTasksRepository.getTasks()方法柄错,體現(xiàn)的 presenter 調(diào)用 model 的方法;同時 getTasks() 方法中傳入了 TasksDataSource.LoadTasksCallback() 參數(shù)苦酱,該接口定義如下:
public interface TasksDataSource {
interface LoadTasksCallback {
void onTasksLoaded(List<Task> tasks);
void onDataNotAvailable();
}
//售貌。。疫萤。颂跨。
而在 TasksRepository(Model)中 getTasks() 方法定義如下:
public class TasksRepository implements TasksDataSource {
@Override
public void getTasks(@NonNull final LoadTasksCallback callback) {
checkNotNull(callback);
// Respond immediately with cache if available and not dirty
if (mCachedTasks != null && !mCacheIsDirty) {
callback.onTasksLoaded(new ArrayList<>(mCachedTasks.values()));
return;
}
if (mCacheIsDirty) {
// If the cache is dirty we need to fetch new data from the network.
getTasksFromRemoteDataSource(callback);
} else {
// Query the local storage if available. If not, query the network.
mTasksLocalDataSource.getTasks(new LoadTasksCallback() {
@Override
public void onTasksLoaded(List<Task> tasks) {
refreshCache(tasks);
callback.onTasksLoaded(new ArrayList<>(mCachedTasks.values()));
}
@Override
public void onDataNotAvailable() {
getTasksFromRemoteDataSource(callback);
}
});
}
}
//。扯饶。恒削。池颈。。
}
也就是說钓丰,在getTasks()方法中躯砰,TasksRepository 處理完數(shù)據(jù)之后,回調(diào)了 StatisticsPresenter 中實現(xiàn)的方法携丁,也就體現(xiàn)了 Model -> Presenter琢歇。
小結(jié)
到這里,關(guān)于 todo-mvp 的架構(gòu)就分析的差不多了梦鉴,總體上看李茫,MVP 的使用使得整個結(jié)構(gòu)十分的清晰,畢竟我這樣的菜鳥都能去分析源碼了肥橙,雖然代碼量略微增多涌矢,但是每個模塊的界限很清晰,責(zé)任單一快骗,高度的解耦娜庇,使得維護起來很輕松。
動手?jǐn)]一個 Demo
上面我們分析了Google的源碼方篮,但看懂畢竟只是看懂名秀,距離我們深入理解還差得遠(yuǎn),下面就動手實踐一下藕溅,擼一個 MVP 的簡易 Demo匕得。
Demo 地址見文章結(jié)尾。
先放上最終的效果:
接下來是 Demo 的結(jié)構(gòu)圖:
下面我們就來一點點實現(xiàn)巾表。
Model
首先肯定需要有一個實體類 User汁掠,然后對于這個 Demo,業(yè)務(wù)邏輯只有一個:登錄集币,那么我們也就至少有一個 login() 方法來實現(xiàn)登錄業(yè)務(wù)考阱。同時,我還需要能告知 presenter 我是否登陸成功了鞠苟,那么我就需要寫一個 回調(diào)接口 供 presenter 來實現(xiàn)乞榨。
User.java
package com.bit.whdalive.demomvp.bean;
public class User {
private String username;
private String password;
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
}
IUserModel.java
package com.bit.whdalive.demomvp.mvp;
public interface IUserModel {
void login(String username,String password,OnLoginListener listener);
//回調(diào)接口,我放到IUserModel中当娱,實際上也可以單獨抽離出來吃既,或者放到 presenter 的接口中都是可以的,畢竟這個demo功能太單一了
public interface OnLoginListener{
void loginSuccess();
void loginFailed();
}
}
IUserModelImpl.java
package com.bit.whdalive.demomvp.mvp;
public class UserModelImpl implements IUserModel{
private IUserLoginPresenter mIUserLoginPresenter;
public UserModelImpl(IUserLoginPresenter IUserLoginPresenter) {
mIUserLoginPresenter = IUserLoginPresenter;
}
@Override
public void login(final String username, final String password,final IUserModel.OnLoginListener listener) {
new Thread(){
@Override
public void run() {
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
if("whdalive".equals(username)&&"123...".equals(password)){
User user = new User();
user.setUsername(username);
user.setPassword(password);
listener.loginSuccess();
}else{
listener.loginFailed();
}
}
}.start();
}
}
依舊是 面向接口編程 的思想跨细,將 Model 層的方法抽離成一個接口鹦倚,日后彼此交互也都是通過傳遞接口類型的引用,避免強耦合冀惭。
View
我們考慮一下 View 中應(yīng)該有哪些功能震叙,首先效果圖中有兩個按鈕愤诱,login 和 clear。
想要實現(xiàn) login 捐友,就需要能夠提供我們輸入的文本,對應(yīng)如下方法:
String getUserName();
String getPassword();
實現(xiàn) clear溃槐,那么意味著 View 需要有清除 輸入文本 的功能匣砖,也就是需要如下兩個方法:
void clearUserName();
void clearPassword();
同時,我們看到登錄時有個 Progressbar 來提示登錄過程(畢竟實際上這是個耗時的過程)昏滴,那么就需要能夠顯示和隱藏它:
void showLoading();
void hideLoading();
最后猴鲫,無論我們登錄成功與否,都需要有個提示顯示我們是否登錄成功了:
void toMainActivity();
void showFailedError();
綜上谣殊,完整的接口定義為:
IUserLoginView
package com.bit.whdalive.demomvp.mvp;
public interface IUserLoginView {
String getUserName();
String getPassword();
void clearUserName();
void clearPassword();
void showLoading();
void hideLoading();
void toMainActivity();
void showFailedError();
}
接下來就是寫它的實現(xiàn)類了(實際上就是個純碎的Activity)
UserLoginActivity
package com.bit.whdalive.demomvp.mvp;
public class UserLoginActivity extends AppCompatActivity implements IUserLoginView {
private EditText mEdtUsername,mEdtPwd;
private Button mBtnLogin,mBtnClear;
private ProgressBar mPbLoading;
private IUserLoginPresenter mIUserLoginPresenter;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
initViews();
}
private void initViews(){
mIUserLoginPresenter = new UserLoginPresenterImpl(this);
mEdtUsername = findViewById(R.id.input_account);
mEdtPwd = findViewById(R.id.input_password);
mBtnClear = findViewById(R.id.btn_clear);
mBtnLogin = findViewById(R.id.btn_login);
mPbLoading = findViewById(R.id.pb_loading);
mBtnLogin.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
mIUserLoginPresenter.doLogin();
}
});
mBtnClear.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
mIUserLoginPresenter.clear();
}
});
}
@Override
public String getUserName() {
return mEdtUsername.getText().toString();
}
@Override
public String getPassword() {
return mEdtPwd.getText().toString();
}
@Override
public void clearUserName() {
mEdtUsername.setText("");
}
@Override
public void clearPassword() {
mEdtPwd.setText("");
}
@Override
public void showLoading() {
mPbLoading.setVisibility(View.VISIBLE);
}
@Override
public void hideLoading() {
mPbLoading.setVisibility(View.GONE);
}
@Override
public void toMainActivity() {
Toast.makeText(this,"login success, to MainActivity",Toast.LENGTH_SHORT).show();
}
@Override
public void showFailedError() {
Toast.makeText(this,"Login failed",Toast.LENGTH_SHORT).show();
}
}
對于 View 而言拂共,因為其只和 用戶的交互 打交道,因此我們只需要考慮好 哪些操作需要改動界面顯示姻几?哪些操作需要什么反饋宜狐? 并以此來編寫對應(yīng)方法并抽象成接口,而一旦寫好了接口蛇捌,那么實現(xiàn)類就是手到擒來的事情了抚恒。
Presenter
Presenter 作為 Model 和 View 的橋梁,需要能夠調(diào)用 Model 的方法络拌,來執(zhí)行具體的業(yè)務(wù)方法俭驮;同時需要調(diào)用 View 的方法,來更新界面春贸。
在這個 Demo 中混萝,就只有兩個功能可言:doLogin 和 clear。
對于 doLogin 實際就是調(diào)用了 Model 中的 login方法萍恕,clear 則是調(diào)用了 View 中的 clearUserName() 和 clearPassword() 來清除文本逸嘀。
IUserLoginPresenter.java
package com.bit.whdalive.demomvp.mvp;
public interface IUserLoginPresenter {
void doLogin();
void clear();
}
UserLoginPresenterImpl.java
package com.bit.whdalive.demomvp.mvp;
import android.os.Handler;
public class UserLoginPresenterImpl implements IUserLoginPresenter,IUserModel.OnLoginListener {
private IUserLoginView mIUserLoginView;
private IUserModel mIUserModel;
private Handler mHandler = new Handler();
public UserLoginPresenterImpl(IUserLoginView IUserLoginView) {
mIUserLoginView = IUserLoginView;
mIUserModel = new UserModelImpl(this);
}
@Override
public void doLogin() {
String username = mIUserLoginView.getUserName();
String password = mIUserLoginView.getPassword();
mIUserLoginView.showLoading();
mIUserModel.login(username,password,this);
}
@Override
public void loginSuccess() {
mHandler.post(new Runnable() {
@Override
public void run() {
mIUserLoginView.hideLoading();
mIUserLoginView.toMainActivity();
}
});
}
@Override
public void loginFailed() {
mHandler.post(new Runnable() {
@Override
public void run() {
mIUserLoginView.hideLoading();
mIUserLoginView.showFailedError();
}
});
}
@Override
public void clear() {
mIUserLoginView.clearUserName();
mIUserLoginView.clearPassword();
}
}
上述代碼中,由于 Model 需要通知 Presenter 是否登陸成功允粤,因此 presenter 實現(xiàn)了 IUserModel.OnLoginListener 接口厘熟。
同時由于Presenter 分別和 Model、View 雙向通信维哈,因此 Presenter 持有后兩者的引用绳姨,而 Model和View彼此不持有對方的引用,都只有 Presenter 的引用阔挠。
契約類寫法
當(dāng)然如果讀者偏愛于 契約類Contracts 的寫法飘庄,問題也不大:
LoginContract.java
package com.bit.whdalive.demomvp.mvp_contracts;
import com.bit.whdalive.demomvp.bean.User;
public interface LoginContract {
public interface View {
String getUserName();
String getPassword();
void clearUserName();
void clearPassword();
void showLoading();
void hideLoading();
void toMainActivity();
void showFailedError();
}
public interface Presenter {
void login();
void clear();
}
}
之后,在實現(xiàn)類中對實現(xiàn)接口的名字從 IUserLoginPresenter/IUserLoginView 改為 LoginContract.Presenter/LoginContract.View 即可购撼。
小結(jié)
最后以 Login 登錄功能捋順一下該demo的執(zhí)行流程:
- 用戶輸入賬號密碼
- 點擊Login按鈕跪削,View 將該事件傳遞給 Presenter
- Presenter 接收到 login 請求谴仙,從 View 中提取 賬號密碼文本,并一并交給 Model 執(zhí)行具體的 login 操作(也就是調(diào)用 Model 的login()方法)
- Model 執(zhí)行 login 操作后碾盐,通知 Presenter 是否登錄成功
- Presenter 接收到 Model 的反饋晃跺,通知 View 更新頁面
- View 根據(jù) Presenter 的指令,更改當(dāng)前頁面毫玖。
上面我們就通過一步步拆解掀虎,逐步講解如何手?jǐn)]一個簡易的Demo,讀者也可以自己找一些簡單的場景加以練習(xí)付枫。
附上 Demo 地址:
最后烹玉,愿本文對大家有所幫助。