MVP在Android平臺上的應用
- 原文鏈接 : Introduction to Model-View-Presenter on Android
- 原文作者 : konmik
- 譯文出自 : 其他 http://konmik.github.io/introduction-to-model-view-presenter-on-android.html
- 譯者 : MiJack
- 校對者: MiJack
- 狀態 : 校對完成 </ul> </div>
- View 層主要是用于展示數據并對用戶行為做出反饋。在Android平臺上,他可以對應為Activity, Fragment,View或者對話框。
- Model 是數據訪問層,往往是數據庫接口或者服務器的API。
- Presenter 層可以想View層提供來自數據訪問層的數據,除此以外,他也會處理一些后臺事務。
- 大部分的安卓應用只使用View-Model結構
- 程序員現在更多的是和復雜的View打交道而不是解決業務邏輯。
-
保存/恢復 for Activity, View, Fragment, DialogFragment;
-
重啟后臺請求由于進程重啟
- 示例程序不會在每次切換屏幕的時候都開始一個新的請求
- 當進程重啟時,示例程序將會重新加載數據。
- 當MainActivity銷毀時,MainPresenter不會持有MainActivity的引用,因此不會在切換屏幕的時候發生內存泄漏,而且沒必要去unsubscribe請求。
-
它支持在View/Fragment/Activity的Bundle中保存/恢復Presenter的狀態,一個Presenter可以保存請求參數,以便之后重啟它們
-
只需要一行代碼,它就可以直接將請求結果或者錯誤反饋給View,所以你不需要寫 != null 之類的核對代碼。
-
它允許你可以有多個持有Presenter的實例。 不過你不能在用 Dagger 實例化的presenter中這樣使用(傳統方法).
-
它可以用一行代碼快速的將View和Presenter綁定。
-
它提供一些現成的基類,例如: NucleusView , NucleusFragment , NucleusSupportFragment , NucleusActivity . 你可以將他們的代碼拷貝出來改造出一個自己的類以利用Nucleus的presenter。
-
支持在進程重啟后,自動重新發起請求,在 onDestroy 方法中,自動的退訂RxJava的訂閱。
-
最后,它簡潔明了,每一個開發者都會理解,Presenter的驅動只用了180行代碼,RxJava用了230行代碼。
-
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
-
deliver() 只是推遲onNext、onError、onComplete的調用,直到視圖有效。使用它,你只需要一次請求,就像發起登陸web服務一樣。 Javadoc
-
deliverLatest() 當有新的的onNext值,將會舍棄原有的值,如果你有可更新的數據源,這將讓你去除那些不需要的數據。 Javadoc
-
deliverLatestCache() ,和 deliverLatest() 一樣,但除了它會在內存中保存最新的結果外,當View的另一個實例可用(例如:在配置更改的時候)時,還是會觸發一次。如果你不想組織請求在你的View中的保存/恢復事務(比方說,結果太大或者不能很容易地保存在Bundle中),這個方法可以讓用戶體驗更好。 Javadoc
-
void onCreate(Bundle savedState) – 每一個Presenter構造時 . Javadoc
-
void onDestroy() – 用戶離開View時調用 . Javadoc
-
void onSave(Bundle state) – 在View的 onSaveInstanceState 方法中調用,用于持有Presenter的狀態. Javadoc
-
void onTakeView(ViewType view) – 在Activity或者Fragment的 onResume() 方法中或者 android.view.View#onAttachedToWindow() 調用. Javadoc
-
void onTakeView(ViewType view) – 在Activity或者Fragment的 onResume() 方法中或者 android.view.View#onAttachedToWindow() 調用. Javadoc
-
void onDropView() – 在Activity或者Fragment的 onPause() 方法中或者 android.view.View#onDetachedFromWindow() 調用. Javadoc
-
Model代表著應用程序的內部狀態。它可以負責存儲,當然也可以不考慮。
-
View是唯一的與MVP相同的部分 – 它用于將模型呈現在屏幕上,應用程序的一部分。
-
Controller表示輸入裝置,如鍵盤,鼠標或操縱桿。
=======
Android平臺上MVP的介紹
這篇文章向你介紹Android平臺上的MVP模式,從一個簡淺的例子開始實踐之路。文章也會介紹一個一個庫讓你在Android平臺上輕松的實現MVP
簡單嗎?我怎么才能從中受益?
什么是MVP?
在Android平臺上,MVP可以將后臺事務從Activity/View/Fragment中分離出來,讓它們獨立于大部分生命周期事件。這樣,一個應用將會變得簡單, 整個應用可靠性可以提高10倍,應用的代碼將會變短, 代碼的可維護性提高,開發者也為此感到高興。
Android為什么需要MVP
理由1:盡量簡單
如果你還有讀過這篇文章,請閱讀它: Kiss原則 (Keep It Stupid Simple)
當你在應用中只使用Model-View時,到最后,你會發現“所有的事物都被連接到一起”。

