軟件設計模式:基于MVP的Android項目架構

一、概述
每一個 app 的運營都需要經過不斷的迭代與更新!在產品不斷的升級過程中,項目的代碼量會變得越來越大。當采用 Android 原生的架構( Android 項目的架構:當建立起一個新項目時,默認的就像是個 MVC 的架構)去不斷完善、升級項目時。到最后項目就變得越來越臃腫,這時,隨著項目的越來越大,也許不得已需要進行項目的重構,然而這是個工作量很大的任務。所以,做好項目的架構,寫好模型往往是很重要的。

google 官方在 GitHub 上也有著對應的官方規范已完善的開源項目架構(可以直接到這里下載 demo 學習):

  1. todo-mvp/- Basic Model-View-Presenter architecture(基本的 mvp 架構)
  2. todo-mvp-loaders/- Based on todo-mvp, fetches data using Loaders.(基于 todo-mvp ,數據獲取采用 Loaders)
  3. todo-databinding/- Based on todo-mvp, uses the Data Binding Library.(基于 todo-mvp ,使用 databinding 開源庫)
  4. todo-mvp-clean/- Based on todo-mvp, uses concepts from Clean Architecture.(基于 todo-mvp,使用 Clean 架構)
  5. todo-mvp-dagger/- Based on todo-mvp, uses Dagger2 for Dependency Injection(基于 todo-mvp,使用 dagger2 進行依賴注入)
  6. todo-mvp-contentproviders/- Based on todo-mvp-loaders, fetches data using Loaders and uses Content Providers(基于 todo-mvp-loaders ,數據獲取采用 Loaders 和 ContentProviders)
  7. todo-mvp-rxjava/- Based on todo-mvp, uses RxJava for concurrency and data layer abstraction.(基于 todo-mvp ,使用了 Rxjava )

二、Android Architecture Blueprints:todo-mvp 學習
(1)首先,我們來看一下 todo-mvp 的包結構:

包結構
按功能來分包
model層

我們可以看到,在 todo-mvp demo 中,一個包就對應著一個功能模塊。在 tasks 包中:

  1. TasksContract(定義 Presenter、View 的接口)
  2. TasksFragment(實現了 TasksContract.View 接口定義的功能)
  3. TasksPresenter(實現了 TasksContract.Presenter 接口定義的功能) 4. TasksActivity(TasksFragment 的載體)

在 data 包中,則是定義了對應的功能需要的數據實體、model 接口:

  1. 任務實體 Task
  2. model 接口 TasksDataSource
  3. 對應功能的 TasksDataSource實現類

(2)源碼分析

  1. 首先,我們先看一下Presenter、View的兩個基類
public interface BasePresenter { void start();}

start()方法在View界面開始顯示的時候調用,一般是在Activity、Fragment的onStart()方法中

public interface BaseView<T> { void setPresenter(T presenter);}

setPresenter(T presenter)方法在Presenter實例化之后調用,一般是在Presenter構造方法最后調用,傳入自己。

  1. 我們從 TasksActivity 入手,在 onCreate() 方法中,可以看到下面一段代碼
@Override 
protected void onCreate(Bundle savedInstanceState){ super.onCreate(savedInstanceState); 
...略 
        //添加TasksFragment 
        TasksFragment tasksFragment = (TasksFragment)getSupportFragmentManager().findFragmentById(R.id.contentFrame); 
        if (tasksFragment == null) {
                tasksFragment = TasksFragment.newInstance();
                ActivityUtils.addFragmentToActivity( getSupportFragmentManager(), tasksFragment, R.id.contentFrame); 
        } 
        //新建TasksPresenter實例 
        mTasksPresenter = new TasksPresenter(Injection.provideTasksRepository(getApplicationContext()), tasksFragment);
 }

TasksActivity 中,在添加 TasksFragment 后 new 了一個 TasksPresenter,接下來看TasksPresenter的構造方法。

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的構造方法中,TasksPresenter獲得了對mTasksView引用,并且還調用了mTasksView的setPresenter()方法。此時,TasksPresenter就傳遞到了TasksFragment 中,只要在TasksFragment 中對mPresenter 進行賦值,就完成了TasksFragment 與TasksPresenter實例引用的相互持有。

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

