前言
MVP的相關文章在剛哥、郭神的公眾號看過挺多的,對于一個剛開始學MVP的初學者來說,還是一臉懵逼,要自己獨立去寫一個有一種無從下手的感覺。早段時間在簡書上看到一篇轉載的文章,里面有一段寫的很好:MVP 把 Activity 中的 UI 邏輯抽象成 View 接口,把業務邏輯抽象成 Presenter 接口,Model 類還是原來的 Model(博客地址)。文章所講述內容是對一個基礎MVP的實現,并沒有通過各種基類來達到解耦封裝。
MVC和MVP
全名Model View Contorller,用于隔離業務邏輯、數據、界面展示,在更改ui界面時不需要對業務邏輯進行代碼變更。
優點:便于理解,開發快速,一定程度隔離業務與ui的耦合度。
缺點:業務復雜有可能導致controller爆炸,不利于維護。
MVP全名Mode View Presenter,Presenter處理邏輯業務,Model提供數據,View更新展示界面。完全隔離界面顯示與業務邏輯,不論更改界面或者更改業務邏輯均為單方面更改。
優點:完全隔離業務邏輯與ui顯示,便與迭代維護測試。
缺點:開發效率低下(時間上),接口類過多。
接口
基礎的MVP開發需要編寫一定的接口,View層和Model層越復雜,接口類的定義就越多,因此,對接口的理解和運用對MVP的理解很重要。
接口是對行為的抽象,簡單地理解就是這個對象會做什么,就是所說的方法,從某種意義上理解,可以把接口看作是包含某一類對象的行為的一個集合,比如動物“吃”的行為,雞和貓是不一樣的。另外,接口有一個很重要的特性,它不能實例化,只能通過它的實現類來實例化對象,我們可以使用接口來聲明一個變量,但只能由實現類來初始化變量,將它實例化,因此,接口可以實現回調的功能。
那么,我們在View層(Model層)定義好接口,在Presenter層使用接口聲明變量,在它的構造方法中,通過其實現類來初始化接口變量,至于實現類如何實現,在哪實現就不是Presenter層所關心的,它只要拿到View層和Model層的引用并且處理好它們之間的業務邏輯就可以了。
對于Model層,我們可以只把實現的方法暴露出去,其它的實現細節可以隱藏掉,體現了Java的封裝性。
實現過程
先看下這個模塊(一個小作品中的一個模塊)的項目結構,它是一個用戶登錄的場景:
接口
ILoginView
負責彈出Toast
,以及更新UI,比如登錄密碼錯誤的時候。接口IUserLogin
負責登錄,而接口IUserLoginListener
負責監聽登錄狀態,成功還是失敗。LoginPresenter
負責處理好View層和Model層的業務邏輯。LoginActivity
實現了ILoginView
,它只需做一些變量的初始化,調用api,更新UI(在Activity下很容易實現)的工作就可以了,代碼很簡潔。UserLoginModel
實現了IUserLogin
,負責處理真正的登錄業務。
那么,下面就直接上代碼了,先看Model層的:
// 接口-IUserLogin
public interface IUserLogin {
void login(String account, String password);
}
// 接口-IUserLoginListener
public interface IUserLoginListener {
void success();
void failed();
// 請求登錄過程中出現的錯誤,如密碼錯誤
void error(int type);
}
// 實現類-UserLoginModel
public class UserLoginModel implements IUserLogin {
private IUserLoginListener loginListener;
private MyHandler handler;
public UserLoginModel(IUserLoginListener loginListener) {
this.loginListener = loginListener;
handler = new MyHandler();
}
@Override
public void login(String account, String password) {
if (account.length() == 0 || password.length() == 0) {
loginListener.error(Constant.TYPE_ACCOUNT_PASSWORD_ERROR_NULL);
return;
}
OkHttpClient client = new OkHttpClient();
FormBody.Builder formBody = new FormBody.Builder();
formBody.add("account", account);
formBody.add("password", password);
Request request = new Request.Builder()
.url(Constant.RESOURCE_URL + "LoginServlet")
.post(formBody.build())
.build();
client.newCall(request).enqueue(new LoginCallBack());
}
private class LoginCallBack implements Callback {
@Override
public void onFailure(@NotNull Call call, @NotNull IOException e) {
loginListener.failed();
e.printStackTrace();
}
@Override
public void onResponse(@NotNull Call call, @NotNull Response response) throws IOException {
if (response.isSuccessful()) {
String result = response.body().string();
Log.d(Constant.TAG, "result:" + result);
// 不能再子線程更新UI
Message message = handler.obtainMessage();
message.obj = result;
handler.sendMessage(message);
}
}
}
private class MyHandler extends Handler {
@Override
public void handleMessage(Message msg) {
String result = (String) msg.obj;
if ("success".equals(result)) {
loginListener.success();
} else if ("not_exist".equals(result)) {
loginListener.error(Constant.TYPE_ACCOUNT_NOT_EXIST);
} else if ("error_password".equals(result)) {
loginListener.error(Constant.TYPE_PASSWORD_ERROR);
}
}
}
}
接著是View層:
// 接口-ILoginView
public interface ILoginView {
void loginSuccess();
void loginFailed();
// 賬號出現錯誤
void updateAccountView(String info);
// 密碼出現錯誤
void updatePasswordView(String info);
// 根據不同的類型清空輸入框
void clear(int type);
// 信息驗證不通過時
void checkout();
void updateView();
// 銷毀LoginActivity
void destroyActivity();
}
// LoginActivity
public class LoginActivity extends AppCompatActivity implements View.OnClickListener, ILoginView {
private TextInputLayout tilAccount;
private TextInputLayout tilPassword;
private TextInputEditText tieAccount;
private TextInputEditText tiePassword;
private MaterialButton btnLogin;
private MaterialButton btnRegisterActivity;
private LoginPresenter presenter;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_login);
presenter = new LoginPresenter(this, this);
String a = RecordInfoState.getAccount(this);
if (!"account".equals(a) && a != null) {
startActivity(new Intent(this, MainActivity.class));
finish();
}
init();
}
private void init() {
tilAccount = findViewById(R.id.til_account);
tilPassword = findViewById(R.id.til_password);
tieAccount = findViewById(R.id.tie_account);
tiePassword = findViewById(R.id.tie_password);
btnLogin = findViewById(R.id.btn_login);
btnRegisterActivity = findViewById(R.id.btn_register_activity);
btnLogin.setOnClickListener(this);
btnRegisterActivity.setOnClickListener(this);
}
@Override
public void onClick(View v) {
switch (v.getId()) {
case R.id.btn_login:
String account = tieAccount.getText().toString();
String password = tiePassword.getText().toString();
presenter.doLogin(account, password);
break;
case R.id.btn_register_activity:
startActivity(new Intent(LoginActivity.this, RegisterActivity.class));
finish();
break;
default:
break;
}
}
@Override
public void loginSuccess() {
Toast.makeText(LoginActivity.this, "登錄成功", Toast.LENGTH_SHORT).show();
}
@Override
public void loginFailed() {
Toast.makeText(LoginActivity.this, "登錄失敗", Toast.LENGTH_SHORT).show();
}
@Override
public void updateAccountView(String info) {
tilAccount.setErrorEnabled(true);
tilAccount.setError(info);
}
@Override
public void updatePasswordView(String info) {
tilPassword.setErrorEnabled(true);
tilPassword.setError(info);
}
@Override
public void clear(int type) {
switch (type) {
case Constant.TYPE_ACCOUNT_NOT_EXIST:
tieAccount.setText("");
break;
case Constant.TYPE_PASSWORD_ERROR:
tiePassword.setText("");
break;
default:
break;
}
}
@Override
public void checkout() {
Toast.makeText(LoginActivity.this, "賬號或密碼不能為空", Toast.LENGTH_SHORT).show();
}
@Override
public void updateView() {
tilAccount.setErrorEnabled(false);
tilPassword.setErrorEnabled(false);
}
@Override
public void destroyActivity() {
this.finish();
}
}
最后時Presenter層的:
// LoginPresenter
public class LoginPresenter {
private ILoginView loginView;
private IUserLogin loginModel;
private Context context;
private String accout;
public LoginPresenter(Context context, ILoginView loginView) {
this.context = context;
this.loginView = loginView;
loginModel = new UserLoginModel(new UserLoginListerImpl());
}
public void doLogin(String account, String password) {
this.accout = account;
loginView.updateView();
loginModel.login(account, password);
}
private class UserLoginListerImpl implements IUserLoginListener {
@Override
public void success() {
RecordInfoState.logined(context, accout);
loginView.loginSuccess();
initPunchCard();
RecordInfoState.recordPlan(context, "4");
context.startActivity(new Intent(context, MainActivity.class));
loginView.destroyActivity();
}
// 將保存在遠程數據庫里的打卡日期緩存到本地
public void initPunchCard() {
OkHttpClient client = new OkHttpClient();
FormBody.Builder body = new FormBody.Builder();
body.add("type", "4");
body.add("name", RecordInfoState.getAccount(context));
Request request = new Request.Builder()
.url(Constant.RESOURCE_URL + "ResPathServlet")
.post(body.build())
.build();
client.newCall(request).enqueue(new Callback() {
@Override
public void onFailure(@NotNull Call call, @NotNull IOException e) {
}
@Override
public void onResponse(@NotNull Call call, @NotNull Response response) throws IOException {
if (response.isSuccessful()) {
String pathJson = response.body().string();
try {
JSONArray jsonArray = new JSONArray(pathJson);
for (int i = 0; i < jsonArray.length(); i++) {
JSONObject jsonObject = jsonArray.getJSONObject(i);
if (jsonObject != null) {
RecordInfoState.recordPunchCard(context, jsonObject.getString("date"));
}
}
} catch (JSONException e) {
e.printStackTrace();
}
}
}
});
}
@Override
public void failed() {
loginView.loginFailed();
}
@Override
public void error(int type) {
switch (type) {
case Constant.TYPE_ACCOUNT_PASSWORD_ERROR_NULL:
loginView.checkout();
break;
case Constant.TYPE_ACCOUNT_NOT_EXIST:
loginView.updateAccountView("賬號不存在!");
loginView.clear(Constant.TYPE_ACCOUNT_NOT_EXIST);
break;
case Constant.TYPE_PASSWORD_ERROR:
loginView.updatePasswordView("密碼錯誤!");
loginView.clear(Constant.TYPE_PASSWORD_ERROR);
break;
default:
break;
}
}
}
}
總結
一個簡單的MVP模式就實現完了,筆者認為對接口的理解和運用時理解MVP的關鍵。通過接口的方式將定義與實現分開,實現View層與Model層的解耦。還有要明確不同層之間的工作:
- View:對應于Activity和XML(在一些特殊的場景下,dialog,fragment也可以充當View),負責View的繪制以及與用戶的交互。
- Model:依然是實體模型。
- Presenter:負責完成View與Model間的交互和業務邏輯。
所以,View做的工作Model不關心,Model做的工作View也不關心,Presenter充當了工作類似現實生活中的中介工作,它使得View與Model可以愉快地溝通。
你只要從View層與Model層需要解耦這個切入點去思考,來不斷解決心中的疑問,比如為什么要通過定義接口來實現?別人為什么這樣寫?如果業務和UI更復雜了應該怎么寫等等。不管思考的過程怎樣(不要一直死磕),但一定要有自己的總結、反思。積累夠了,自然而然就要一種恍然大悟的感覺。