Google官方MVP示例代碼閱讀筆記

DawnaJln 7年前發布 | 6K 次閱讀 安卓開發 Android開發 移動開發

寫在前面

這個項目很久之前就從 android-architecture 這個倉庫clone了這個MVP架構的todoapp,源碼也讀過,不過沒有整理過。最近整理資料準備畢設了,再讀一遍源碼,感受和以前又不同了。

如果各位對MVP模式不是很熟悉,可以看我之前的一篇文:

Android之MVP初嘗試,簡單易懂。下文的view一般是指MVP中的view。

剝絲抽繭,理清項目結構

國際慣例,上項目結構圖:

結構

從包名上很容易分辨出功能:addedittask是添加任務,data是數據管理,statistics是統計,taskdetail是任務詳情,tasks是任務瀏覽之類的。事實上這個項目的關鍵也就是: TasksTaskDetailAddEditTaskStatistics

這四個關鍵的地方都有相同之處:

  • 定義了view和presenter的契約
  • Activity負責fragment和presenter的創建
  • Fragment實現了view接口
  • presenter實現了presenter接口

也就是說,幾個功能每一個都是MVP的模式,只不過Model層是公用的。而且這個項目里View層都是Fragment,果然google推薦用Fragment自己的項目里也給我們做個示范……其實關于到底是不是要用Fragment,還是有些爭議的, 我為什么不主張使用Fragment ,這篇文關于Fragment講的比較到位了。那么到底要不要用呢?我覺得對于個體而言,不管你喜不喜歡,都要用一用,試一試,因為人要成長,必須踩坑。對于正式項目而言,則需要綜合考量,使用Fragment的利是否大于弊。

扯遠了,接下來看一下他代碼倉庫給的一張結構圖:

結構圖

可以看出來左邊是數據管理,典型的Model層。而右邊呢,你可能認為Activity是Presenter,事實上并不是,Presenter在Activity內,Fragment是View無疑。到這,我覺得關于這個項目結構的簡介已經足夠了,接下來看代碼。

我覺得看一個Android項目的正確姿勢應該是先把玩一下app,看一下功能。貼幾張app的圖:

首頁

添加任務

統計

任務詳情

接著就該上入口的Activity看一下了,這個項目的入口Activity是TasksActivity,所在的包是tasks,看一下有哪些東西:

tasks

第一個是自定義View,第二個就是入口Activity了,第三個即上面所說的“契約”,里面包含了View接口和Presenter接口。TasksFilterType則是一個枚舉,里面有三個過濾類型:所有,進行中的,完成的。TasksFragment就是MVP中的View了,TasksPresenter則是MVP中的Presenter了。看一下TasksActivity中的初始化代碼:

protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.tasks_act);
        Log.e(getClass().getSimpleName(),"onCreate");

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

    /**
     * 以下的DrawerLayout暫時不看了
     */
    // 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);
    }

    // 獲取fragment并將之添加到視圖上
    // 懸浮按鈕在這個taksFragment里設置的點擊事件
    TasksFragment tasksFragment =
            (TasksFragment) getSupportFragmentManager().findFragmentById(R.id.contentFrame);

// getSupportFragmentManager().findFragmentById() if (tasksFragment == null) { // Create the fragment tasksFragment = TasksFragment.newInstance(); // 提供方法幫助activity加載ui // 這個方法其實就是拿到一個事務,然后把這個fragment add到對應的id上了 ActivityUtils.addFragmentToActivity( getSupportFragmentManager(), tasksFragment, R.id.contentFrame); }

    // Create the presenter
    mTasksPresenter = new TasksPresenter(
            Injection.provideTasksRepository(getApplicationContext()), tasksFragment);

    // Load previously saved state, if available.
    if (savedInstanceState != null) {
        TasksFilterType currentFiltering =
                (TasksFilterType) savedInstanceState.getSerializable(CURRENT_FILTERING_KEY);
        mTasksPresenter.setFiltering(currentFiltering);
    }
}</code></pre> 

首先是初始化toolbar和側滑,這里不必深入細節,可以跳過這倆。之后初始化fragment和presenter,初始化Fragment先是嘗試通過id尋找可能已經存在的Fragment對象,如果沒有,則重新創建一個Fragment對象。下一步則是創建一個presenter,最后則是讓應用在橫豎屏狀態切換的情況下恢復數據。

接下來看一下View和Presenter的“契約”:

public interface TasksContract {

    interface View extends BaseView<Presenter> {

        void setLoadingIndicator(boolean active);

        void showTasks(List<Task> tasks);

        void showAddTask();

        void showTaskDetailsUi(String taskId);

        void showTaskMarkedComplete();

        void showTaskMarkedActive();

        void showCompletedTasksCleared();

        void showLoadingTasksError();

        void showNoTasks();

        void showActiveFilterLabel();

        void showCompletedFilterLabel();

        void showAllFilterLabel();

        void showNoActiveTasks();

        void showNoCompletedTasks();

        void showSuccessfullySavedMessage();

