1. 程式人生 > >細說Android框架設計三劍客MVC、MVP和MVVM

細說Android框架設計三劍客MVC、MVP和MVVM

    最近幾年的移動端開發越來越火,功能越來越強大,處理業務越來越複雜,因此對系統擴充套件性的要求越來越高。而為了更好地進行移動端架構設計,我們最常用的就是MVC和MVP,今天本篇部落格就和大家一起聊一聊這兩種框架設計。

MVC框架

MVC的定義

    MVC (Model-View-Controller):M是指邏輯模型,V是指檢視模型,C則是控制器。使用MVC的目的是將M和V的實現程式碼分離,從而使同一個程式可以使用不同的表現形式,而C存在的目的則是確保M和V的同步,一旦M改變,V應該同步更新,這與《設計模式》中的觀察者模式是完全一樣。

為何用MVC

  • 從使用者的角度出發,使用者可以根據自己的需求,選擇自己合適的瀏覽資料的方式。
  • 從開發者的角度,MVC把應用程式的邏輯層與介面是完全分開的,這樣,介面設計人員可以直接參與到介面開發,程式設計師就可以把精力放在邏輯層上。而不是像以前那樣,設計人員把所有的材料交給開發人員,由開發人員來實現介面。

MVC的通訊方式

這裡寫圖片描述

首先,View傳送命令到Controller,然後Controller處理完業務邏輯後讓Model改變狀態,最後由Model將新的資料傳送到View,使用者得到資料響應。

Android中的MVC

    1.檢視層(View):一般採用XML檔案進行介面的描述,使用的時候可以非常方便的引入。
    2.控制層(Controller):Android的控制層通常是在Acitvity中實現。
    3.模型層(Model):對資料庫的操作、對網路等的操作都應該在Model裡面處理,當然對業務計算等操作也是必須放在的該層的。

MVP框架

MVP的定義

    MVP (Model-View-Presenter):MVP其實是由MVC演變而來的,其中的M依然是指邏輯模型,V依然是指檢視模型,而P(中間橋樑)則代替了C成為了邏輯控制器的角色。

MVC和MVP到底有啥區別

    區別就在於MVP中View並不直接使用Model,它們之間的通訊是通過Presenter (MVC中的Controller)來進行的,所有的互動都發生在Presenter內部,而在MVC中View會直接從Model中讀取資料而不是通過 Controller。我們知道在MVC裡,View是可以直接訪問Model的。從而,View裡會包含Model資訊,不可避免的還要包括一些業務邏輯。 在MVC模型裡,更關注的Model的不變,而同時有多個對Model的不同顯示,即View。所以,在MVC模型裡,Model不依賴於View,但是View是依賴於Model的。

MVP的通訊方式

這裡寫圖片描述

    首先MVP各部分之間的通訊都是雙向的,但是唯獨View與Model之間是不發生聯絡的,二者之間的通訊都是通過Presenter傳遞的。在MVP裡,應用程式的邏輯主要在Presenter來實現,其中的View是很薄的一層。在這個過程中,View是很簡單的,能夠把資訊顯示清楚就可以了。在後面,根據需要再隨便更改View,而對Presenter沒有任何的影響了。 如果要實現的UI比較複雜,而且相關的顯示邏輯還跟Model有關係,就可以在View和Presenter之間放置一個Adapter。由這個 Adapter來訪問Model和View,避免兩者之間的關聯。而同時,因為Adapter實現了View的介面,從而可以保證與Presenter之間介面的不變。這樣就可以保證View和Presenter之間介面的簡潔,又不失去UI的靈活性。 在MVP模式裡,View只應該有簡單的Set/Get的方法,使用者輸入和設定介面顯示的內容,除此就不應該有更多的內容,絕不容許直接訪問Model–這就是與MVC很大的不同之處。

MVP的優缺點

1、模型與檢視完全分離,我們可以修改檢視而不影響模型
2、可以更高效地使用模型,因為所有的互動都發生在一個地方——Presenter內部
3、我們可以將一個Presenter用於多個檢視,而不需要改變Presenter的邏輯。這個特性非常的有用,因為檢視的變化總是比模型的變化頻繁。
4、如果我們把邏輯放在Presenter中,那麼我們就可以脫離使用者介面來測試這些邏輯(單元測試)

