android實踐的一些思考:實現一個MVP架構設計
MVP是從MVC架構演變而來的,目的是為了使得程式開發高內聚低耦合,易於擴充套件,方便維護。
MVC中的M指的是model模型, V 指的是View檢視,C指的是Controller。 MVC 大體的思想是,控制器控制著模型與檢視的同步。當模型發生了變動,通知控制器然後控制器通知檢視同步。比如我們瀏覽一個動態網頁,以JSP為例: 使用者請求路徑--> 得到一個檢視V,在檢視上修改某個javaBean,即修改模型,此時它會交給servlet控制器C,然後控制器通過後臺的業務邏輯程式碼完成模型M的同步。 修改完模型後,需要回到servlet控制器C中,判斷是否需要將模型封裝到檢視解析器中,通過jstl標籤編譯成網頁檢視V返回給使用者。 總之,它的特點就是單向通訊。 V->C->M; M->C->V。
MVP中,M指的是model模型,V指的是View檢視,P指的是Presenter(中介者)。 它的面向物件的思想更強。 具體的理解我將結合自己的練習來進行學習理解。
還有一種16年左右紅起來的MVVM的架構,現在主流的三大前端框架,angular,react,vue就是基於這個。 其中VM指的是ViewModel,檢視模型。 它的最強大的一個地方是將同步的任務交給程式碼完成,刪減了很多的冗餘程式碼,提高開發效率。 貌似叫做響應式開發。 angular的雙向繫結,確是一種非常強大理想的特性。
我的想法出發點是這樣的:
第一,每個Activity中需要寫的內容分為三部分, 一部分是佈局檔案xml,一部分是針對佈局檔案的業務邏輯程式碼,另一部分是二者的同步! 要完成這三個基本的邏輯功能,通常我們需要寫很多的程式碼在activity中。 這樣會帶來一些問題,如,邏輯不清晰,維護困難,看到程式碼頹喪消極。 因為,稍微一個有些功能的介面,都至少是一百好幾行程式碼。 當這些程式碼乘以寫的頁面數,改起來要老命了!
第二,每一個同步的程式碼需要寫好幾步: 第一要確定每個檢視id與所對應的成員變數名,那麼這裡就存在一個命名的問題(而,實際上這個命名除了起個標誌位的作用,沒其他任何作用,我們要還要儘量做到見名知意)。 第二,要寫很多無聊的findById進行繫結。第三,為了完成單向取值,通常需要一個一個從檢視元件呼叫相應的方法取得自己想要的屬性。 冗餘的程式碼量很多。 體驗很差。 通過開源注入框架ButterKnife可以很大程度上解決這個問題。
第三,雖說使用了注入框架,程式碼看起來清爽了很多。 可是為了完成檢視與模型的同步,我們還是不得不定義相關的成員變數進行繫結。 有沒有一種方法,可以完成類似於angular的雙向繫結呢?
基於MVP的抽象層設計:
View頂層抽象:
package com.automannn.meimeijiong.activity.view.api.baseApi;
public interface IView {
}
Model 頂層抽象:
package com.automannn.meimeijiong.model.api.base;
public interface IModel {
}
Presenter 頂層抽象:
package com.automannn.meimeijiong.presenter.api;
import com.automannn.meimeijiong.model.api.base.IModel;
import com.automannn.meimeijiong.activity.view.api.baseApi.IView;
public abstract class BasePresenter<M extends IModel,V extends IView>{
protected M currModel;
protected V currView;
public BasePresenter(V view){
this.currView = view;
}
public abstract void init();
}
在這裡可以看到,每個中介者持有模型與檢視的引用。 它的目的是為了更好的發揮他協調者的作用。
View的第二層抽象:
package com.automannn.meimeijiong.activity.api;
import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;
import com.automannn.meimeijiong.presenter.api.BasePresenter;
public abstract class BaseActivity<T extends BasePresenter> extends AppCompatActivity {
protected T currentPresenter;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(getLayout());
initView();
currentPresenter = initPresent();
onPrepare();
}
protected abstract void onPrepare();
protected abstract T initPresent();
protected abstract int getLayout();
protected abstract void initView();
}
Presenter的第二層抽象:
使用示例:
package com.automannn.meimeijiong.presenter;
import com.automannn.meimeijiong.model.ChatModel;
import com.automannn.meimeijiong.presenter.api.BasePresenter;
public class ChatPresenter extends BasePresenter<ChatModel,ChatActivity> {
public ChatPresenter(ChatActivity view) {
super(view);
currModel = new ChatModel();
}
@Override
public void init() {
//todo,do something
}
}
從這裡可以看到,V與P二者是雙向持有對方的關聯的。 這就難免導致一些邏輯上的問題(比如到底以presenter為主還是以activity為主,方法的呼叫的邏輯應該怎麼佈局?)。我是這樣解決的:
整個app入口在manifest檔案,而我們直接互動的介面在activity中。 所以說activity可以近似看成人機介面。 一個activity發生變化應該有三種方式: 第一,activity本身具有自身的生命週期方法; 第二,activity作為檢視的載體,具有反饋事件回撥的功能;第三,通過looper機制,結合hander可以完成一些其它的功能。hander的方式由於出現了很多的開源框架,因此不推薦使用,但是理解它是很有必要的。 因此,在整個佈局的時候,將只存在兩部分,以生命週期線索組織起來的程式碼邏輯;和 以事件回撥線索組織起來的程式碼邏輯。
也就是說,每一個功能性方法的入口都在activity,(檢視)中,但是功能的實現邏輯則分佈在presenter,P中。 因為它是資源的持有者,即持有V,也持有M。
以上討論的是解決V與P的關係。 但是我們的根本目的是為了解決M與V的關係。 為了完成二者同步,我們可以很自然的想到基於檢視介面卡的ListView,ListAdapter。 因為它可以不用頻繁切換場景,直接定義好檢視繫結規則,即可完成很多的操作。 但是針對單頁的適配(或者說普通的檢視適配)並沒有相關解決方案,但是通過類比介面卡的原始碼,我們可以抽象出一個檢視持有者,基於它來完成類似介面卡的功能。
ViewHoler抽象:
package com.automannn.meimeijiong.adapter;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.Drawable;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageView;
import android.widget.TextView;
public class ViewHolder {
//代表檢視 dto的根部局
private View currView;
public ViewHolder(View view){
this.currView = view;
currView.setTag(this);
}
public <T extends View> T getView(int id){
T t = (T) currView.findViewById(id);
return t;
}
public View getRootView() {
return currView;
}
/**
* 設定文字
*/
public ViewHolder setText(int id, CharSequence text) {
View view = getView(id);
if (view instanceof TextView) {
((TextView) view).setText(text);
}
return this;
}
/**
* 設定圖片
*/
public ViewHolder setImageResource(int id, int drawableRes) {
View view = getView(id);
if (view instanceof ImageView) {
((ImageView) view).setImageResource(drawableRes);
} else {
view.setBackgroundResource(drawableRes);
}
return this;
}
public ViewHolder setImageResourceFromBitmap(int id, Bitmap bitmap) {
View view = getView(id);
if (view instanceof ImageView) {
((ImageView) view).setImageBitmap(bitmap);
} else {
Drawable drawable = new BitmapDrawable(bitmap);
view.setBackground(drawable);
}
return this;
}
/**
* 設定可見
*/
public ViewHolder setVisibility(int id, int visible) {
getView(id).setVisibility(visible);
return this;
}
/**
* 設定標籤
*/
public ViewHolder setTag(int id, Object obj) {
getView(id).setTag(obj);
return this;
}
public void invalidate(){
this.currView.invalidate();
}
}
它的來源是,將當前Activity的整體充當一個View的根部局,然後建立一個引用,與之相關的所有的同步操作均由此類完成。通過該抽象我們可以完成類似雙向繫結的功能,只不是不是響應式的,需要我們手動控制。 這樣就會減少很多因檢視同步所需要的寫的冗餘程式碼。
下面舉個完整例子展示:
佈局:
<?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=".MainActivity">
<EditText
android:id="@+id/user_nickname"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="暱稱"/>
<EditText
android:id="@+id/user_realname"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="真實姓名"/>
<EditText
android:id="@+id/user_age"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="number"
android:hint="年齡"/>
<EditText
android:id="@+id/user_sex"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="性別"/>
<EditText
android:id="@+id/user_email"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="郵箱"/>
<Button
android:id="@+id/user_submit"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="提交"/>
</LinearLayout>
activity檢視:
package automannn.com.testmvp;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.view.View;
import android.widget.Button;
import android.widget.Toast;
import automannn.com.testmvp.presenter.UserPresenter;
import automannn.com.testmvp.view.api.ISingleModelView;
import automannn.com.testmvp.view.api.IView;
public class MainActivity extends BaseActivity<UserPresenter> implements ISingleModelView {
private Button button;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
}
@Override
protected void onPrepare() {
currentPresenter.init();
}
@Override
protected UserPresenter initPresent() {
return new UserPresenter(this);
}
@Override
protected int getLayout() {
return R.layout.activity_main;
}
@Override
protected void initView() {
button =findViewById(R.id.user_submit);
button.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
currentPresenter.submit();
}
});
}
@Override
public View getRootView() {
return getWindow().getDecorView();
}
public void showToast(String msg){
Toast.makeText(this,msg,Toast.LENGTH_SHORT).show();
}
}
model:
package automannn.com.testmvp.model;
import android.view.View;
import android.widget.Adapter;
import android.widget.TextView;
import automannn.com.testmvp.R;
import automannn.com.testmvp.adapter.ViewHolder;
import automannn.com.testmvp.entity.User;
import automannn.com.testmvp.model.api.ISingleModel;
public class UserModel implements ISingleModel<User> {
private User data;
private ModelViewHolder modelViewHolder;
@Override
public User getdata() {
return data;
}
@Override
public Adapter getAdapter() {
return null;
}
@Override
public void init() {
}
@Override
public void setCurrentData(User o) {
data =o;
}
@Override
public void myNotifyDataSetChanged() {
modelViewHolder.getViewHolder().invalidate();
}
@Override
public void initModelViewHolder(View rootView) {
modelViewHolder = new ModelViewHolder(rootView);
}
@Override
public void M2VbindModelViewHolder() {
ViewHolder viewHolder = modelViewHolder.getViewHolder();
viewHolder.setText(R.id.user_nickname,data.getNickName());
viewHolder.setText(R.id.user_realname,data.getRealName());
viewHolder.setText(R.id.user_sex,data.getSex());
viewHolder.setText(R.id.user_age,data.getAge()==null?"":data.getAge()+"");
viewHolder.setText(R.id.user_email,data.getEmail());
}
@Override
public void V2MbindModelViewHolder() {
ViewHolder viewHolder = modelViewHolder.getViewHolder();
data.setNickName(((TextView)viewHolder.getView(R.id.user_nickname)).getText().toString());
data.setRealName(((TextView)viewHolder.getView(R.id.user_realname)).getText().toString());
data.setSex(((TextView)viewHolder.getView(R.id.user_sex)).getText().toString());
data.setAge(Integer.valueOf(((TextView)viewHolder.getView(R.id.user_age)).getText().toString()));
data.setEmail(((TextView)viewHolder.getView(R.id.user_email)).getText().toString());
}
}
presenter:
package automannn.com.testmvp.presenter;
import automannn.com.testmvp.MainActivity;
import automannn.com.testmvp.entity.User;
import automannn.com.testmvp.model.UserModel;
public class UserPresenter extends BasePresenter<UserModel,MainActivity> {
public UserPresenter(MainActivity view) {
super(view);
currModel= new UserModel();
}
//生命週期的核心業務邏輯在此處書完成
@Override
public void init() {
//設定資料來源(注意,該資料來源有可能來自任意的位置)
currModel.setCurrentData(new User());
//初始化
currModel.initModelViewHolder(currView.getRootView());
}
public void submit() {
currModel.V2MbindModelViewHolder();
currView.showToast(currModel.getdata().toString());
//新建一個模型,模擬用於同步檢視
currModel.setCurrentData(new User());
currModel.M2VbindModelViewHolder();
}
}
------------------------------------------------
在單頁的狀態下,model充當了檢視模型的作用,可以類似完成雙向繫結的效果,所有的操作可以面向資料物件,而不是面向檢視元件。 在一定程度上減少了相當的冗餘程式碼。 同時,對於後期的修改升級與維護,邏輯還算清晰。
對於具有集合資料的頁面,大體上差不多。 只是相應的資料來源會發生一些改變。 集合資料的頁面最終還是會以跳轉到單頁頁面為結果,此時單頁頁面的資料來源就是從intent中接收,而非重新例項化。
demo在github的位置:這裡 (在androidManifest.xml中將入口改成User對應的activity就可以了。)
為了防止由於時間的流逝而使得自己曾經的思考付諸流水,特此記錄!