        boolean isActive();

        void showFilteringPopUpMenu();
    }

    interface Presenter extends BasePresenter {

        void result(int requestCode, int resultCode);

        void loadTasks(boolean forceUpdate);

        void addNewTask();

        void openTaskDetails(@NonNull Task requestedTask);

        void completeTask(@NonNull Task completedTask);

        void activateTask(@NonNull Task activeTask);

        void clearCompletedTasks();

        void setFiltering(TasksFilterType requestType);

        TasksFilterType getFiltering();
    }
}

這個接口里包含了View和Presenter,可以看到View和Presenter里的方法比較多,事實上這是應該的。因為在MVP架構里,View只負責根據Presenter的指示繪制UI,View將所有的用戶交互交給Presenter處理。所以Presenter的很多方法可能就是對用戶的輸入的處理,而有輸入必然有輸出,View接口定義的各個方法便是給Presenter回調的。Presenter通過回調函數將對用戶的輸入的處理結果推到View中,View再根據這個結果對UI進行相應的更新。而在此項目中,Fragment就是View,在Fragment的各個點擊事件中都調用了Presenter的對應方法,將業務邏輯交給Presenter處理。這看起來比傳統的MVC強上很多,因為傳統MVC中Activity既可以認為是Controller亦可以認為是View,職責難以分離,寫到后面可能一個Activity就有上千行的代碼,這會為后續的維護帶來不少麻煩。而MVP則將業務邏輯抽取到了Presenter中,作為View的Fragment或者Activity職責更加單一,無疑為后續的開發維護帶來了便利。

接下來詳細的看Presenter的初始化,Presenter的創建是在TasksActivity中完成的,查看其構造函數:

public TasksPresenter(@NonNull TasksRepository tasksRepository, @NonNull TasksContract.View tasksView) {
        mTasksRepository = checkNotNull(tasksRepository, "tasksRepository cannot be null");
        mTasksView = checkNotNull(tasksView, "tasksView cannot be null!");

        mTasksView.setPresenter(this);
    }

前兩個檢查傳入的參數是否為空,接著將其賦值給TasksPresenter內的引用,調用view的setPresenter方法,將自身傳入,這樣view中就可以使用presenter對象了,比直接從activity中拿看起來要優雅了不少。Presenter具體的邏輯就不看了,都是一些比較簡單的代碼,回顧一下打開這個app所發生的事件的流程:創建TasksActivity -> 初始化Toolbar -> 初始化側滑 -> 創建TasksFragment對象 -> 創建TaskPresenter對象 -> 給Fragment設置Presenter對象 -> 初始化Fragment布局,這樣一套流程下來,整個流程就理清了,接下來只是等待用戶的輸入了。

接下來要看的是從本文開始到現在都一直忽略了的Model:TasksRepository。不過在分析TasksRepository之前,安利一下這個項目里的實體類,寫的比較優雅,我們平時寫實體類時最好也能按照他的套路來寫。我為什么說他寫的比較優雅呢?因為各個屬性或者是帶返回值的方法都打上了@Nullable或者@NoNull注解來說明是否可以為空,事實上空指針這個錯可以算是平時經常遇到的錯了……不過如果你有良好的設計和編碼習慣,是可以避免的,帶上這兩個注解可以在編譯期給你相關的提示。不僅如此,這個實體類還復寫了equals()、hashCode()和toString()方法,而且實現的方式也符合規范,關于如何復寫這三個方法,在《effective java》上有很好的總結,各位可以去讀一下。

/*
 * Copyright 2016, The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

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

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

import com.google.common.base.Objects;
import com.google.common.base.Strings;

import java.util.UUID;

/**
 * Immutable model class for a Task.
 */
public final class Task {

    @NonNull
    private final String mId;

    @Nullable
    private final String mTitle;

    @Nullable
    private final String mDescription;

    private final boolean mCompleted;

    /**
     * Use this constructor to create a new active Task.
     *
     * @param title       title of the task
     * @param description description of the task
     */
    public Task(@Nullable String title, @Nullable String description) {
        this(title, description, UUID.randomUUID().toString(), false);
    }

    /**
     * Use this constructor to create an active Task if the Task already has an id (copy of another
     * Task).
     *
     * @param title       title of the task
     * @param description description of the task
     * @param id          id of the task
     */
    public Task(@Nullable String title, @Nullable String description, @NonNull String id) {
        this(title, description, id, false);
    }

    /**
     * Use this constructor to create a new completed Task.
     *
     * @param title       title of the task
     * @param description description of the task
     * @param completed   true if the task is completed, false if it's active
     */
    public Task(@Nullable String title, @Nullable String description, boolean completed) {
        this(title, description, UUID.randomUUID().toString(), completed);
    }

    /**
     * Use this constructor to specify a completed Task if the Task already has an id (copy of
     * another Task).
     *
     * @param title       title of the task
     * @param description description of the task
     * @param id          id of the task
     * @param completed   true if the task is completed, false if it's active
     */
    public Task(@Nullable String title, @Nullable String description,
                @NonNull String id, boolean completed) {
        mId = id;
        mTitle = title;
        mDescription = description;
        mCompleted = completed;
    }

    @NonNull
    public String getId() {
        return mId;
    }

    @Nullable
    public String getTitle() {
        return mTitle;
    }

    @Nullable
    public String getTitleForList() {
        if (!Strings.isNullOrEmpty(mTitle)) {
            return mTitle;
        } else {
            return mDescription;
        }
    }

    @Nullable
    public String getDescription() {
        return mDescription;
    }

    public boolean isCompleted() {
        return mCompleted;
    }

    public boolean isActive() {
        return !mCompleted;
    }

    public boolean isEmpty() {
        return Strings.isNullOrEmpty(mTitle) &&
               Strings.isNullOrEmpty(mDescription);
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        Task task = (Task) o;
        return Objects.equal(mId, task.mId) &&
               Objects.equal(mTitle, task.mTitle) &&
               Objects.equal(mDescription, task.mDescription);
    }

    @Override
    public int hashCode() {
        return Objects.hashCode(mId, mTitle, mDescription);
    }

    @Override
    public String toString() {
        return "Task with title " + mTitle;
    }
}

先看一下TasksRepository所在的包的結構:

data

可以從包名上看出local是從本地讀取數據,remote是遠程讀取,當然了,這里只是模擬遠程讀取。本地采用了數據庫存取的方式。在TasksRepository(下文簡稱TR)內部有兩個TasksDataSource的引用:

private final TasksDataSource mTasksRemoteDataSource;

    private final TasksDataSource mTasksLocalDataSource;

TasksDataSource是data包內的一個接口,使用接口引用,無非是想解耦,就算以后需求變更,不想采用數據庫的方式存儲數據,只要實現了這個接口,TR內部的代碼也無需變更。TR用了單例,實現方式并不是線程安全的:

/**
     * 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;
    }

說到底,他根本沒有線程安全的必要,至少在這個app里,沒有并發創建這個對象的場景,所以夠用就行了。在TR內部使用了一個LinkedHashMap作為容器來保存Tasks,主要看一下兩個方法,首先是存儲:

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

會將傳入的task存儲到遠程數據源和本地數據源(本地數據庫)中,然后將這個task傳到mCachedTasks(LinkedHashMap)中。代碼比較簡單,不做更多的分析,接下來看一下讀取Task:

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

這個taskId是需要獲取Task的id,也是唯一標識,GetTaskCallback則是負責傳遞數據的接口回調。首先是從內存中讀取數據,getTaskWithId方法就是,看一下代碼:

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

就從保存task的LinkedHashMap中讀取數據。如果這個過程讀取不到數據那么接著從本地數據源中讀取數據,如果本地數據源也沒有拿到這個數據,那么最終就從遠程數據源中讀取數據。

至此,我們簡單的過了一遍這個項目。

總結 & 再談MVP

Google這個示例項目,架構非常的清晰,也是很標準的MVP模式,項目中解耦做的也非常好。但是相對于一個功能簡單的應用來說,代碼量還是比較多的。當然,因為這只是一個小例子而已,可能會讓人覺得反而不如普通的MVC來開發方便,但是人無遠慮必有近憂。我們做東西的時候要盡量做長遠的打算,不然以后可能就會被淹沒在頻繁的需求變更里了。Google的這個項目有非常多值得我們學習的地方,比如我們寫MVP的時候也可以用一個Contract類來將View和Presenter放入其中,方便我們管理(改代碼)。

我們都知道MVP與MVC的主要區別是View和Model不直接交互,而是通過Presenter來完成交互,這樣可以修改View而不影響Model,實現了Model和View真正的完全分離。而MVP中將業務邏輯抽取放到Presenter中,使各個模塊的職責更加清晰,層次明了。而且還有很關鍵的一點,使用MVP架構使得應用能更加方便的進行單元測試。Android中雖然有很多測試框架,但是講實話,你不研究個一段時間很難使用那些框架進行有效的測試。而且很多測試是難以進行的,因為有的需要依賴Android環境或者UI環境。而如果使用了MVP架構,View層因為是用接口定義的,所以完全可以自己建一個View模擬視圖對象,這樣就可以使得我們的測試不必依賴UI環境。這樣最大的好處就是我們不必花費太多的時間去研究那些測試框架,也能寫出有效的單元測試,保證我們代碼的質量。

相較于MVP的優點,其缺點也是非常明顯的,從Google的這個示例代碼也能看出來,代碼量比較大,小型Android應用的開發用這個反而麻煩。Presenter既負責業務邏輯,又負責Model和View的交互,到后期也難免會膨脹、臃腫,最終造成這玩意可能維護起來也不簡單。

雖然MVP還是有不足的地方,但是相較于MVC,還是更容易的寫出易維護、測試的代碼的,所以各位不妨都閱讀一下Google的這個代碼~

 

來自:http://www.jianshu.com/p/ea967db22c2d

 

 本文由用戶 DawnaJln 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!