由於對檢視的渲染放在了Presenter中,所以檢視和Presenter的互動會過於頻繁。還有一點需要明白,如果Presenter過多地渲染了檢視,往往會使得它與特定的檢視的聯絡過於緊密。一旦檢視需要變更,那麼Presenter也需要變更了。

MVVM框架

MVVM的定義

    MVVM(Model-View-ViewModel):MVVM和MVP的區別其實不大,只不過是把presenter層換成了ViewModel層,再有就是View層和ViewModel層是相互繫結的關係,當我們更新ViewModel層的資料的時候,View層會相應的更新UI。

MVVM的通訊方式

這裡寫圖片描述
    MVVM它採用的是資料繫結(data-binding)方式,而且是雙向繫結:View繫結到ViewModel,然後執行一些命令在向它請求一個動作。而反過來,ViewModel跟Model通訊,告訴它更新來響應UI。

MVVM優點

以下內容來自百度百科
MVVM模式和MVC模式一樣,主要目的是分離檢視(View)和模型(Model),有幾大優點

  1. 低耦合。檢視(View)可以獨立於Model變化和修改,一個ViewModel可以繫結到不同的”View”上,當View變化的時候Model可以不變,當Model變化的時候View也可以不變。
  2. 可重用性。你可以把一些檢視邏輯放在一個ViewModel裡面,讓很多view重用這段檢視邏輯。
  3. 獨立開發。開發人員可以專注於業務邏輯和資料的開發(ViewModel),設計人員可以專注於頁面設計。
  4. 可測試。介面素來是比較難於測試的,而現在測試可以針對ViewModel來寫。

MVP案例實踐

    下面我選取MVP框架作為主要案例,給大家講解如何將一個普通專案改造成MVP框架模式的專案,幫助大家理解MVP框架模式的意義。

傳統專案結構

這裡寫圖片描述

該專案中我們定義了一個學生資訊列表,用來顯示圖片和文字;專案原始碼如下:

MainActivity.java

/**
 * 功能:MainActivity 用列表形式實現
 * 作者:猴子搬來的救兵
 * 部落格地址:http://blog.csdn.net/mynameishuangshuai
 * 日期:2016/10/01
 */
public class MainActivity extends AppCompatActivity implements IStudentView{

    ListView myList;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        myList = (ListView) findViewById(R.id.my_list);
        myList.setAdapter(new StudentAdapter(MainActivity.this));
    }
}

StudentAdapter.java

/**
 * 功能:StudentAdapter
 * 作者:猴子搬來的救兵
 * 部落格地址:http://blog.csdn.net/mynameishuangshuai
 * 日期:2016/10/01
 */
public class StudentAdapter extends BaseAdapter {

    private LayoutInflater myInflater;
    private List<Student> data;

    public StudentAdapter(Context context, List<Student> data) {
        myInflater = LayoutInflater.from(context);
        this.data = data;
    }

    @Override
    public int getCount() {
        return DataUtils.stuSize();
    }

    @Override
    public Object getItem(int position) {
        return DataUtils.get(position);
    }

    @Override
    public long getItemId(int position) {
        return position;
    }

    @Override
    public View getView(int position, View convertView, ViewGroup parent) {
        View stuView = myInflater.inflate(R.layout.stu_item, null);
        Student student = DataUtils.get(position);
        ImageView imgStu = (ImageView) stuView.findViewById(R.id.img_stu);
        imgStu.setImageResource(student.getStuImg());
        TextView tvStu = (TextView) stuView.findViewById(R.id.tv_name);
        tvStu.setText(student.getName());
        return stuView;
    }

}

DataUtils.java

/**
 * 功能:初始化資料工具類
 * 作者:猴子搬來的救兵
 * 部落格地址:http://blog.csdn.net/mynameishuangshuai
 * 日期:2016/10/01
 */
public class DataUtils {

    public final static List<Student> stuData = new ArrayList<Student>();

    public static Student get(int i){
        return stuData.get(i);
    }

    public static int stuSize(){
        return stuData.size();
    }