如果這張圖看上去還不是很復雜,那么請你想象一下以下情況:每一個View在任意一個時刻都有可能出現或者消失。不要忘記View的保存和恢復,在臨時的view上掛載一個后臺任務。
“所有的事物都被連接到一起”的替代品是一個萬能對象(god object)。
god object是十分復雜的,他的每一個部分都不能重復利用,無法輕易的測試、或者調試和重構。
With MVP
使用MVP
復雜的任務被分成細小的任務,并且很容易解決。越小的東西,bug越少,越容易debug,更好測試。在MVP模式下的View層將會變得簡單,所以即便是他請求數據的時候也不需要回調函數。View邏輯變成十分直接。
理由2:后臺任務
當你編寫一個Actviity、Fragment、自定義View的時候,你會把所有的和后臺任務相關的方法寫在一個靜態類或者外部類中。這樣,你的Task不再和Activity聯系在一起,這既不會導致內存泄露,也不依賴于Activity的重建。
這里有若干種方法處理后臺任務,但是它們的可靠性都不及MVP。
為什么它是可行的?
這里有一張表格,用于展示在configuration改變、Activity 重啟、Out-Of-Memory時,不同的應用部分會發生什么?
情景 1 | 情景 2 | 情景 3 | |
---|---|---|---|
配置改變 | Activity 重啟 | 進程重啟 | |
對話框 | 重置 | 重置 | 重置 |
Activity, View, Fragment | 保存/恢復 | 保存/恢復 | 保存/恢復 |
Fragment with setRetainInstance(true) | 無變化 | 保存/恢復 | 保存/恢復 |
Static variables and threads | 無變化 | 無變化 | 重置 |
情景 1: 當用戶切換屏幕、更改語言設置或者鏈接外部的模擬器時,往往意味著設置改變。 相關更多請閱讀 這里 。
情景 2:Activity的重啟發生在當用戶在開發者選項中選中了“Don’t keep activities”(“中文下為 不保留活動”)的復選框,然后另一個Activity在最頂上的時候。
情景 3: 進程的重啟發生在應用運行在后臺,但是這個時候內存不夠的情況下。
總結
現在你可以發現,一個調用了setRetainInstance(true)的Fragment也不奏效,我們還是需要保存/恢復fragment的狀態,所以為簡化問題,我們暫不考慮上述情況的Fragment。 Occam’s razor
配置改變, Activity重啟 | 進程重啟 | |
---|---|---|
Activity, View, Fragment, DialogFragment | 保存/恢復 | 保存/恢復 |
Static variables and threads | 無變化 | 重置 |
現在,看上去更舒服了,我們只需要寫兩段代碼為了恢復應用:
第一個部分,用Android的API可以實現。第二個部分,就是Presenter的作用了。Presenter將會記住有哪些請求需要執行,當進程在執行過程中重啟時,Presenter將會出現執行它們。
一個簡單的例子(no MVP)
這個例子用于從遠程服務器加載數據并呈現,當發生異常時,會通過Toast提示。
我推薦使用 RxJava 構建Presenter,因為這個庫更容易控制數據流。
我想對創造如此簡單的API的伙計說聲謝謝,我把它用于 The Internet Chuck Norris Database
無MVP的 例子00 :
<br />public class MainActivity extends Activity { public static final String DEFAULT_NAME = "Chuck Norris"; private ArrayAdapter<ServerAPI.Item> adapter; private Subscription subscription; @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); ListView listView = (ListView)findViewById(R.id.listView); listView.setAdapter(adapter = new ArrayAdapter<>(this, R.layout.item)); requestItems(DEFAULT_NAME); } @Override protected void onDestroy() { super.onDestroy(); unsubscribe(); } public void requestItems(String name) { unsubscribe(); subscription = App.getServerAPI() .getItems(name.split("\\s+")[0], name.split("\\s+")[1]) .delay(1, TimeUnit.SECONDS) .observeOn(AndroidSchedulers.mainThread()) .subscribe(new Action1<ServerAPI.Response>() { @Override public void call(ServerAPI.Response response) { onItemsNext(response.items); } }, new Action1<Throwable>() { @Override public void call(Throwable error) { onItemsError(error); } }); } public void onItemsNext(ServerAPI.Item[] items) { adapter.clear(); adapter.addAll(items); } public void onItemsError(Throwable throwable) { Toast.makeText(this, throwable.getMessage(), Toast.LENGTH_LONG).show(); } private void unsubscribe() { if (subscription != null) { subscription.unsubscribe(); subscription = null; } } }
有經驗的開發者會注意到這個例子有以下不妥:
當用戶翻轉屏幕時候會開始請求,應用發起了過多的請求,將會是屏幕在切換的時候呈現空白的界面。
當用戶頻繁的切換屏幕,這將會造成內存泄露,請求運行時,每一個回調將會持有MainActivity的引用,讓其保存在內存中。因此引起的OOM和應用反應遲緩,會引發應用的Crash。
MVP模式下的 例子 01
public class MainPresenter { public static final String DEFAULT_NAME = "Chuck Norris"; private ServerAPI.Item[] items; private Throwable error; private MainActivity view; public MainPresenter() { App.getServerAPI() .getItems(DEFAULT_NAME.split("\\s+")[0], DEFAULT_NAME.split("\\s+")[1]) .delay(1, TimeUnit.SECONDS) .observeOn(AndroidSchedulers.mainThread()) .subscribe(new Action1<ServerAPI.Response>() { @Override public void call(ServerAPI.Response response) { items = response.items; publish(); } }, new Action1<Throwable>() { @Override public void call(Throwable throwable) { error = throwable; publish(); } }); } public void onTakeView(MainActivity view) { this.view = view; publish(); } private void publish() { if (view != null) { if (items != null) view.onItemsNext(items); else if (error != null) view.onItemsError(error); } } }
從嚴格意義上來說,MainPresenter有三個事件處理線程: onNext , onError , onTakeView 。他們調用了 publish() 方法, onNext 或 onError 的值將會在MainActivity中發布,而不是由onTakeView提供。
public class MainActivity extends Activity { private ArrayAdapter<ServerAPI.Item> adapter; private static MainPresenter presenter; @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); ListView listView = (ListView)findViewById(R.id.listView); listView.setAdapter(adapter = new ArrayAdapter<>(this, R.layout.item)); if (presenter == null) presenter = new MainPresenter(); presenter.onTakeView(this); } @Override protected void onDestroy() { super.onDestroy(); presenter.onTakeView(null); if (isFinishing()) presenter = null; } public void onItemsNext(ServerAPI.Item[] items) { adapter.clear(); adapter.addAll(items); } public void onItemsError(Throwable throwable) { Toast.makeText(this, throwable.getMessage(), Toast.LENGTH_LONG).show(); } }
MainActivty構建了MainPresenter,將其維持在onCreate/onDestroy周期外,MainActivity持有MainPresenter的靜態引用,所以每一個進程由于OOM重啟時,MainActivity可以確認Presenter是否仍然存在,必要時創建。
當然,確認和使用靜態變量可能是代碼變得臃腫,稍后我們會告訴你如何好看些:。:)
重要思路:
Nucleus
Nucleus是我從 Mortar 和 Keep It Stupid Simple 這篇文章得到的靈感而建立的庫。
它有以下特征:
public class MainPresenter extends RxPresenter<MainActivity> { public static final String DEFAULT_NAME = "Chuck Norris"; @Override protected void onCreate(Bundle savedState) { super.onCreate(savedState); App.getServerAPI() .getItems(DEFAULT_NAME.split("\\s+")[0], DEFAULT_NAME.split("\\s+")[1]) .delay(1, TimeUnit.SECONDS) .observeOn(AndroidSchedulers.mainThread()) .compose(this.<ServerAPI.Response>deliverLatestCache()) .subscribe(new Action1<ServerAPI.Response>() { @Override public void call(ServerAPI.Response response) { getView().onItemsNext(response.items); } }, new Action1<Throwable>() { @Override public void call(Throwable throwable) { getView().onItemsError(throwable); } }); } } @RequiresPresenter(MainPresenter.class) public class MainActivity extends NucleusActivity<MainPresenter> { private ArrayAdapter<ServerAPI.Item> adapter; @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); ListView listView = (ListView)findViewById(R.id.listView); listView.setAdapter(adapter = new ArrayAdapter<>(this, R.layout.item)); } public void onItemsNext(ServerAPI.Item[] items) { adapter.clear(); adapter.addAll(items); } public void onItemsError(Throwable throwable) { Toast.makeText(this, throwable.getMessage(), Toast.LENGTH_LONG).show(); } }
正如你看到的,跟上一個代碼相比,這個例子十分簡潔。Nucleus 可以構造/銷毀/變成 Presenter, 向Presenter中添加/分離 View ,并且自動向附加的view發送請求。。
MainPresenter 的代碼比較短,因為它使用 deliverLatestCache() 的操作,延遲了由一個數據源發出所有的數據和錯誤,直到View可用。它還把數據緩存在內存中,以便它可以在Configuration change時可以被重用。
MainActivity 的代碼比較短,因為主Presenter的創作由 NucleusActivity 管理。當你需要綁定一個Presenter的時候,只需要添加注解 @RequiresPresenter(MainPresenter.class) 。
警告!注釋!在Android中,如果你使用注解,這是最好檢查以下這么做會不會降低性能。以我使用的’Galaxy S`(2010年設備)為例,處理此批注耗時不超過0.3毫秒。只在實例化view的時候才會發生,因此注解在這里對性能的影響可以忽略。
更多例子
一個擴展的例子,帶有請求參數的Presenter: Nucleus Example 。
帶有單元測試的例子: Nucleus Example With Tests
deliverLatestCache() 方法
這個RxPresenter的工具方法有三個變種:
Presenter的生命周期
相比Android組件,Presenter的生命周期更加簡短。
View的回收與View棧
通常來說,你的view(比如fragment或者自定義的view)在用戶的交互過程中掛載與解掛(attached and detached)都是隨機發生的。 這倒是不讓presenter在view每次解掛(detached)的時候都銷毀的一個啟發。你可以在任何時候掛載與解掛view,但是presenter可以在這些行為中幸存下來,繼續后臺的工作。
這里還存在著一個關于View回收的問題:一個Fragment在Configuration change或者從stack中彈出的情況下,不知道自身有沒有解掛(detached)。
默認只在Activity處于finish時,才在調用View的 onDetachedFromWindow() / onDestroy() 銷毀Presenter。
所以,當你在常規的Activity生命周期內,銷毀View,你需要給給View一個銷毀Presenter的信號。在這里,公有方法 NucleusLayout.destroyPresenter() and NucleusFragment.destroyPresenter() 就派上用場了。
例如,在我的項目中,下面的是我如何進行FragmentManager的 pop() 操作:
fragment = fragmentManager.findFragmentById(R.id.fragmentStackContainer); fragmentManager.popBackStackImmediate(); if (fragment instanceof NucleusFragment) ((NucleusFragment)fragment).destroyPresenter();
在進行 replace Fragment棧和對處于底部的Fragment進行 push 操作時,你可能需要進行相同的操作。
在View從Activity解掛(detached)時,您可能會選擇摧毀Presenter來避免問題的發生,但是,這將意味著當View解掛(detached)時,后臺任務無法繼續進行。
所以這一節的 “view recycling”完全留你你自己考慮,也許有一天我會找到更好的解決辦法,如果你有一個辦法,請告訴我。
最佳實踐
在Presenter中保存你的請求參數。
規則很簡單:Presenter的主要作用是管理請求。所以,View不應該自己處理或者重啟請求。從View中,我們可以看見,后臺事務不會消失,總是會返回結果或者錯誤, 而不是通過回調的方式 。
<br />public class MainPresenter extends RxPresenter<MainActivity> { private String name = DEFAULT_NAME; @Override protected void onCreate(Bundle savedState) { super.onCreate(savedState); if (savedState != null) name = savedState.getString(NAME_KEY); ... @Override protected void onSave(@NonNull Bundle state) { super.onSave(state); state.putString(NAME_KEY, name); } }
我推薦使用一個很棒的庫 Icepick 。在不使用運行時注解的前提下,它可以減少代碼量,并簡化應用程序邏輯 – 所有的事都在編譯過程中已經處理好了。這個庫和 ButterKnife 搭配是個不錯的選擇。
public class MainPresenter extends RxPresenter<MainActivity> { @Icicle String name = DEFAULT_NAME; @Override protected void onCreate(Bundle savedState) { super.onCreate(savedState); Icepick.restoreInstanceState(this, savedState); ... } @Override protected void onSave(@NonNull Bundle state) { super.onSave(state); Icepick.saveInstanceState(this, state); } }
如果你有不止一對請求參數,這個庫在不使用運行時注解的前提下。您可以創建 BasePresenter 并把 Icepick 到該類中,所有的子類將會自動保存標有 @Icicle 這一注解的變量,而你將不再需要去實現 OnSave 。這也適用于保存Activity,Fragment,View的狀態。
在主線程中調用 onTakeView 進行即時查詢 Javadoc
有時候,你要進行少量的數據查詢,如從數據庫中讀取少量的數據。雖然你可以很容易地用Nucleus創建一個可重啟的請求,你不必到處使用這個強大的工具。如果你在fragment創建的時候初始化一個后臺請求,即使只有幾毫秒,用戶也會看到一會兒的空白屏。因此,為了使代碼更短,用戶體驗更好,可以使用主線程。
不要讓Presenter控制你的View
這不是很好的工作方式 – 由于這種不自然的方式,應用程序邏輯變得太復雜。
自然的方式是操作流由用戶發起,通過View,Presenter和Model,最后流向數據。畢竟,用戶將使用應用,用戶是控制應用程序的源頭。因此,控制應該從用戶開始而不是一些應用的內部結構。
從view,到presenter到model是很直接的形式,很容易書寫這樣的代碼。你將得到以下序列: user -> view -> presenter -> model -> data 。但是,當控制流變成這樣時: user -> view -> presenter -> view -> presenter -> model -> data ,它只是違反KISS原則.
Fragments?不好意思它是違背了這種自然操作流程的。它們太復雜。這里是一個非常好講訴Fragment的文章: 抨擊Android的Fragment 。fragment的替代者 Flow 并沒有簡化多少東西。
MVC
如果你對MVC(模型-View-控制器)-不要去使用。模型-View-控制器和MVP完全不同,不能解決接口開發者面對的問題。
What is MVC?
什么是MVC?
MVC在過去以鍵盤為驅動的應用中(比如游戲),是比較好的模式。沒有窗口和圖形用戶界面的交互——應用接受輸入(Controller),維持狀態(Model),產生輸出(View)。同樣,數據和控制的關系是這樣的。 controller -> model -> view 。這種模式是在Android絕對無用。
這里有一些關于MVC的困惑。人們(Web開發人員)覺得他們使用MVC,而實際上,他們使用的MVP。許多Android開發者認為Controller是用于控制View的,所以他們試圖在創建View時,從視圖(View)中提取視圖邏輯,交由專門的控制器控制。我個人是沒有看出這種架構的好處。
在數據復雜的項目中使用固定的數據結構
在這方面, AutoValue 是十分好的庫,在它的描述中,你會發現一大堆好處,我建議你閱讀它。Android平臺上還有一個接口: AutoParcel 。其主要原因是,你可以四處傳遞,而不用關心是否在程序的某個地方被修改了。而且他們也是線程安全的。
總結
試試MVP吧,然后告訴你的朋友。:)