1. 程式人生 > >MVC、MVP、MVVM深入理解與使用

MVC、MVP、MVVM深入理解與使用

引言

最近有打算寫一些獨立的App的打算,所以對現在的Android架構與技術的選擇進行了重新的思考,同時總結了對三個典型架構深入的理解及它們的應用方法。

MVC

概念

MVC是Model(模型)-View(檢視)-Controller(控制器)的縮寫,它用一種將業務邏輯、資料、介面顯示分離的方法組織程式碼,將業務邏輯聚集到Model層元件裡面,在改進和個性化定製介面及使用者互動的同時,不需要重新編寫業務邏輯。下圖說明了各層之間的關係:
這裡寫圖片描述

  1. View層:
    負責介面的展示。
    在原生的Android專案中,layout.xml裡面的xml檔案就對應於MVC的View層,裡面都是一些view的佈局程式碼。
  2. Controller層:
    負責獲取和處理使用者的輸入;
    負責將使用者輸入傳給負責業務邏輯層去做資料上的操作(如增刪改查);
    負責獲取Model層對於資料操作的結果,並將其傳給View層去做展示;
    在原生的Android專案中,對應的就是各種activity元件。
  3. Model層:
    負責業務邏輯處理,比如資料庫存取操作,網路操作,複雜的演算法,耗時的任務。
    在原生的Android專案中,各種java bean,還有一些類似repository類就對應於model層。

將系統進行MVC分層的核心思路就是分離元件,降低元件耦合性,元件獨立演化。

應用範例

比如,你的介面有一個按鈕,按下這個按鈕去網路上下載一個檔案。這個按鈕是view層的,是使用xml來寫的,而那些和網路連線相關的程式碼寫在其他類裡,比如你可以寫一個專門的networkFile類,這個就是model層,那怎麼連線這兩層呢?是通過button.setOnClickListener()這個函式,這個函式就寫在了activity中,對應於controller層。

缺陷

在原生的Android專案中,xml作為view層,僅僅是靜態的佈局介面,你想去動態的改變一個頁面的背景,或者動態的隱藏/顯示一個按鈕,或者顯示提示下載進度使用的view元件progressDialog,這些都沒辦法在xml中做,只能把程式碼寫在activity中,造成了activity既是controller層,又承擔了view層的部分功能,兩層之間產生了耦合。這樣是非常不利於進行單元測試的。

MVP

概念

MVP是Model–View–Presenter的縮寫,MVP與MVC的主要區別是在MVP中View並不直接使用Model,它們之間的通訊是通過Presenter (MVC中通過Controller)來進行的,所有的互動都發生在Presenter內部,而在MVC中View會從直接Model中讀取資料而不是通過 Controller。模型與檢視完全分離,我們可以修改檢視而不影響模型,可以更高效地使用模型,因為所有的互動在Presenter裡面,同時方便對各層進行單元測試。
這裡寫圖片描述


在Android開發實踐中,如何應用MVP呢?MVC中,Activity耦合了controller層和view層的問題如何在MVP中解決呢?

首先,我們知道面向介面的程式設計可以很好的解決各元件模組之間的耦合問題。對於view層和presenter層的通訊,我們是可以通過介面實現的,具體的意思就是說我們的activity,fragment可以去實現實現定義好的介面,而在對應的presenter中通過介面呼叫方法。因此,有兩種解決思路:

  1. 直接將Activity看作View,讓它只承擔View的責任。
  2. 將Activity看作一個MVP三者以外的一個controller,用於建立、連線View和Presenter。
    Google推出的官方MVP例項裡,使用的就是第2種思路,它讓每一個Activity都擁有一個Fragment來作為View,然後每個Activity也對應一個Presenter,在Activity裡只處理與生命週期有關的內容,並跳出MVP之外,負責例項化Model,View,Presenter,並負責將三者合理的建立聯絡,承擔的就是一個上帝視角。下面摘錄了Google官方demo的關鍵類及介面,可以對照上面的解說理解其含義。

應用範例

1.建立介面BaseView,裡面宣告通用的介面方法:

package com.example.android.architecture.blueprints.todoapp;

public interface BaseView<T> {

    void setPresenter(T presenter);

}

2.介面BasePresenter,裡面宣告通用的介面方法:

package com.example.android.architecture.blueprints.todoapp;

public interface BasePresenter {

    void start();

}

3.建立複合介面StatisticsContract,方便根據需求擴充套件新的介面方法:

package com.example.android.architecture.blueprints.todoapp.statistics;

import com.example.android.architecture.blueprints.todoapp.BasePresenter;
import com.example.android.architecture.blueprints.todoapp.BaseView;

