MVP源碼實戰解析——告別MVC,初識MVP

最近在籌備新項目的開發,打算使用google官方推薦的MVP配合Retrofit+RxAndroid打造一套新項目的框架。

先從MVP開始學習,然而網上關於MVP的博客以及學習資料實在是太多,所以打算刪繁就簡,先研究一番google官方的MVP實例。

谷歌的MVP框架源碼已經發布五個多月了,如今已經成爲了時下最火熱的Android框架,其視圖與模型完全分離的特性也受到了越來越多開發者的喜愛。

谷歌的MVP框架sample下載地址爲https://github.com/googlesamples/android-architecture/tree/todo-mvp/

本文是基於該實例進行對MVP進行源碼與概念的研究的。


MVP與MVP概念區分:

傳統MVC概念分爲:模型,視圖,控制三個模塊,如下圖所示:


其中,View是可以直接訪問Model的,所以也就造成了View還要承擔一定的業務邏輯,不能作爲單純的視圖層來使用,而且View和Controller也很難分開,很多時候我們經常會在View的響應時間裏寫很多Controller代碼,這樣直接導致代碼的複用性大大降低,到處都是UI實現相同但是邏輯略微不同的代碼,導致項目變得越來越臃腫不堪。

MVP的模式可由下圖所示:


從圖中可以清楚的看到,View僅僅只是“View”,不需要處理任何業務層的代碼,一切View和Model的操作都在Presenter中執行。

View是指顯示數據並且和用戶交互的層。在安卓中,它們可以是一個Activity,一個Fragment,一個android.view.View或者是一個Dialog。

Model 是數據源層。比如數據庫接口或者遠程服務器的api。

Presenter是從Model中獲取數據並提供給View的層,Presenter還負責處理後臺任務。

這裏需要說明的是Presenter與View是沒有直接關聯的,而是通過定義好的接口進行交互,從而使得在變更View的時候可以保持presenter的不邊,即重用View!這是MVC無法做到的。

這樣也使得我們在設計調試程序的時候變得更爲簡單,不必要先寫出詳細的View 佈局才能進行邏輯功能測試,因爲View在MVP中只是薄薄的一層顯示功能,我們可以首先設計和開發Presenter,在這個時候View只需要顯示一些基本的信息就可以了,在後面再根據需求更改View,這樣對Presenter層也不會有任何影響。

絕對不能與Model發生關係,是View作爲顯示層的關鍵原則。但是有的時候可能業務邏輯比較複雜,需要用到Model層的相關數據,這個時候可以在View和Presenter之間放置一個adapter,由這個adapter來訪問Model和View,避免兩者之間發生直接關聯,這個adapter也必須實現View的接口,保證了與Presenter之間接口的不變。這樣便可以實現View與Model層之間的完全解耦。

最後也是最重要的一點,在MVP模式裏,View應該只有簡單的Get和Set方法,不需要有任何複雜的業務邏輯,只需要toshow就可以了。


MVP官方實例源碼分析:

Github地址再發一遍:TODO MVP

導入到本地之後,我們先分析它的包結構:


其中功能分別爲:
addedittask: 添加編輯任務
data:            數據操作包
statistics:     統計數據包
task detail:   任務詳情包
tasks:        主要部分,包含定義視圖和presenter的接口規範以及部分實體類
我們可以看到google的模式是按項目功能進行分包,國內大部分項目可能是按類別進行分包,比如Aty一個包,bean一個包,不過我現在感覺還是按功能分包比較好,原因就是好找。。。可以做個簡單的計算,比如每次從一個包裏尋找一個類的時間爲t,一個功能線需要的類爲5,如果按類別分包,我們需要從不同的包比如aty,bean,adapter等裏面找到需要的類,總共花費時間至少5t,然而如果按功能分包,我們只需要一開始瀏覽一下需要的功能在哪個包裏,然後東西就全在裏面了。。。花費時間爲t!
我們再來看看每個包的具體類:

可以看到每個包大致都包含四個部分:
Contract:定義View與Presenter的接口。
Activity: 負責fragment的創建和presenter的初始化。
Fragment:繼承了Contract.view接口,MVP模式中View的存在。
Presenter:繼承了Contract.presenter接口,MVP模式中presenter的存在。
至於Model層是在MVP中操作的數據的部分,這裏主要集中在data包中。

下面選取一個比較有代表性的包進行具體的MVP邏輯分析:
addedittaskactivity包:
首先我們先來分析接口類AddEditTaskContract:
/**
 * This specifies the contract between the view and the presenter.
 */
public interface AddEditTaskContract {

    interface View extends BaseView<Presenter> {

        void showEmptyTaskError();

        void showTasksList();

        void setTitle(String title);

        void setDescription(String description);

        boolean isActive();
    }

    interface Presenter extends BasePresenter {

        void saveTask(String title, String description);

        void populateTask();
    }
}

可以看到其中定義了View和Presenter層的接口,其中View接口只有基本的顯示與設置方法,改變UI的顯示文字,還有一個isActive方法判斷Fragment是否已經被添加到主Activity中。Presenter接口有兩個方法,一個saveTask負責儲存數據,一個populateTask從本地數據源獲取任務。

Activity:AddEditTaskActivity
/**
 * Displays an add or edit task screen.
 */
public class AddEditTaskActivity extends AppCompatActivity {

    public static final int REQUEST_ADD_TASK = 1;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.addtask_act);

        // Set up the toolbar.
        Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
        setSupportActionBar(toolbar);
        ActionBar actionBar = getSupportActionBar();
        actionBar.setDisplayHomeAsUpEnabled(true);
        actionBar.setDisplayShowHomeEnabled(true);

        AddEditTaskFragment addEditTaskFragment =
                (AddEditTaskFragment) getSupportFragmentManager().findFragmentById(R.id.contentFrame);

        String taskId = null;
        if (addEditTaskFragment == null) {
            addEditTaskFragment = AddEditTaskFragment.newInstance();

            if (getIntent().hasExtra(AddEditTaskFragment.ARGUMENT_EDIT_TASK_ID)) {
                taskId = getIntent().getStringExtra(
                        AddEditTaskFragment.ARGUMENT_EDIT_TASK_ID);
                actionBar.setTitle(R.string.edit_task);
                Bundle bundle = new Bundle();
                bundle.putString(AddEditTaskFragment.ARGUMENT_EDIT_TASK_ID, taskId);
                addEditTaskFragment.setArguments(bundle);
            } else {
                actionBar.setTitle(R.string.add_task);
            }

            ActivityUtils.addFragmentToActivity(getSupportFragmentManager(),
                    addEditTaskFragment, R.id.contentFrame);
        }

        // Create the presenter
        new AddEditTaskPresenter(
                taskId,
                Injection.provideTasksRepository(getApplicationContext()),
                addEditTaskFragment);
    }

    @Override
    public boolean onSupportNavigateUp() {
        onBackPressed();
        return true;
    }

    @VisibleForTesting
    public IdlingResource getCountingIdlingResource() {
        return EspressoIdlingResource.getIdlingResource();
    }
}
根據傳來的參數判斷是添加還是編輯,並初始化presenter(39-42)與view(16-18)。
Fragment:AddEditTaskFragment
/**
 * Main UI for the add task screen. Users can enter a task title and description.
 */
public class AddEditTaskFragment extends Fragment implements AddEditTaskContract.View {

    public static final String ARGUMENT_EDIT_TASK_ID = "EDIT_TASK_ID";

    private AddEditTaskContract.Presenter mPresenter;

    private TextView mTitle;

    private TextView mDescription;

    public static AddEditTaskFragment newInstance() {
        return new AddEditTaskFragment();
    }

    public AddEditTaskFragment() {
        // Required empty public constructor
    }

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

    @Override
    public void setPresenter(@NonNull AddEditTaskContract.Presenter presenter) {
        mPresenter = checkNotNull(presenter);
    }

    @Override
    public void onActivityCreated(Bundle savedInstanceState) {
        super.onActivityCreated(savedInstanceState);

        FloatingActionButton fab =
                (FloatingActionButton) getActivity().findViewById(R.id.fab_edit_task_done);
        fab.setImageResource(R.drawable.ic_done);
        fab.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                mPresenter.saveTask(mTitle.getText().toString(), mDescription.getText().toString());
            }
        });
    }

    @Nullable
    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container,
                             Bundle savedInstanceState) {
        View root = inflater.inflate(R.layout.addtask_frag, container, false);
        mTitle = (TextView) root.findViewById(R.id.add_task_title);
        mDescription = (TextView) root.findViewById(R.id.add_task_description);

        setHasOptionsMenu(true);
        setRetainInstance(true);
        return root;
    }

    @Override
    public void showEmptyTaskError() {
        Snackbar.make(mTitle, getString(R.string.empty_task_message), Snackbar.LENGTH_LONG).show();
    }

    @Override
    public void showTasksList() {
        getActivity().setResult(Activity.RESULT_OK);
        getActivity().finish();
    }

    @Override
    public void setTitle(String title) {
        mTitle.setText(title);
    }

    @Override
    public void setDescription(String description) {
        mDescription.setText(description);
    }

    @Override
    public boolean isActive() {
        return isAdded();
    }
}
其中包含一個presenter的回調接口實例,用於響應回調,並更新回調presenter進行操作時帶來的視圖改變,代碼很簡單。
presenter:AddEditTaskPresenter
/**
 * Listens to user actions from the UI ({@link AddEditTaskFragment}), retrieves the data and updates
 * the UI as required.
 */
public class AddEditTaskPresenter implements AddEditTaskContract.Presenter,
        TasksDataSource.GetTaskCallback {

    @NonNull
    private final TasksDataSource mTasksRepository;

    @NonNull
    private final AddEditTaskContract.View mAddTaskView;

    @Nullable
    private String mTaskId;

    /**
     * Creates a presenter for the add/edit view.
     *
     * @param taskId ID of the task to edit or null for a new task
     * @param tasksRepository a repository of data for tasks
     * @param addTaskView the add/edit view
     */
    public AddEditTaskPresenter(@Nullable String taskId, @NonNull TasksDataSource tasksRepository,
            @NonNull AddEditTaskContract.View addTaskView) {
        mTaskId = taskId;
        mTasksRepository = checkNotNull(tasksRepository);
        mAddTaskView = checkNotNull(addTaskView);

        mAddTaskView.setPresenter(this);
    }

    @Override
    public void start() {
        if (!isNewTask()) {
            populateTask();
        }
    }

    @Override
    public void saveTask(String title, String description) {
        if (isNewTask()) {
            createTask(title, description);
        } else {
            updateTask(title, description);
        }
    }

    @Override
    public void populateTask() {
        if (isNewTask()) {
            throw new RuntimeException("populateTask() was called but task is new.");
        }
        mTasksRepository.getTask(mTaskId, this);
    }

    @Override
    public void onTaskLoaded(Task task) {
        // The view may not be able to handle UI updates anymore
        if (mAddTaskView.isActive()) {
            mAddTaskView.setTitle(task.getTitle());
            mAddTaskView.setDescription(task.getDescription());
        }
    }

    @Override
    public void onDataNotAvailable() {
        // The view may not be able to handle UI updates anymore
        if (mAddTaskView.isActive()) {
            mAddTaskView.showEmptyTaskError();
        }
    }

    private boolean isNewTask() {
        return mTaskId == null;
    }

    private void createTask(String title, String description) {
        Task newTask = new Task(title, description);
        if (newTask.isEmpty()) {
            mAddTaskView.showEmptyTaskError();
        } else {
            mTasksRepository.saveTask(newTask);
            mAddTaskView.showTasksList();
        }
    }

    private void updateTask(String title, String description) {
        if (isNewTask()) {
            throw new RuntimeException("updateTask() was called but task is new.");
        }
        mTasksRepository.saveTask(new Task(title, description, mTaskId));
        mAddTaskView.showTasksList(); // After an edit, go back to the list.
    }
}
可以看到presenter體現了其控制器的核心作用,所有的數據交互與視圖改變都在這裏得到體現,在以前的MVC模式中這些代碼一般都嵌套在Activity的各個角落,然而使用MVP模式使其完全抽離了出來,這纔是MVP模式最寶貴的思想精華。



********************************************************************************
最近發生了很多事情,有開心的事有不開心的事。
但是無論如何那有怎麼樣呢,生活還是要繼續,人還是要向前看。
現在還遠遠不夠的,我還是要努力,變得更強~





發佈了70 篇原創文章 · 獲贊 78 · 訪問量 27萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章