安卓中的Model-View-Presenter模式介紹


轉載自:http://www.jcodecraeer.com/a/anzhuokaifa/androidkaifa/2015/0425/2782.html

英文原文:Introduction to Model-View-Presenter on Android 

轉載此譯文需注明出處: http://jcodecraeer.com/a/anzhuokaifa/androidkaifa/2015/0425/2782.html 

這是一篇觀點比較激進的文章,完全否定了MVC模式在安卓開發的中的意義,認為其是沒有任何用處的。這篇文章因為使用了靜態變量來定義Presenter,因此在原文的評論部分也受到一些爭議。不過我覺得還是從中學到了一些思想。我甚至同意MVC模式在安卓開發的中毫無意義的說法。沒有完美的文章。另外關於MVP,還看到了一篇思路更清晰的文章,也准備翻譯出來。

 

 

這是一篇安卓中MVP模式的詳細教程,從最簡單的例子到最佳實踐。本文還介紹了一個讓在安卓中使用MVP模式變得非常簡單的library。

它是不是很簡單,我們如何才能從中獲益?

什么是MVP

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

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

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

MVP是一個將后台任務和activities/views/fragment分離的方法,讓它們獨立於絕大多數跟生命周期相關的事件。這樣應用就會變得更簡單,整個應用的穩定性提高10倍以上,代碼也變得更短,可維護性增強,程序員也不會過勞死了~~。

為什么要在安卓上使用MVP

原因之一: 盡量簡單

如果你還沒有閱讀過這篇文章,閱讀它: Kiss原則。- kiss是Keep It Stupid Simple或者Keep It Simple, Stupid的縮寫。

 .絕大多數的安卓程序都只使用了View-Model架構。

 .程序員被絞盡了復雜的界面開發中,而不是解決事務邏輯。

在應用中使用Model-View的壞處是“每個東西之間都是相互關聯的”如下圖:

如果上面的圖解看起來還不夠復雜,那么想想這些情況:每個view可能在任意的時間出現或者消失,view數據需要保存與恢復,在臨時的view上掛載一個后台任務。

而與“每個東西之間都是相互關聯的”的相反選擇是使用一個萬能對象(god object)。注:god object是指一個對象/例程在系統中做了太多的事情,或者說是有太多不怎么相關的事情放在一個對象/例程里面來完成。

 

god object過於復雜,他的不同部分無法重用、測試,無法輕易的debug和重構。

 

使用MVP

.復雜的任務被分割成簡單的任務。

.更小的對象,更少的bug。

.更好測試

MVP的view層變得如此簡單,在請求數據的時候甚至不需要使用回調。view的邏輯變得非常直接。

原因之二: 后台任務

當你需要寫一個Activity,Fragment或者一個自定義View的時候,你可以將所有和后台任務相關的方法放在一個外部的或者靜態的類中。這樣你的后台任務就不會再與Activity相關聯,不會在泄漏內存同時也不會依賴於Activity的重建。我們稱這樣的一個類為“Presenter”。注:要理解此話的含義最好先看懂第一個MVP示例的代碼。

雖然有一些方法可以解決后台任務的問題,但是沒有一種和MVP一樣可靠。

為什么這是可行的

下面的圖解顯示了在configuration改變或者發生out-of-memory事件的情況下應用的不同部分所發生的事情。每一個開發者都應該知道這些數據,但是這些數據並不好發現。

  1.                                           |    Case 1     |   Case 2     |    Case 3
  2.                                           |A configuration| An activity  |  A process
  3.                                           |   change      |   restart    |   restart
  4.  ---------------------------------------- | ------------- | ------------ | ------------
  5.  Dialog                                   |     reset     |    reset     |    reset
  6.  Activity, View, Fragment                 | save/restore  | save/restore | save/restore
  7.  Fragment with setRetainInstance(true)    |   no change   | save/restore | save/restore
  8.  Static variables and threads             |   no change   |   no change  |    reset

情景1:configuration的改變通常發生在旋轉屏幕,修改語言設置,鏈接外部的模擬器等情況下。要知道更多的configuration change事件請閱讀:configChanges