/**
 * This specifies the contract between the view and the presenter.
 */
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 {

    }
}

4.在Presenter層中,建立 StatisticsPresenter類同時必須實現介面StatisticsContract.Presenter:

import android.support.annotation.NonNull;

import com.example.android.architecture.blueprints.todoapp.data.Task;
import com.example.android.architecture.blueprints.todoapp.data.source.TasksDataSource;
import com.example.android.architecture.blueprints.todoapp.data.source.TasksRepository;
import com.example.android.architecture.blueprints.todoapp.util.EspressoIdlingResource;

import java.util.List;

import static com.google.common.base.Preconditions.checkNotNull;

/**
 * Listens to user actions from the UI ({@link StatisticsFragment}), retrieves the data and updates
 * the UI as required.
 */
public class StatisticsPresenter implements StatisticsContract.Presenter {

    private final TasksRepository mTasksRepository;

    private final StatisticsContract.View mStatisticsView;

    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;

                // This callback may be called twice, once for the cache and once for loading
                // the data from the server API, so we check before decrementing, otherwise
                // it throws "Counter has been corrupted!" exception.
                if (!EspressoIdlingResource.getIdlingResource().isIdleNow()) {
                    EspressoIdlingResource.decrement(); // Set app as idle.
                }

                // We calculate number of active and completed tasks
                for (Task task : tasks) {
                    if (task.isCompleted()) {
                        completedTasks += 1;
                    } else {
                        activeTasks += 1;
                    }
                }
                // The view may not be able to handle UI updates anymore
                if (!mStatisticsView.isActive()) {
                    return;
                }
                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();
            }
        });
    }
}

5.在View層中,編寫 類StatisticsFragment同時必須實現介面StatisticsContract.View:

/**
 * Main UI for the statistics screen.
 */
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);
    }

    @Nullable
    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container,
                             Bundle savedInstanceState) {
        View root = inflater.inflate(R.layout.statistics_frag, container, false);
        mStatisticsTV = (TextView) root.findViewById(R.id.statistics);
        return root;
    }

    @Override
    public void onResume() {
        super.onResume();
        mPresenter.start();
    }

    @Override
    public void setProgressIndicator(boolean active) {
        if (active) {
            mStatisticsTV.setText(getString(R.string.loading));
        } else {
            mStatisticsTV.setText("");
        }
    }

    @Override
    public void showStatistics(int numberOfIncompleteTasks, int numberOfCompletedTasks) {
        if (numberOfCompletedTasks == 0 && numberOfIncompleteTasks == 0) {
            mStatisticsTV.setText(getResources().getString(R.string.statistics_no_tasks));
        } else {
            String displayString = getResources().getString(R.string.statistics_active_tasks) + " "
                    + numberOfIncompleteTasks + "\n" + getResources().getString(
                    R.string.statistics_completed_tasks) + " " + numberOfCompletedTasks;
            mStatisticsTV.setText(displayString);
        }
    }

    @Override
    public void showLoadingStatisticsError() {
        mStatisticsTV.setText(getResources().getString(R.string.statistics_error));
    }

    @Override
    public boolean isActive() {
        return isAdded();
    }
}

6.在Model層中,建立介面TasksDataSource:

package com.example.android.architecture.blueprints.todoapp.data.source;

import android.support.annotation.NonNull;

import com.example.android.architecture.blueprints.todoapp.data.Task;

import java.util.List;

public interface TasksDataSource {

    interface LoadTasksCallback {

        void onTasksLoaded(List<Task> tasks);

        void onDataNotAvailable();
    }

    interface GetTaskCallback {

        void onTaskLoaded(Task task);

        void onDataNotAvailable();
    }

    void getTasks(@NonNull LoadTasksCallback callback);

    void getTask(@NonNull String taskId, @NonNull GetTaskCallback callback);

    void saveTask(@NonNull Task task);

    void completeTask(@NonNull Task task);

    void completeTask(@NonNull String taskId);

    void activateTask(@NonNull Task task);

    void activateTask(@NonNull String taskId);

    void clearCompletedTasks();

    void refreshTasks();

    void deleteAllTasks();

    void deleteTask(@NonNull String taskId);
}

7.在Model層中,建立類TasksRepository實現介面TasksDataSource:

package com.example.android.architecture.blueprints.todoapp.data.source;

import static com.google.common.base.Preconditions.checkNotNull;

import android.support.annotation.NonNull;
import android.support.annotation.Nullable;

import com.example.android.architecture.blueprints.todoapp.data.Task;

import java.util.ArrayList;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;

