原文出處----->[Introduction to Model View Presenter on Android](http://konmik.com/post/introduction_to_model_view_presenter_on_android/),作者[GitHub地址](https://github.com/konmik)
### 譯文如下:
本文是從最簡單的例子到最佳實踐,分步介紹Android上的MVP。文章還介紹了一個新的庫,使得Android上的MVP非常簡單。
**這很簡單嗎?我怎樣才能使用它?**
### 什么是MVP
* **視圖**(View)是顯示數據并對用戶操作做出反應的圖層。在Android上,這可能是一個Activity,一個片段,一個android.view.View或一個對話框。
* **模型**(Model)是一個數據訪問層,如數據庫API或遠程服務器API。
* **Presenter**是一個為View提供來自Model的數據的圖層。演示者還處理后臺任務。
在Android MVP上,將背景任務與活動/視圖/片段分開,使其獨立于大多數與生命周期相關的事件。這樣一個應用程序變得更簡單,整體應用程序可靠性提高達10倍,應用程序代碼變得更短,代碼可維護性變得更好,開發人員的生活變得更加快樂。
### 為什么在Android上MVP
#### 原因1:盡量簡單
如果你還沒有閱讀這篇文章,那么做:[親吻原則](http://web.archive.org/web/20160206225831/https://people.apache.org/~fhanik/kiss.html)
* 大多數現代Android應用程序只使用View-Model架構。
* 程序員參與了View的復雜性而不是解決業務任務。
在你的應用程序中只使用模型視圖,你通常會得到“一切都與一切有關”。

如果這個圖看起來不復雜,那么想想每個View都會隨機消失并出現。不要忘記保存/恢復視圖。附加幾個后臺任務到臨時視圖,蛋糕準備就緒!
“一切與萬物有關”的另一種選擇是上帝的對象。

神的目標過于復雜; 其部分不能重復使用,測試或輕松調試和重構。
與MVP

* 復雜的任務分解成更簡單的任務,更容易解決。
* 更小的對象,更少的錯誤,更容易調試。
* 可測試。
MVP視圖層變得如此簡單,所以在請求數據時甚至不需要回調。查看邏輯變得非常線性。
#### 原因2:后臺任務
每當你編寫一個活動,一個片段或一個自定義視圖,你可以把所有與后臺任務連接的方法放到不同的外部或靜態類中。這樣你的后臺任務將不會與Activity連接,不會泄漏內存,也不會依賴Activity的重新創建。我們稱這個對象為“演示者”。
有幾種不同的方法來處理后臺任務,一個正確實施的MVP庫是最可靠的。
**為什么這個工作**
下面是一個小圖,顯示配置更改期間或內存不足事件期間不同應用程序部件發生的情況。每個Android開發人員都應該知道這些數據,但是這個數據卻很難找到。
~~~
| Case 1 | Case 2 | Case 3
|A configuration| An activity | A process
| change | restart | restart
---------------------------------------- | ------------- | ------------ | ------------
Dialog | reset | reset | reset
Activity, View, Fragment | save/restore | save/restore | save/restore
Fragment with setRetainInstance(true) | no change | save/restore | save/restore
Static variables and threads | no change | no change | reset
~~~
**案例1**:配置更改通常發生在用戶翻轉屏幕,更改語言設置,連接外部監視器等情況下。有關此事件的更多信息,請參閱:[configChanges](http://developer.android.com/reference/android/R.attr.html#configChanges)。
**案例2**:當用戶在開發者設置中設置了“不要保留活動”復選框,并且另一個活動變為最上層時,會發生活動重新啟動。
**情況3**:如果沒有足夠的內存并且應用程序在后臺,則進程重新啟動。
**結論**
現在你可以看到,帶有setRetainInstance的片段(true)在這里沒有幫助 - 我們需要保存/恢復這個片段的狀態。所以我們可以簡單地扔掉殘留的碎片來限制問題的數量。
~~~
|A configuration|
| change, |
| An activity | A process
| restart | restart
---------------------------------------- | ------------- | -------------
Activity, View, Fragment, DialogFragment | save/restore | save/restore
Static variables and threads | no change | reset
~~~
現在看起來好多了。我們只需要編寫兩段代碼就可以在任何可能的情況下完全恢復應用程序:
* 保存/恢復Activity,View,Fragment,DialogFragment;
* 在進程重啟的情況下重啟后臺請求。
第一部分可以通過Android API的常規手段完成。第二部分是Presenter的工作。Presenter只記得它應該執行的請求,如果一個進程在執行期間重新啟動,Presenter將再次執行它們。
### 一個簡單的例子
這個例子將加載并顯示來自遠程服務器的一些項目。如果發生錯誤,會顯示一點吐司。
我建議使用[RxJava](https://github.com/ReactiveX/RxJava)來建立主持人,因為這個庫允許輕松地控制數據流。
我想感謝那個創造了一個簡單的API的人,我用我的例子:[Internet Chuck Norris Database](http://www.icndb.com/)
**沒有MVP** [例子00](https://github.com/konmik/MVPExamples/tree/master/example00):
~~~
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的引用,并在請求運行時將其保留在內存中。由于內存不足錯誤或重要的應用程序減速,絕對有可能導致應用程序崩潰。
**用MVP** [例子01](https://github.com/konmik/MVPExamples/tree/master/example01):
請不要在家里試試這個!:)此示例僅用于演示目的。在現實生活中,你不會使用靜態變量來保持演示者。
~~~
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 (!isChangingConfigurations())
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();
}
}
~~~
MainActivity創建MainPresenter并將其保持在onCreate / onDestroy周期之外。MainActivity使用靜態變量來引用MainPresenter,所以每當進程由于內存不足事件而重新啟動時,MainActivity應檢查演示者是否仍在此處,并在需要時創建它。
是的,這看起來有點臃腫與檢查,并使用靜態變量,但后來我會展示如何使這看起來好多了。:)
主要想法是:
* 每次用戶翻轉屏幕時,示例應用程序都不會啟動請求。
* 如果一個進程已經重新啟動,這個例子再次加載數據。
* MainPresent不保留MainActivity實例的引用,而MainActivity被銷毀,所以在屏幕上沒有內存泄漏,并且不需要取消訂閱請求。
#### 核
Nucleus是我創建的庫,同時受到[Mortar庫](https://github.com/square/mortar)和[Keep It Stupid Simple](https://people.apache.org/~fhanik/kiss.html)文章的啟發。
這里是一個功能列表:
* 它支持在View / Fragment / Activity的狀態包中保存/恢復Presenter的狀態。演示者可以將請求參數保存到該捆綁包中以稍后重新啟動它們。
* 它提供了一個工具,只需一行代碼即可將請求結果和錯誤直接引導到視圖中,因此您不必編寫所有的!= null檢查。
* 它允許您有一個以上需要演示者的視圖實例。如果你用Dagger(傳統的方式)實例化主持人,你不能這樣做。
* 它提供了一個快捷方式,只需一行即可將演示者綁定到視圖。
* 它提供了基本視圖類:NucleusView,NucleusFragment,NucleusSupportFragment,NucleusActivity。您也可以從其中一個復制/粘貼代碼,以使您可以使用任何類來利用Nucleus的演示者。
* 它可以在進程重新啟動后自動重啟請求,并在此期間自動取消訂閱RxJava訂閱onDestroy。
* 最后,這很簡單,任何開發人員都可以理解。(我建議花一些時間潛入RxJava)。只有大約180行代碼來驅動Presenter和230行代碼,用于RxJava支持。
**例如具有**[核](https://github.com/konmik/nucleus) [例02](https://github.com/konmik/MVPExamples/tree/master/example02)
>[info] 注意:自寫這篇文章以來,新版本的Nucleus發布了。您可以在Nucleus項目資源庫中找到更新的例子。
~~~
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創建/銷毀/保存演示者,附加/分離View并自動將請求結果發送到附加視圖。
MainPresenter代碼更短,因為它使用deliverLatestCache()延遲數據源發出的所有數據和錯誤的操作,直到視圖變為可用。它還將數據緩存在內存中,以便在配置更改時重用。
MainActivity由于演示者的創建是由管理的,因此代碼更短NucleusActivity。所有你需要綁定一個演示者是寫@RequiresPresenter(MainPresenter.class)注釋。
警告!一個注釋!在Android世界中,如果使用注釋,最好檢查一下這不會降低性能。我在Galaxy S(2010年設備)上所做的基準測試表明,處理這個注釋需要的時間少于0.3毫秒。這只發生在視圖的實例化過程中,所以注釋被認為是免費的。
**更多的例子**
具有請求參數持久性的擴展示例如下:[Nucleus示例](https://github.com/konmik/nucleus/tree/master/nucleus-example)。
單元測試的一個例子:具有測試的[核心例子](https://github.com/konmik/nucleus/tree/master/nucleus-example-with-tests)
**deliverLatestCache() 方法**
這個RxPresenter幫助方法有三個變種:
1. deliver()將延遲所有onNext,onError和onComplete排放,直到View可用。如果您正在進行一次性請求(如登錄到Web服務),請使用它。
2. deliverLatest()如果新的onNext值可用,則會放棄較舊的onNext值。如果你有一個可更新的數據源,這將允許你不積累不必要的數據。
3. deliverLatestCache()是一樣的,deliverLatest()但它會保持最新的結果在內存中,并將重新交付時,另一個視圖的實例變得可用(即在配置更改)。如果您不希望在視圖中組織保存/恢復請求結果(如果結果很大或者無法輕易保存到Bundle中),此方法將使您可以更好地體驗用戶體驗。
**演講者的生命周**期
Presenter的生命周期比其他Android組件的生命周期短得多。
* void onCreate(Bundle savedState) - 每個演示者的創作都會被調用。
* void onDestroy() - 當用戶View被破壞而不是因為配置改變而被調用。
* void onSave(Bundle state)- 在View中也會調用onSaveInstanceStatePresenter的狀態。
* void onTakeView(ViewType view)- 在活動或片段的onResume()過程中或過程中調用android.view.View#onAttachedToWindow()。
* void onDropView()- 在活動onDestroy()或片段的onDestroyView()過程中或過程中調用android.view.View#onDetachedFromWindow()。
**查看回收和查看堆棧**
通常情況下,您的視圖(即片段和自定義視圖)在用戶交互過程中隨機附加和分離。這可能是一個好主意,不要摧毀一個主持人每次一個視圖分離。你可以隨時分離和附上觀點,主持人將勝過所有這些行動,繼續后臺工作。
#### 最佳做法
**在Presenter中保存您的請求參數**
規則很簡單:主講人的主要目的是管理請求。所以View不應該處理或重新啟動請求本身。從View的角度來看,后臺任務是永不消失的,并且總會返回一個結果或一個沒有任何回調的錯誤。
~~~
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庫](https://github.com/frankiesardo/icepick)。它減少了代碼的大小,簡化了應用程序邏輯,而無需使用運行時注釋 - 編譯過程中所有事情都會發生 這個庫是[黃油刀](http://jakewharton.github.io/butterknife)的好伙伴。
~~~
public class MainPresenter extends RxPresenter<MainActivity> {
@State 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);
}
~~~
如果你有更多的請求參數,這個庫自然會保存生命。您可以創建IcepickBasePresenter并將其 放入該類,并且所有子類都將自動獲得保存其注釋的字段的能力@State,您將永遠不需要onSave再次實施。這也適用于保存活動,片段或視圖的狀態。
#### 在主線程中執行即時查詢 onTakeView
有時候你有一個簡短的數據查詢,比如從數據庫中讀取少量的數據。雖然您可以使用Nucleus輕松創建可重新啟動的請求,但您不必在任何地方使用這個強大的工具。如果在創建片段的過程中啟動后臺請求,則用戶將會看到一個空白屏幕,即使該查詢只需要幾毫秒。所以,為了縮短代碼并讓用戶更快樂,請使用主線程。
#### 不要試圖讓您的演示者控制您的視圖
這樣做效果不好 - 應用程序邏輯變得太復雜了,因為它不自然。
自然的方法是通過視圖,主持人和模型來實現從用戶到數據的控制流。最終用戶將使用應用程序,用戶是應用程序的控制源。所以控制應該從用戶而不是從一些內部的應用程序結構。
當控制從視圖到演示者,然后從演示者到模型,這只是一個直接的流程,很容易寫這樣的代碼。你得到一個簡單的用戶 - >查看 - >演示 - >模型 - >數據序列。但是當控制如下:user - > view - > presenter - > view - > presenter - > model - > data,這只是違反了KISS原則。不要在你的觀點和主持人之間打乒乓球。
### 結論
給MVP一試,告訴朋友。