情景2:Activity的重啟發生在當用戶在開發者選項中選中了“Don't keep activities”(“中文下為 不保留活動”)的復選框,然后另一個Activity在最頂上的時候。

情景3:進程的重啟發生在應用運行在后台,但是這個時候內存不夠的情況下。

結論

現在你可以發現,一個擁有setRetainInstance(true)的Fragment並沒有帶來幫助 - 我們還是要保存和/恢復這種fragment的狀態。因此我們可以去掉可保持Fragment的情景,把問題簡單化。Occam's razor.

  1.                                           |A configuration|
  2.                                           |   change,     |
  3.                                           | An activity   |  A process
  4.                                           |   restart     |   restart
  5.  ---------------------------------------- | ------------- | -------------
  6.  Activity, View, Fragment, DialogFragment | save/restore  | save/restore
  7.  Static variables and threads             |   no change   |    reset

現在看起來就好多了。我們只需要寫兩部分代碼來實現任意情況下完全恢復應用的狀態:

.保存/恢復Activity, View, Fragment, DialogFragment;

.在進程重啟的情況下重新開啟后台請求。

第一部分我們可以通過常規的Android API方式來實現,第二部分就是Presenter的工作了。Presenter可以記住哪個請求應該被執行,並且在執行期間如果進程重啟,Presenter可以重新執行這些請求。

一個簡單的例子 (未使用MVP)

這個例子將從遠程服務器中加載與顯示一些item元素(就是顯示在ListView中的意思)。如果遇到錯誤會顯示一個toast提示。

我推薦使用RxJava 來建立presenter,因為這個庫可以讓數據流的控制更簡單。

我還要感謝那個創立了一個簡單api的小伙伴,我的例子中用到了它:The Internet Chuck Norris Database 。作者的遠程數據就是來自於這個api。貌似是一個提供笑話內容的api。

不使用 MVP 示例 00:

  1. public class MainActivity extends Activity {
  2.     public static final String DEFAULT_NAME = "Chuck Norris";
  3.  
  4.     private ArrayAdapter<ServerAPI.Item> adapter;
  5.     private Subscription subscription;
  6.  
  7.     @Override
  8.     public void onCreate(Bundle savedInstanceState) {
  9.         super.onCreate(savedInstanceState);
  10.         setContentView(R.layout.activity_main);
  11.         ListView listView = (ListView)findViewById(R.id.listView);
  12.         listView.setAdapter(adapter = new ArrayAdapter<>(this, R.layout.item));
  13.         requestItems(DEFAULT_NAME);
  14.     }
  15.  
  16.     @Override
  17.     protected void onDestroy() {
  18.         super.onDestroy();
  19.         unsubscribe();
  20.     }
  21.  
  22.     public void requestItems(String name) {
  23.         unsubscribe();
  24.         subscription = App.getServerAPI()
  25.             .getItems(name.split("\\s+")[0], name.split("\\s+")[1])
  26.             .delay(1, TimeUnit.SECONDS)
  27.             .observeOn(AndroidSchedulers.mainThread())
  28.             .subscribe(new Action1<ServerAPI.Response>() {
  29.                 @Override
  30.                 public void call(ServerAPI.Response response) {
  31.                     onItemsNext(response.items);
  32.                 }
  33.             }, new Action1<Throwable>() {
  34.                 @Override
  35.                 public void call(Throwable error) {
  36.                     onItemsError(error);
  37.                 }
  38.             });
  39.     }
  40.  
  41.     public void onItemsNext(ServerAPI.Item[] items) {
  42.         adapter.clear();
  43.         adapter.addAll(items);
  44.     }
  45.  
  46.     public void onItemsError(Throwable throwable) {
  47.         Toast.makeText(this, throwable.getMessage(), Toast.LENGTH_LONG).show();
  48.     }
  49.  
  50.     private void unsubscribe() {
  51.         if (subscription != null) {
  52.             subscription.unsubscribe();
  53.             subscription = null;
  54.         }
  55.     }
  56. }

注:別被RxJava嚇到,你就當成一般的異步請求就行了。

一個有經驗的開發者應該注意到這個簡單的例子存在很嚴重的問題:

.每次用戶翻轉屏幕的時候都會開始請求 - app做了多余實際需要的請求,並且用戶在旋轉屏幕之后會觀察到一段時間的空白屏幕。

.如果用戶翻轉屏幕的此時很頻繁會導致內存泄漏 - 每次回調都會保存一個對MainActivity的引用,在請求運行的時候這個引用將保存在內存中。這幾乎會必然導致應用因為out-of-memory錯誤或者運行緩慢而崩潰。

譯者注:為什么平時我們沒有發現這樣的問題?因為我們完全不去考慮用戶頻繁旋轉屏幕的情況,我們認為用戶這樣用手機是找虐,還有,絕大多數的中文應用都禁止屏幕旋轉,只有豎屏,因此就避免了這種問題的發生。

使用MVP 示例 01:

  1. public class MainPresenter {
  2.  
  3.     public static final String DEFAULT_NAME = "Chuck Norris";
  4.  
  5.     private ServerAPI.Item[] items;
  6.     private Throwable error;
  7.  
  8.     private MainActivity view;
  9.  
  10.     public MainPresenter() {
  11.         App.getServerAPI()
  12.             .getItems(DEFAULT_NAME.split("\\s+")[0], DEFAULT_NAME.split("\\s+")[1])
  13.             .delay(1, TimeUnit.SECONDS)
  14.             .observeOn(AndroidSchedulers.mainThread())
  15.             .subscribe(new Action1<ServerAPI.Response>() {
  16.                 @Override
  17.                 public void call(ServerAPI.Response response) {
  18.                     items = response.items;
  19.                     publish();
  20.                 }
  21.             }, new Action1<Throwable>() {
  22.                 @Override
  23.                 public void call(Throwable throwable) {
  24.                     error = throwable;
  25.                     publish();
  26.                 }
  27.             });
  28.     }
  29.  
  30.     public void onTakeView(MainActivity view) {
  31.         this.view = view;
  32.         publish();
  33.     }
  34.  
  35.     private void publish() {
  36.         if (view != null) {
  37.             if (items != null)
  38.                 view.onItemsNext(items);
  39.             else if (error != null)
  40.                 view.onItemsError(error);
  41.         }
  42.     }
  43. }

嚴格意義上來說MainPresenter有三個事件:onNext, onError, onTakeView(onNext指代view.onItemsNext,同理onError指代view.onItemsError)。這三個事件在publish()方法中結合到了一起。onNext和onError的值被發布給了onTakeView()方法提供的MainActivity的實例。

  1. public class MainActivity extends Activity {
  2.  
  3.     private ArrayAdapter<ServerAPI.Item> adapter;
  4.  
  5.     private static MainPresenter presenter;
  6.  
  7.     @Override
  8.     public void onCreate(Bundle savedInstanceState) {
  9.         super.onCreate(savedInstanceState);
  10.         setContentView(R.layout.activity_main);
  11.  
  12.         ListView listView = (ListView)findViewById(R.id.listView);
  13.         listView.setAdapter(adapter = new ArrayAdapter<>(this, R.layout.item));
  14.  
  15.         if (presenter == null)
  16.             presenter = new MainPresenter();
  17.         presenter.onTakeView(this);
  18.     }
  19.  
  20.     @Override
  21.     protected void onDestroy() {
  22.         super.onDestroy();
  23.         presenter.onTakeView(null);
  24.         if (isFinishing())
  25.             presenter = null;
  26.     }
  27.  
  28.     public void onItemsNext(ServerAPI.Item[] items) {
  29.         adapter.clear();
  30.         adapter.addAll(items);
  31.     }
  32.  
  33.     public void onItemsError(Throwable throwable) {
  34.         Toast.makeText(this, throwable.getMessage(), Toast.LENGTH_LONG).show();
  35.     }
  36. }

MainActivity創建MainPresenter,並讓它在onCreate/onDestroy的周期之外。MainActivity用靜態變量來引用MainPresenter,因此每次進程因為out-of-memory事件重啟的時候,MainActivity都會檢查presenter是否還在,如果必要再新建一個。是的,使用靜態變量看起來會覺得讓人不舒服,但是稍后我們會告訴你如何好看些:

主要的考慮是:

示例程序不會在每次切換屏幕的時候都開始一個新的請求。

如果進程重啟,示例程序會重新加載數據。

在MainActivity銷毀(destroyed)的時候MainPresenter不會再持有對MainActivity的引用,因此不會在切換屏幕的時候發生內存泄漏,而且沒必要去unsubscribe請求。

Nucleus

Nucleus是我從Mortar庫和 Keep It Stupid Simple  這篇文章得到的靈感而建立的庫。

下面列出其特點:

1.支持在View、Fragment或者Activity的Bundle中保存與恢復Presenter的狀態。Presenter可以將請求參數保存在這個bundle中,在稍后重啟請求。

2.只需一行代碼就能將請求的結果與錯誤信息交給view,你不需要寫什么!= null之類的檢查代碼。

3.presenter允許擁有多個View的實例。不過你不能在用Dagger實例化的presenter中這樣使用。

4.支持只用一行代碼將presenter和view綁定。

5.提供一些現成的基類:NucleusView, NucleusFragment, NucleusSupportFragment, NucleusActivity。你可以將他們的代碼拷貝出來改造出一個自己的類以利用Nucleus的presenter。

6.支持在進程重啟的時候自動重啟一個請求,以及在銷毀(onDestroy)期間自動取消RxJava的訂閱。

7.最后,它非常簡單,任何一個開發者都能理解。只有Presenter的驅動只有180行代碼,而對於RxJava的支持只有230行代碼。

 Nucleus的例子 example 02

  1. public class MainPresenter extends RxPresenter<MainActivity> {
  2.  
  3.     public static final String DEFAULT_NAME = "Chuck Norris";
  4.  
  5.     @Override
  6.     protected void onCreate(Bundle savedState) {
  7.         super.onCreate(savedState);
  8.  
  9.         App.getServerAPI()
  10.             .getItems(DEFAULT_NAME.split("\\s+")[0], DEFAULT_NAME.split("\\s+")[1])
  11.             .delay(1, TimeUnit.SECONDS)
  12.             .observeOn(AndroidSchedulers.mainThread())
  13.             .compose(this.<ServerAPI.Response>deliverLatestCache())
  14.             .subscribe(new Action1<ServerAPI.Response>() {
  15.                 @Override
  16.                 public void call(ServerAPI.Response response) {
  17.                     getView().onItemsNext(response.items);
  18.                 }
  19.             }, new Action1<Throwable>() {
  20.                 @Override
  21.                 public void call(Throwable throwable) {
  22.                     getView().onItemsError(throwable);
  23.                 }
  24.             });
  25.     }
  26. }
  27.  
  28. @RequiresPresenter(MainPresenter.class)
  29. public class MainActivity extends NucleusActivity<MainPresenter> {
  30.  
  31.     private ArrayAdapter<ServerAPI.Item> adapter;
  32.  
  33.     @Override
  34.     public void onCreate(Bundle savedInstanceState) {
  35.         super.onCreate(savedInstanceState);
  36.         setContentView(R.layout.activity_main);
  37.  
  38.         ListView listView = (ListView)findViewById(R.id.listView);
  39.         listView.setAdapter(adapter = new ArrayAdapter<>(this, R.layout.item));
  40.     }
  41.  
  42.     public void onItemsNext(ServerAPI.Item[] items) {
  43.         adapter.clear();
  44.         adapter.addAll(items);
  45.     }
  46.  
  47.     public void onItemsError(Throwable throwable) {
  48.         Toast.makeText(this, throwable.getMessage(), Toast.LENGTH_LONG).show();
  49.     }
  50. }

就如你看到的那樣,這個例子比前面的例子要簡短多了。Nucleus可以創建/銷毀/保存 presenter,附加或者解除和一個view的關系,並且自動向附加的view發送請求。

MainPresenter的代碼變短是因為我們使用了deliverLatestCache()操作將數據源發出的所有數據與錯誤信息延遲到了view可用之后。它還能將數據緩存到內存中,因此可以在onfiguration change的時候重用。

