1. 程式人生 > >android實踐的一些思考:實現一個MVP架構設計

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就可以了。)

      為了防止由於時間的流逝而使得自己曾經的思考付諸流水,特此記錄!