到這里你也許會問,你只說了 V 與 P 的聯系,那 M 去哪里了呢?嘿嘿,你在返回去看TasksPresenter的構造方法,這時你是否會發現這一句代碼。

 mTasksRepository = checkNotNull(tasksRepository, "tasksRepository cannot be null");

沒錯,就是在實例化TasksPresenter的同時,將數據處理model TasksRepository 賦予了TasksPresenter。至此,既然知道了各自的引用關系,那View、Presenter、Model的關系也就清楚了。

注意:Presenter的實例化在Activity中,但View對Presenter的引用卻不一定在Activity中。因為View的實現類不一定是Activity,也可能是Fragment。至此,我們可以得出一個結論:Presenter的實例化在Activity中,View對Presenter的引用在View的實現類中。

3.既然清楚了View、Presenter、Model之間的關系,那么我們來看看它們之間到底是怎樣協作的。由于代碼比較多,我們就選擇刷新數據功能來講。

  • View
@Nullable 
@Override 
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
        ...略  
       swipeRefreshLayout.setOnRefreshListener(new SwipeRefreshLayout.OnRefreshListener() { 
       @Override
        public void onRefresh() {
                mPresenter.loadTasks(false); 
               }
        });  
       ...略 
       return root; 
}

在刷新時,調用了 mPresenter.loadTasks(false);可以看到,View將刷新功能交給了Presenter來做。再來看看mPresenter.loadTasks(false)方法 - Presenter

@Override 
public void loadTasks(boolean forceUpdate) {
         loadTasks(forceUpdate || mFirstLoad, true);
         mFirstLoad = false; 
} 
 //私有方法,操作數據 
private void loadTasks(boolean forceUpdate, final boolean showLoadingUI) {
         if (showLoadingUI) { 
                //對mTasksView的選中框進行顯示  
                mTasksView.setLoadingIndicator(true);
         } 
        if (forceUpdate) {
                //mTasksRepository中的數據操作、刷新數據
                mTasksRepository.refreshTasks(); 
        } 
        EspressoIdlingResource.increment(); 
        // App is busy until further notice 
        //mTasksRepository中的回調方法處理返回的數據
        mTasksRepository.getTasks(new  TasksDataSource.LoadTasksCallback() { 
        @Override 
        public void onTasksLoaded(List<Task> tasks) {
         List<Task> tasksToShow = new ArrayList<Task>();
         // 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 filter the tasks based on the requestType
         for (Task task : tasks) { 
         switch (mCurrentFiltering) {
                case ALL_TASKS:
                        tasksToShow.add(task); 
                break; 
                case ACTIVE_TASKS:
                 if (task.isActive()) { 
                         tasksToShow.add(task); 
                }
                break; 
                case COMPLETED_TASKS:
                if (task.isCompleted()) {
                         tasksToShow.add(task);
                } 
                break;
                default: 
                        tasksToShow.add(task); 
                break;
              }
         } 
        // The view may not be able to handle UI updates anymore 
        if (!mTasksView.isActive()) { 
                return;
         }
        if (showLoadingUI) { 
                mTasksView.setLoadingIndicator(false);
        }
           processTasks(tasksToShow); 
        }

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

mPresenter.loadTasks()方法通過調用一個同名私有的重載的方法,調用 mTasksView.setLoadingIndicator(true)顯示是選中框、 mTasksRepository.refreshTasks()刷新數據、 mTasksRepository.getTasks()的回調方法處理返回的數據。

總結:MVP在我看來就是一種“代理模式”。View、Model只需要做好自己的任務,然后,Model處理后的數據交于Presenter,Presenter通過View的引用將處理后的數據進行顯示。最后,還有個契約類,也就是Contract,主要用于管理Presenter與View的接口,方便擴展。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 229,362評論 6 537
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,013評論 3 423
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 177,346評論 0 382
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,421評論 1 316
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,146評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,534評論 1 325
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,585評論 3 444
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,767評論 0 289
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,318評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,074評論 3 356
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,258評論 1 371
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,828評論 5 362
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,486評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,916評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,156評論 1 290
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,993評論 3 395
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,234評論 2 375

推薦閱讀更多精彩內容