/**
 * Concrete implementation to load tasks from the data sources into a cache.
 * <p>
 * For simplicity, this implements a dumb synchronisation between locally persisted data and data
 * obtained from the server, by using the remote data source only if the local database doesn't
 * exist or is empty.
 */
public class TasksRepository implements TasksDataSource {

    private static TasksRepository INSTANCE = null;

    private final TasksDataSource mTasksRemoteDataSource;

    private final TasksDataSource mTasksLocalDataSource;

    /**
     * This variable has package local visibility so it can be accessed from tests.
     */
    Map<String, Task> mCachedTasks;

    /**
     * Marks the cache as invalid, to force an update the next time data is requested. This variable
     * has package local visibility so it can be accessed from tests.
     */
    boolean mCacheIsDirty = false;

    // Prevent direct instantiation.
    private TasksRepository(@NonNull TasksDataSource tasksRemoteDataSource,
                            @NonNull TasksDataSource tasksLocalDataSource) {
        mTasksRemoteDataSource = checkNotNull(tasksRemoteDataSource);
        mTasksLocalDataSource = checkNotNull(tasksLocalDataSource);
    }

    /**
     * Returns the single instance of this class, creating it if necessary.
     *
     * @param tasksRemoteDataSource the backend data source
     * @param tasksLocalDataSource  the device storage data source
     * @return the {@link TasksRepository} instance
     */
    public static TasksRepository getInstance(TasksDataSource tasksRemoteDataSource,
                                              TasksDataSource tasksLocalDataSource) {
        if (INSTANCE == null) {
            INSTANCE = new TasksRepository(tasksRemoteDataSource, tasksLocalDataSource);
        }
        return INSTANCE;
    }

    /**
     * Used to force {@link #getInstance(TasksDataSource, TasksDataSource)} to create a new instance
     * next time it's called.
     */
    public static void destroyInstance() {
        INSTANCE = null;
    }


    @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);
                }
            });
        }
    }

    @Override
    public void saveTask(@NonNull Task task) {
        checkNotNull(task);
        mTasksRemoteDataSource.saveTask(task);
        mTasksLocalDataSource.saveTask(task);

        // Do in memory cache update to keep the app UI up to date
        if (mCachedTasks == null) {
            mCachedTasks = new LinkedHashMap<>();
        }
        mCachedTasks.put(task.getId(), task);
    }

    @Override
    public void completeTask(@NonNull Task task) {
        checkNotNull(task);
        mTasksRemoteDataSource.completeTask(task);
        mTasksLocalDataSource.completeTask(task);

        Task completedTask = new Task(task.getTitle(), task.getDescription(), task.getId(), true);

        // Do in memory cache update to keep the app UI up to date
        if (mCachedTasks == null) {
            mCachedTasks = new LinkedHashMap<>();
        }
        mCachedTasks.put(task.getId(), completedTask);
    }

    @Override
    public void completeTask(@NonNull String taskId) {
        checkNotNull(taskId);
        completeTask(getTaskWithId(taskId));
    }

    @Override
    public void activateTask(@NonNull Task task) {
        checkNotNull(task);
        mTasksRemoteDataSource.activateTask(task);
        mTasksLocalDataSource.activateTask(task);

        Task activeTask = new Task(task.getTitle(), task.getDescription(), task.getId());

        // Do in memory cache update to keep the app UI up to date
        if (mCachedTasks == null) {
            mCachedTasks = new LinkedHashMap<>();
        }
        mCachedTasks.put(task.getId(), activeTask);
    }

    @Override
    public void activateTask(@NonNull String taskId) {
        checkNotNull(taskId);
        activateTask(getTaskWithId(taskId));
    }

    @Override
    public void clearCompletedTasks() {
        mTasksRemoteDataSource.clearCompletedTasks();
        mTasksLocalDataSource.clearCompletedTasks();

        // Do in memory cache update to keep the app UI up to date
        if (mCachedTasks == null) {
            mCachedTasks = new LinkedHashMap<>();
        }
        Iterator<Map.Entry<String, Task>> it = mCachedTasks.entrySet().iterator();
        while (it.hasNext()) {
            Map.Entry<String, Task> entry = it.next();
            if (entry.getValue().isCompleted()) {
                it.remove();
            }
        }
    }

    @Override
    public void getTask(@NonNull final String taskId, @NonNull final GetTaskCallback callback) {
        checkNotNull(taskId);
        checkNotNull(callback);

        Task cachedTask = getTaskWithId(taskId);

        // Respond immediately with cache if available
        if (cachedTask != null) {
            callback.onTaskLoaded(cachedTask);
            return;
        }

        // Load from server/persisted if needed.

        // Is the task in the local data source? If not, query the network.
        mTasksLocalDataSource.getTask(taskId, new GetTaskCallback() {
            @Override
            public void onTaskLoaded(Task task) {
                callback.onTaskLoaded(task);
            }

            @Override
            public void onDataNotAvailable() {
                mTasksRemoteDataSource.getTask(taskId, new GetTaskCallback() {
                    @Override
                    public void onTaskLoaded(Task task) {
                        callback.onTaskLoaded(task);
                    }

                    @Override
                    public void onDataNotAvailable() {
                        callback.onDataNotAvailable();
                    }
                });
            }
        });
    }

    @Override
    public void refreshTasks() {
        mCacheIsDirty = true;
    }

    @Override
    public void deleteAllTasks() {
        mTasksRemoteDataSource.deleteAllTasks();
        mTasksLocalDataSource.deleteAllTasks();

        if (mCachedTasks == null) {
            mCachedTasks = new LinkedHashMap<>();
        }
        mCachedTasks.clear();
    }

    @Override
    public void deleteTask(@NonNull String taskId) {
        mTasksRemoteDataSource.deleteTask(checkNotNull(taskId));
        mTasksLocalDataSource.deleteTask(checkNotNull(taskId));

        mCachedTasks.remove(taskId);
    }

    private void getTasksFromRemoteDataSource(@NonNull final LoadTasksCallback callback) {
        mTasksRemoteDataSource.getTasks(new LoadTasksCallback() {
            @Override
            public void onTasksLoaded(List<Task> tasks) {
                refreshCache(tasks);
                refreshLocalDataSource(tasks);
                callback.onTasksLoaded(new ArrayList<>(mCachedTasks.values()));
            }

            @Override
            public void onDataNotAvailable() {
                callback.onDataNotAvailable();
            }
        });
    }

    private void refreshCache(List<Task> tasks) {
        if (mCachedTasks == null) {
            mCachedTasks = new LinkedHashMap<>();
        }
        mCachedTasks.clear();
        for (Task task : tasks) {
            mCachedTasks.put(task.getId(), task);
        }
        mCacheIsDirty = false;
    }

    private void refreshLocalDataSource(List<Task> tasks) {
        mTasksLocalDataSource.deleteAllTasks();
        for (Task task : tasks) {
            mTasksLocalDataSource.saveTask(task);
        }
    }

    @Nullable
    private Task getTaskWithId(@NonNull String id) {
        checkNotNull(id);
        if (mCachedTasks == null || mCachedTasks.isEmpty()) {
            return null;
        } else {
            return mCachedTasks.get(id);
        }
    }
}