    static {
        stuData.add(new Student("張三",R.mipmap.ic_launcher));
        stuData.add(new Student("李四",R.mipmap.ic_launcher));
        stuData.add(new Student("王五",R.mipmap.ic_launcher));
        stuData.add(new Student("趙六",R.mipmap.ic_launcher));
        stuData.add(new Student("陳七",R.mipmap.ic_launcher));
        stuData.add(new Student("孫八",R.mipmap.ic_launcher));
        stuData.add(new Student("猴子搬來的救兵",R.mipmap.ic_launcher));

    }
}

Student.java

/**
 * 功能:學生Bean
 * 作者:猴子搬來的救兵
 * 部落格地址:http://blog.csdn.net/mynameishuangshuai
 * 日期:2016/10/01
 */
public class Student {

    private String name;
    private int stuImg;

    public Student() {
    }

    public Student(String name, int stuImg) {
        this.name = name;
        this.stuImg = stuImg;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getStuImg() {
        return stuImg;
    }

    public void setStuImg(int stuImg) {
        this.stuImg = stuImg;
    }

}

專案執行結果:

這裡寫圖片描述

MVP改造結構

這裡寫圖片描述

改造後的專案,我們增加了三大模組,分別是:

  • Model包負責處理資料
  • View包負責顯示處理
  • Presenter包是中間橋樑,負責Model和View的互動

通過MVP框架實現資料適配

定義Model層介面

public interface IStudentModel {
    // 載入資料
    void loadStudent(StudentOnLoadListener listener);
    interface StudentOnLoadListener{
       void onComplete(List<Student> students);
    }
}

定義View層介面

public interface IStudentView {
    // 顯示進度
    void showLoading();
    // 顯示學生
    void showStudents(List<Student> students);
}

新增Model層

/**
 * 功能:StudentModel 第一次資料處理
 * 作者:猴子搬來的救兵
 * 部落格地址:http://blog.csdn.net/mynameishuangshuai
 * 日期:2016/10/13 0013
 */
public class StudentModelImplOne implements IStudentModel {
    @Override
    public void loadStudent(StudentOnLoadListener listener) {
        Log.i("castiel","執行了StudentModelImplOne資料載入");
        //模擬Json資料
        List<Student> jsonStu1 = new ArrayList<Student>();
        jsonStu1.add(new Student("張三11", R.mipmap.ic_launcher));
        jsonStu1.add(new Student("李四11", R.mipmap.ic_launcher));
        jsonStu1.add(new Student("王五11", R.mipmap.ic_launcher));
        jsonStu1.add(new Student("趙六11", R.mipmap.ic_launcher));
        jsonStu1.add(new Student("陳七11", R.mipmap.ic_launcher));
        jsonStu1.add(new Student("孫八11", R.mipmap.ic_launcher));
        jsonStu1.add(new Student("猴子搬來的救兵11http://blog.csdn.net/mynameishuangshuai", R.mipmap.ic_launcher));
        // 通過回撥方式傳遞資料
        if (listener != null) {
            listener.onComplete(jsonStu1);
        }
    }
}

新增Presenter層

public class StudentPresenterOne {
    // Model
   IStudentModel mStudentModel = new StudentModelImplOne();
    // View
    IStudentView mStudentView;
    // 初始化View
    public StudentPresenterOne(IStudentView mStudentView) {
        this.mStudentView = mStudentView;
    }
    public void fetch(){
        mStudentView.showLoading();// 顯示進度
        // Model獲取資料
        if (mStudentModel != null){
            mStudentModel.loadStudent(new IStudentModel.StudentOnLoadListener() {
                @Override
                public void onComplete(List<Student> students) {
                    // 得到資料後給View顯示資料
                    mStudentView.showStudents(students);
                }
            });
        }
    }
}

執行操作

/**
 * 功能:MainActivity 用列表形式實現
 * 作者:猴子搬來的救兵
 * 部落格地址:http://blog.csdn.net/mynameishuangshuai
 * 日期:2016/10/01
 */
public class MainActivity extends AppCompatActivity implements IStudentView{

    ListView myList;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        myList = (ListView) findViewById(R.id.my_list);
        new StudentPresenterOne(this).fetch();
    }

    @Override
    public void showLoading() {
        Toast.makeText(this, "正在載入資料中……", Toast.LENGTH_SHORT).show();
    }

    @Override
    public void showStudents(List<Student> students) {
        // 負責顯示
        myList.setAdapter(new StudentAdapter(MainActivity.this,students));
    }
}

顯示結果

這裡寫圖片描述

通過MVP框架實現View改變

實現新的Model層

public class StudentModelImplTwo implements IStudentModel {
    @Override
    public void loadStudent(StudentOnLoadListener listener) {

        Log.i("castiel","執行了StudentModelImplTwo資料載入");
        // 模擬網路載入延時資料
        SystemClock.sleep(1000);

        //模擬Json資料
        List<Student> jsonStu2 = new ArrayList<Student>();
        jsonStu2.add(new Student("張三22", R.mipmap.ic_launcher));
        jsonStu2.add(new Student("李四22", R.mipmap.ic_launcher));
        jsonStu2.add(new Student("王五22", R.mipmap.ic_launcher));
        jsonStu2.add(new Student("趙六22", R.mipmap.ic_launcher));
        jsonStu2.add(new Student("陳七22", R.mipmap.ic_launcher));
        jsonStu2.add(new Student("孫八22", R.mipmap.ic_launcher));
        jsonStu2.add(new Student("猴子搬來的救兵22http://blog.csdn.net/mynameishuangshuai", R.mipmap.ic_launcher));

        // 通過回撥方式傳遞資料
        if (listener != null) {
            listener.onComplete(jsonStu2);
        }
    }
}

實現新的Presenter層

public class StudentPresenterTwo {
    // Model
    IStudentModel mStudentModel = new StudentModelImplTwo();
    // View
    IStudentView mStudentView;
    // 初始化View
    public StudentPresenterTwo(IStudentView mStudentView) {
        this.mStudentView = mStudentView;
    }

    public void fetch(){
        mStudentView.showLoading();// 顯示進度
        // Model獲取資料
        if (mStudentModel != null){
            mStudentModel.loadStudent(new IStudentModel.StudentOnLoadListener() {
                @Override
                public void onComplete(List<Student> students) {
                    // 得到資料後給View顯示資料
                    mStudentView.showStudents(students);
                }
            });
        }
    }
}

新增新的Activity,改變列表佈局為網格佈局

/**
 * 功能:TwoActivity 用網格形式實現
 * 作者:猴子搬來的救兵
 * 部落格地址:http://blog.csdn.net/mynameishuangshuai
 * 日期:2016/10/01
 */
public class TwoActivity extends AppCompatActivity implements IStudentView{

    GridView myGrid;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_two);
        myGrid = (GridView) findViewById(R.id.my_grid);
        new StudentPresenterTwo(this).fetch();
    }

    @Override
    public void showLoading() {
        Toast.makeText(this, "正在載入資料中……", Toast.LENGTH_SHORT).show();
    }

    @Override
    public void showStudents(List<Student> students) {
        // 負責顯示
        myGrid.setAdapter(new StudentGridAdapter(TwoActivity.this,students));
    }
}

新的Adapter

public class StudentGridAdapter extends BaseAdapter {

    private LayoutInflater myInflater;
    private List<Student> data;

    public StudentGridAdapter(Context context, List<Student> data) {
        myInflater = LayoutInflater.from(context);
        this.data = data;
    }

    @Override
    public int getCount() {
        return data.size();
    }

    @Override
    public Object getItem(int position) {
        return data.get(position);
    }

    @Override
    public long getItemId(int position) {
        return position;
    }

    @Override
    public View getView(int position, View convertView, ViewGroup parent) {
        View stuView = myInflater.inflate(R.layout.stu_item, null);
        Student student = data.get(position);
        ImageView imgStu = (ImageView) stuView.findViewById(R.id.img_stu);
        imgStu.setImageResource(student.getStuImg());
        TextView tvStu = (TextView) stuView.findViewById(R.id.tv_name);
        tvStu.setText(student.getName());
        return stuView;
    }

}

執行結果

這裡寫圖片描述