警告!這里有一個注解!在安卓的世界里,如果你使用了注解,最好檢查一下它是否會影響性能。

MainActivity的代碼變簡單了是因為presenter的創建是由NucleusActivity管理的。你只需要寫上@RequiresPresenter(MainPresenter.class) 就能綁定presenter。我在Galaxy S(2010年的設備)上的檢測結果顯示,注解在這里

只花費了不到0.3ms。只在實例化view的時候才會發生,因此注解在這里對性能的影響可以忽略。

更多示例

帶有保持請求參數的拓展示例在這里:Nucleus Example.

帶有單元測試的例子:Nucleus Example With Tests

deliverLatestCache() 方法

這個RxPresenter的工具方法有三個變種:

  • deliver() will just delay all onNext, onError and onComplete emissions until a View becomes available. Use it for cases when you're doing a one-time request, like logging in to a web service. Javadoc

  • deliverLatest() will drop the older onNext value if a new onNext value is available. If you have an updatable source of data this will allow you to not accumulate data that is not necessary. Javadoc

  • deliverLatestCache() is the same as deliverLatest() but in addition it will keep the latest result in memory and will re-deliver it when another instance of a view becomes available (i.e. on configuration change). If you don't want to organize save/restore of a request result in your view (in case if a result is big or it can not be easily saved into Bundle) this method will allow you to make user experience better. Javadoc

     

 

 

Presenter的生命周期

Presenter的生命周期要比安卓組建的生命周期簡短得多

  • void onCreate(Bundle savedState) - 在Presenter創建的時候調用 Javadoc

  • void onDestroy() - 在用戶離開一個view的時候調用 Javadoc

  • void onSave(Bundle state) - 在View的onSaveInstanceState同時也是Presenter的狀態保持的時候被調用 Javadoc

  • void onTakeView(ViewType view) -  在Activity或者Fragment的Resume()或者android.view.View#onAttachedToWindow()的時候調用. Javadoc

  • void onDropView() - 在Activity或者Fragment的onPause()或者android.view.View#onDetachedFromWindow()的時候調用. Javadoc

View的生命周期與view棧 

通常來說你的view(比如fragment或者自定義的view)在用戶的交互過程中掛載與解掛(attached and detached)都是隨機發生的。 這倒是不讓presenter在view每次解掛(detached)的時候都銷毀的一個啟發。你可以在任何時候掛載與解掛view,但是presenter可以在這些行為中幸存下來,繼續后台的工作。

關於view的周期有一個問題:fragment會因為configuration change或者從棧中去掉而不知道自己是否被解掛(detached)。

Nucleus view默認:只有在activity結束的時候,在view的onDetachedFromWindow()/onDestroy()期間才會銷毀presenter。

因此,如果你要在Activity正常的生命期間銷毀一個view,你必須向view發出presenter也必須銷毀的信號。通過公共方法NucleusLayout.destroyPresenter()和NucleusFragment.destroyPresenter()來做這個事情。

比如,下面是fragment manager的pop()操作在我的一個項目里是如何工作的:

  1.     fragment = fragmentManager.findFragmentById(R.id.fragmentStackContainer);
  2.     fragmentManager.popBackStackImmediate();
  3.     if (fragment instanceof NucleusFragment)
  4.         ((NucleusFragment)fragment).destroyPresenter();

同樣在fragment的replace操作中也要做相同的事情,在最底部的fragment銷毀的時候也要如此。

你可能會決定在每次view從Activity解掛的時候都銷毀presenter來避免這樣的問題,但是如果這樣的話,在view銷毀的時候你無法繼續后台任務。所以這一節的 "view recycling"完全留你你自己考慮,也許有一天我會找到更好的解決辦法,如果你有一個辦法,請告訴我。

最佳實踐

在Presenter中保存你的請求參數