8.編寫 StatisticsActivity.java檔案,在裡面例項化StatisticsFragment及StatisticsPresenter:

/**
 * Show statistics for tasks.
 */
public class StatisticsActivity extends AppCompatActivity {

    private DrawerLayout mDrawerLayout;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        setContentView(R.layout.statistics_act);

        // Set up the toolbar.
        Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
        setSupportActionBar(toolbar);
        ActionBar ab = getSupportActionBar();
        ab.setTitle(R.string.statistics_title);
        ab.setHomeAsUpIndicator(R.drawable.ic_menu);
        ab.setDisplayHomeAsUpEnabled(true);

        // Set up the navigation drawer.
        mDrawerLayout = (DrawerLayout) findViewById(R.id.drawer_layout);
        mDrawerLayout.setStatusBarBackground(R.color.colorPrimaryDark);
        NavigationView navigationView = (NavigationView) findViewById(R.id.nav_view);
        if (navigationView != null) {
            setupDrawerContent(navigationView);
        }

        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);
    }

    @Override
    public boolean onOptionsItemSelected(MenuItem item) {
        switch (item.getItemId()) {
            case android.R.id.home:
                // Open the navigation drawer when the home icon is selected from the toolbar.
                mDrawerLayout.openDrawer(GravityCompat.START);
                return true;
        }
        return super.onOptionsItemSelected(item);
    }

    private void setupDrawerContent(NavigationView navigationView) {
        navigationView.setNavigationItemSelectedListener(
                new NavigationView.OnNavigationItemSelectedListener() {
                    @Override
                    public boolean onNavigationItemSelected(MenuItem menuItem) {
                        switch (menuItem.getItemId()) {
                            case R.id.list_navigation_menu_item:
                                Intent intent =
                                        new Intent(StatisticsActivity.this, TasksActivity.class);
                                intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK
                                        | Intent.FLAG_ACTIVITY_CLEAR_TASK);
                                startActivity(intent);
                                break;
                            case R.id.statistics_navigation_menu_item:
                                // Do nothing, we're already on that screen
                                break;
                            default:
                                break;
                        }
                        // Close the navigation drawer when an item is selected.
                        menuItem.setChecked(true);
                        mDrawerLayout.closeDrawers();
                        return true;
                    }
                });
    }
}

缺陷

MVP的問題在於,由於我們使用了介面的方式去連線view層和presenter層,這樣就導致了一個問題,如果你的專案中有一個邏輯很複雜的頁面,你的介面會有很多,十幾二十個都不足為奇。想象一個app中有很多個這樣複雜的頁面,維護介面的成本就會非常的大。

這個問題的解決方案就是你得根據自己的業務邏輯去斟酌著寫介面。你可以定義一些基類介面,把一些公共的邏輯,比如網路請求成功失敗,toast等等放在裡面,之後你再定義新的介面的時候可以繼承自那些基型別介面,這樣會好不少。
另外,Presenter有大量的View->Model,Model->View的資料手動同步邏輯,造成Presenter比較笨重,維護起來會比較困難。這就促成了下面要講的MVVM+Data Binding的出現。

MVVM

概念

MVVM模式包含了三個部分:

Model層:代表你的基本業務邏輯;
View 層:顯示內容;
ViewModel 層:將前面兩者聯絡在一起的物件;

一個ViewModel介面提供了兩個東西:動作和資料。動作改變Model的下層(click listener,監聽文字改變的listener等等),而資料則是Model的內容。
比如,為一個拍賣頁面服務的ViewModel可能被作為資料呈現:item裡的一張圖片,標題,描述,價格。也可能作為動作呈現:投標,購買或者聯絡賣家。

在傳統的安卓架構中,controller負責推送資料給view。在Activity中findview,然後在它上面設定內容。

在MVVM中,ViewModel在改變內容之後通知binding framework內容發生了改變。然後binding framework自動更新和那些內容繫結的view。這兩個元件只是通過ViewModel鬆耦合在一起。

這種架構模式之所以牛逼,除了明顯智慧化了的View之外,還方便了測試。

因為ViewModel不在依賴於View了,你可以在沒有View的情況下也能測試ViewModel。在合適的依賴注入的幫助下,測試就會變得非常簡單。

例如,在一個測試用例中,我們不將VM和一個真實的View繫結,而是直接建立一個VM,給它一些資料,然後在上面呼叫操作,以確保資料的變化是正確的。比如剛剛的拍賣案例中,你可以建立一個帶有模擬的API服務的VM,讓它載入任意的item然後在上面呼叫拍賣的行為,以確認結果資料是正確的。所有這些工作都不必和一個具體的view互動。而在測試view的時候,我們可以建立一系列帶有預先設定好資料(比如依賴於網路呼叫的或者資料庫內容的)的模擬model,然後觀察在不同的資料集合面前view是如何表現的。
(關於MVVM的單元測試方法,會在後面的文章單獨討論。)

應用範例

google提出了Data Binding 用於實現MVVM,其中ViewModel可以理解成是View的資料模型和Presenter的合體,ViewModel和View之間的互動通過 Data Binding完成,而Data Binding可以實現雙向的互動,這就使得檢視和控制層之間的耦合程度進一步降低,關注點分離更為徹底,同時減輕了Activity的壓力。

下面我們看一個用Data Binding 實現MVVM的例子。

我們假設,我們要做一個標籤應用,這個應用非常簡單,就是一個recycler view,它其中的項只有各種label。

1.首先配置環境建立工程,必須使用使用的android studio 1.3以上的版本,如果要使用data binding的話在build.gradle裡面新增:

android {
    compileSdkVersion 23
    buildToolsVersion "23.0.2"

    dataBinding {
        enabled = true
    }

    defaultConfig {
        applicationId "com.chan.databindingdemo"
        minSdkVersion 14
        targetSdkVersion 23
        versionCode 1
        versionName "1.0"
    }
    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
        }
    }
}
dataBinding {
   enable = true
} 

2.在Model層,自定義一個類:

public class DemoItem {    
    private String m_label;    
    public DemoItem(String label) {
        m_label = label;
    public String getLabel() {    
        return m_label;
    }
    public void setLabel(String label) {
        m_label = label;
    }
}

程式碼非常簡單,就一個label field,和一些getter & setter

3.在View層,對於recycler中的子項,顯然是要自定義佈局,比如:

<?xml version="1.0" encoding="utf-8"?><layout xmlns:android="http://schemas.android.com/apk/res/android">

    <data>
        <variable   name="data" type="com.chan.databindingdemo.DemoItem"/>
    </data>

    <LinearLayout android:orientation="vertical"
                  android:layout_width="match_parent"
                  android:layout_height="wrap_content">
        <TextView            
            android:id="@+id/label"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:text="@{data.label}"/>
    </LinearLayout>
</layout>

可以看到,這個layout的xml檔案根節點是layout 並且,還定義了名為data型別為com.chan.databindingdemo.DemoItem的資料。它有一個data節點包裹,然後以variable的屬性給出。
往下閱讀便是RecyclerView的內容檢視了,很簡單 就一個TextView,可以看到對於TextView的android:text屬性,它的值@{data.label}。也就是說這個 text view的內容由data的label屬性給出。這時候,編譯器便生成了一個ViewDataBinding類,這個類所在的包名是以 我們的工程包+binding決定的,比如現在的應用包名是com.chan.databindingdemo,那麼這個編譯器生成的程式碼便在 com.chan.databindingdemo.databinding下,對於這個包下面的類的名字呢是以上文的Layout檔名決定的,比如:我的 layout檔名是item_layout.xml,那麼生成的類就為ItemLayoutBinding。
注意:ViewModel層的類由編譯器自動生成的,本例中就是上面提到的生成類ItemLayoutBinding.

RecyclerView的使用方法乃基礎內容,本文並不對基礎的內容做講解,不熟悉的可自行學習。

4.在View層,還要定義view holder:

/**
 * Created by chan on 16/3/12.
 */public class RecyclerHolder extends RecyclerView.ViewHolder {
    ItemLayoutBinding m_itemLayoutBinding;    
    public RecyclerHolder(View itemView, ViewDataBinding viewDataBinding) {        
        super(itemView);
        m_itemLayoutBinding = (ItemLayoutBinding) viewDataBinding;
    }    
    public void bind(DemoItem demoItem) {
        m_itemLayoutBinding.setData(demoItem);
    }
}

RecyclerHolder的建構函式多了一個viewDataBinding,並且這個型別的真實型別為ItemLayouBinding,就是剛剛編譯器自動生成的那個類。一個ViewModel要和view進行 繫結,需要呼叫binding的set方法,具體的set方法名根據上文的layout檔案的variable的name屬性決定,所以我們這裡是 m_itemLayoutBinding.setData(demoItem);
5.在View層,實現adapter:

/**
 * Created by chan on 16/3/12.
 */public class RecyclerAdapter extends RecyclerView.Adapter<RecyclerHolder> {    
    private List<DemoItem> m_demoItems;    
    public RecyclerAdapter(Context context, List<DemoItem> demoItems) {
        m_layoutInflater = LayoutInflater.from(context);
        m_demoItems = demoItems;
    }

    @Override    
    public RecyclerHolder onCreateViewHolder(ViewGroup parent, int viewType) {
        ViewDataBinding viewDataBinding = DataBindingUtil.inflate(m_layoutInflater, R.layout.item_layout, parent, false);        
        return new RecyclerHolder(viewDataBinding.getRoot(), viewDataBinding);
    }

    @Override    
    public void onBindViewHolder(RecyclerHolder holder, int position) {
        final int currentPosition = holder.getLayoutPosition();
        holder.bind(m_demoItems.get(currentPosition));
    }

    @Override    
    public int getItemCount() {        
        return m_demoItems.size();
    }
}

值得注意的是,如果我們要inflate檢視,必須要通過DataBindingUtil來實現!!否則會丟擲一個非捕獲異常!!
也就是這裡的:

 ViewDataBinding viewDataBinding = DataBindingUtil.inflate(m_layoutInflater, R.layout.item_layout, parent, false);

到此,一個簡單的demo出現了 ,不過我們剛剛講過,如果資料改變了,view能自動重新整理我們可以引入觀察者模式。
還記得剛剛的model嗎?我們只需要一個簡單的修改就可以完成了。

/**
 * Created by chan on 16/3/12.
 */public class DemoItem extends BaseObservable {
    private String m_label;    
    public DemoItem(String label) {
        m_label = label;
    }    
    @Bindable
    public String getLabel() {        
        return m_label;
    }    
    public void setLabel(String label) {
        m_label = label;
        notifyPropertyChanged(com.chan.databindingdemo.BR.label);
    }
}

如果一個應用要實現可觀察,只要實現 Observable這個介面就好了,原文:A class implementing the Observable interface will allow the binding to attach a single listener to a bound object to listen for changes of all properties on that object.
不過庫給我們實現了一個簡單的物件BaseObservable,它可以滿足我們的簡單需求。
我們在getter上加了一個bindable註解,並且在setter裡面,我們手動提醒了變化

有一些謎團,比如 com.chan.databindingdemo.BR.label是啥,我引入一下原文:
The Bindable annotation generates an entry in the BR class file during compilation. The BR class file will be generated in the module package. If the base class for data classes cannot be changed, the Observable interface may be implemented using the convenient PropertyChangeRegistry to store and notify listeners efficiently.

在編譯時期,bindable註解會在BR裡面生成一個項,這個項就是針對上文的getter。並且這個br類生成在專案的包下。

6.在View層中,activity完整的程式碼:

public class MainActivity extends AppCompatActivity {
    private RecyclerView m_recyclerView;    
    private List<DemoItem> m_demoItems = new ArrayList<>();    
    private int m_index = 0;    
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
        setSupportActionBar(toolbar);

        m_recyclerView = (RecyclerView) findViewById(R.id.id_recycler);
        m_recyclerView.setLayoutManager(new LinearLayoutManager(this));        
        for(int i = 0; i < 10; ++i) {
            m_demoItems.add(new DemoItem("標籤" + i));
        }

        RecyclerAdapter adapter = new RecyclerAdapter(this, m_demoItems);
        m_recyclerView.setAdapter(adapter);

        FloatingActionButton fab = (FloatingActionButton) findViewById(R.id.fab);
        fab.setOnClickListener(new View.OnClickListener() {            
            @Override
            public void onClick(View view) {
                m_demoItems.get(m_index).setLabel(System.currentTimeMillis() + "");
                m_index = (m_index + 1) % m_demoItems.size();
            }
        });
    }    
    @Override
// Inflate the menu; this adds items to the action bar if it is present.
    public boolean onCreateOptionsMenu(Menu menu) { 
        getMenuInflater().inflate(R.menu.menu_main, menu);        
        return true;
    }    
    @Override
    public boolean onOptionsItemSelected(MenuItem item) {        
        // Handle action bar item clicks here. The action bar will
        // automatically handle clicks on the Home/Up button, so long
        // as you specify a parent activity in AndroidManifest.xml.
        int id = item.getItemId();        
        //noinspection SimplifiableIfStatement
        if (id == R.id.action_settings) {            
            return true;
        }        
        return super.onOptionsItemSelected(item);
    }
}

總結

  1. 通過程式碼可以看出,明顯可以發現MVP模式下,增加了大量的層間介面,程式碼量相對來說確實增加了很多,但是邏輯相對的更加清晰。MVP架構不是很適合小型的專案,小型專案使用MVC架構即可,但是如果是一個較大型的專案還是可以選用MVP架構來做開發,畢竟邏輯清晰,,方便進行單元測試,維護起來也比較方便。
  2. MVVM架構是Android往後發展的趨勢,畢竟谷歌都推出了Datebinding,而且繫結的這種機制也確實帶來了model,view與vm的分離,從邏輯上看也確實清晰了很多,問題是暫時只支援單向繫結,這個也要等谷歌後面的更新完善,而且程式碼的閱讀性會下降很多,比較適合小型的專案,暫時不適合大型的專案

參考資料

相關推薦

MVCMVPMVVM深入理解使用

引言 最近有打算寫一些獨立的App的打算,所以對現在的Android架構與技術的選擇進行了重新的思考,同時總結了對三個典型架構深入的理解及它們的應用方法。 MVC 概念 MVC是Model(模型)-View(檢視)-Controller(控制器)

MVCMVPMVVM理解

最近看了一堆js框架的文件,有點亂,想分門別類整理一下,但是首先需要搞清楚這些框架裡面經常談論的MV*之類的概念。MVC的概念很早就知道,現在發現還有MVP、MVVM,那麼這些設計模式有什麼區別呢?談一下自己的理解。 剛開始理解這些概念的時候認為這幾種模式雖然都是要將vi

MVCMVPMVVM 三者解析 區別聯絡

理想的MVC模式中VC之間沒有直接依賴(沒有單向依賴),但現實中做不到。Native應用要一般由View分發事件給Controller,Controller要決定那些View使用者可見。 Web應用中情況好一點。使用者可以直接通過url直接訪問Controll

【框架篇】mvcmvpmvvm使用關系總結

mvc模型 details eset 網站架構 特性 自動 分享 規模 arch MVC MVC全名是Model View Controller,是模型(model)-視圖(view)-控制器(controller)的縮寫,一種軟件設計典範,用一種業務邏輯、數據、界面顯

MVCMVPMVVM模式對比總結(2)橫向構架模型

span del nec 處理請求 eth .cn pos 實現 通過 前言說明 在實戰項目及學習中來總結一下Android端項目構架 包括MVC、MVP、MVVM,主要針對移動Android端 該篇只描述橫向構架模型 目錄 1.構架基礎 2.橫向構架模型 3.縱向

淺談MVCMVPMVVM架構模式的區別和聯系

.html csdn 獲取 視圖 viewmodel url title tle htm 淺談MVC、MVP、MVVM架構模式的區別和聯系 學習了:http://www.cnblogs.com/guwei4037/p/5591183.html http://blog.csd

Android 程序架構: MVCMVPMVVMUnidirectionalClean...

不同 概念 可能 十年 tin gettext 聲明 數據 content 摘選自:GUI 應用程序架構的十年變遷:MVC、MVP、MVVM、Unidirectional、Cleanhttps://zhuanlan.zhihu.com/p/26799645 MV

從Script到Code BlocksCode Behind到MVCMVPMVVM

主題 描述 傳輸 對象之間的關系 方法 動力 基本 com load() 剛過去的周五(3-14)例行地主持了技術會議,主題正好是《UI層的設計模式——從Script、Code Behind到MVC、MVP、MVVM》,是前一天晚上才定的,中午花了半小時準備了下就開講了。

Android中MVCMVPMVVM具體解釋

line 業務邏輯 指令 問題 今天 操作 才幹 入口 pre 前言 今天有時間就剛好有想寫關於這幾個名詞。對於我來說。事實上這麽多名詞、思想歸根究竟就是要依據項

MVCMVPMVVM之間的關系

mvp 工作 更多 lan mod 技術 https v-model 產生 介紹   寫這篇隨筆完全是為了加深自己的印象,畢竟寫比看能獲得得更多,另外本人對這三種模式的認識還是淺薄的,有待在以後的工作學習中有更深入的理解,因此不免會有誤解,這裏推薦大家閱讀廖雪峰關於MVV

IOS —— MVCMVPMVVM 隨筆

以前做IOS開發工作的時候,部門領導苦口婆心的給我科普過MVC和MVVM的區別,簡要來說MVC框架臃腫,分工不明,只勝在程式碼量少。 MVVM程式碼量繁多、勝在框架分工明確便於除錯及應用。 當然那都是一倆年前對於當時剛入行作為菜雞的自己,對於這個框架的理解 現在重新來根據圖文講講來自三者的功能及區別。

【iOS】MVCMVPMVVM

MVC MVC模式涉及三種物件:模型物件、檢視物件、控制器物件。模型物件中儲存有應用程式的資料,檢視物件負責顯示模型物件的資料,並且允許使用者對其進行編輯。控制器物件是模型物件和檢視物件之間的協調者,負責對模型物件進行初始化,並將模型物件傳遞給檢視物件進行解析顯示。  

三層架構MVCMVPMVVM簡介

1. 三層架構 三層架構就是將整個業務應用劃分為: UI層:介面層(User Interface layer) BLL層:業務邏輯層(Business Logic Layer) DAL層:資料訪問層(Data access layer) 區分層次的目的是為了

Android 架構設計:MVCMVPMVVM和元件化

MVC、MVP和MVVM是常見的三種架構設計模式,當前MVP和MVVM的使用相對比較廣泛,當然MVC也並沒有過時之說。而所謂的元件化就是指將應用根據業務需求劃分成各個模組來進行開發,每個模組又可以編譯成獨立的APP進行開發。理論上講,元件化和前面三種架構設計不是

android原始碼設計模式——框構模式MVCMVPMVVM

一、框架模式、設計模式、架構模式的概念理解        通常來講框架面向於一系列相同行為程式碼的重用,而設計則面向的是一系列相同結構程式碼的重用,通常所說的架構則介於框架與設計之間 二、MVC、MVP、MVVM三種設計模式        2.1、MVC模式,常見的應用模式,

介面之下:還原真實的 MVCMVPMVVM 模式

一、前言 做客戶端開發、前端開發對MVC、MVP、MVVM這些名詞不瞭解也應該大致聽過,都是為了解決圖形介面應用程式複雜性管理問題而產生的應用架構模式。網上很多文章關於這方面的討論比較雜亂,各種MV*模式之間的區別分不清,甚至有些描述都是錯誤的。本文追根溯

iOS架構模式MVCMVPMVVM(內附demo)

MVC MVC的實現思路是:使用者操作View,在Controller層完成業務邏輯處理,更新Model層,將資料顯示在View層。 在MVC中,每個層之間都有關聯,耦合比較緊,在大型專案中,維護起來比較費力。 View把控制權交給Controller層,

淺談mvcmvpmvvm框架模式

背景:隨著軟體工業的發展,複雜的軟體沒有清晰合理的架構,很難開發和維護,於是出現了MVC框架模式。 一、MVC 簡介: MVC,是模型(model)-檢視(view)-控制器(contro

細說Android框架設計三劍客MVCMVPMVVM

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