規則很簡單:presenter的主要職能是管理請求。因此view不應該去處理或者開始請求。從view的角度來看,后台任務是永不消失的,總是會返回一個結果或者錯誤信息的,不需要任何回調的。

  1. public class MainPresenter extends RxPresenter<MainActivity> {
  2.  
  3.     private String name = DEFAULT_NAME;
  4.  
  5.     @Override
  6.     protected void onCreate(Bundle savedState) {
  7.         super.onCreate(savedState);
  8.         if (savedState != null)
  9.             name = savedState.getString(NAME_KEY);
  10.         ...
  11.  
  12.     @Override
  13.     protected void onSave(@NonNull Bundle state) {
  14.         super.onSave(state);
  15.         state.putString(NAME_KEY, name);
  16.     }

我推薦你使用酷爆了的Icepick 庫。在不使用運行時注解的前提下,它減少了代碼量並且簡化了app的邏輯,所有的事情都在編譯過程中就完成了,是ButterKnife 的好伴侶。

  1. public class MainPresenter extends RxPresenter<MainActivity> {
  2.  
  3.     @Icicle String name = DEFAULT_NAME;
  4.  
  5.     @Override
  6.     protected void onCreate(Bundle savedState) {
  7.         super.onCreate(savedState);
  8.         Icepick.restoreInstanceState(this, savedState);
  9.         ...
  10.  
  11.     @Override
  12.     protected void onSave(@NonNull Bundle state) {
  13.         super.onSave(state);
  14.         Icepick.saveInstanceState(this, state);
  15.     }

如果你有多個請求參數,這個庫可以幫助你節省不少時間。你可以創建一個BasePresenter,然后將Icepick放到里面,所有的子類將自動保存被@Icicle注釋的變量,你再也不需要實現onSave了。這對於Activity和Fragment或者View也同樣適用。

Execute instant queries on the main thread in onTakeView Javadoc

有時候我們的數據查詢量並不大,比如從數據庫中讀取少量的數據。雖然使用Nucleus創建一個可重啟的請求非常簡單,但是你不需要每次都用。如果你在fragment創建的時候初始化一個后台請求,即使只有幾毫秒,用戶也會看到一會兒的空白屏。因此為了代碼的簡潔,也為了用戶的感受,使用主線程來初始化。

不要讓Presenter控制View 

這種情況不好工作 - application的邏輯因為使用了不自然的方式變得非常復雜。

最自然的方式是用戶的操作流從view,到presenter到model最后到數據。這樣用戶才是控制應用的源頭。對應用的控制應該來源於用戶,而不是應用的內部結構。從view,到presenter到model是很直接的形式,這樣的代碼也很好寫,操作流是這樣的user -> view -> presenter -> model -> data;但是像這樣的操作流:user -> view -> presenter -> view -> presenter -> model -> data,是違背了KISS原則的。

什么?Fragment?不好意思它是違背了這種自然操作流程的。他們太復雜。這里有一篇關於看待fragment的好文章:不提倡 Android Fragment。但是fragment的替代者 Flow 並沒有簡化多少東西。

MVC

如果你熟悉MVC(Model-View-Controller)- 別那樣做。Model-View-Controller和MVP完全不同,也並沒有解決用戶界面開發上的任何問題。

什么是MVC?

  • Model stands here for internal application state. It can or can not be connected with a storage.

  • View is the only thing that is partially common with MVP - it is a part of an application that renders  Model to the screen.

  • Controller represents an input device, such as keyboard, mouse or joystick.

MVC在過去以鍵盤為驅動的應用中(比如游戲),是比較好的模式。沒有窗口和圖形用戶界面的交互-程序接受輸入(Controller),維護狀態(Model),以及顯示輸出(View)。數據與操作類似於:controller -> model -> view.但是這種模式在安卓中完全無用。MVC有太多的困擾。人們認為他們在使用MVC,其實使用的的MVP(web開發者)。許多安卓開發者認為Controller應該是控制view的東西,因此他們將view的邏輯從view中分離,創建一個輕量級的被代理Controller控制的view。我個人是沒有看出這種架構的好處。

 

在數據復雜的項目中使用固定的數據結構 

AutoValue 是做這件事的一個優秀的庫,在他的描述中有其優點的列表,建議閱讀。有安卓的接口AutoParcel。使用固定數據對象的主要原因是你可以四處傳遞,而不用關心是否在程序的某個地方被修改了。而且它們是線程安全的。

結論

試試mvp吧,並告訴你的朋友。

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM