目錄
1.MVP簡介
2.Mosby簡介
3.Hello MVP World
4.MvpPresenter的基類
5.LCE視圖
6.MvpLceActivity和MvpLceFragment
7.ViewState簡介
8.思考
一.MVP簡介
MVP的出發(fā)點(diǎn)是關(guān)注點(diǎn)分離荐吵,將視圖和業(yè)務(wù)邏輯解耦骑冗。Model-View-Presenter三個(gè)部分可以簡單理解為:
- Model是將在視圖中顯示的數(shù)據(jù)。
- View是顯示數(shù)據(jù)(model)的界面先煎,同時(shí)將用戶指令(事件)發(fā)送給Presenter來處理贼涩。View通常含有Presenter的引用。在Android中Activity榨婆,F(xiàn)ragment和ViewGroup都扮演視圖的角色磁携。
- Presenter是中間人,同時(shí)有兩者的引用良风。請注意單詞model非常有誤導(dǎo)性。它應(yīng)該是獲取或處理model的業(yè)務(wù)邏輯闷供。例如:如果你的數(shù)據(jù)庫表中存儲(chǔ)著User烟央,而你的視圖想顯示用戶列表,那么Presenter將有一個(gè)數(shù)據(jù)庫業(yè)務(wù)邏輯(例如DAO)類的引用歪脏,Presenter通過它來查詢用戶列表疑俭。
思考:MVC,MVP和MVVM之間有什么區(qū)別和聯(lián)系婿失?
- 消極視圖:在MVP中钞艇,View是消極視圖(Passive View)啄寡,也就是說它盡量不去主動(dòng)做事,而是讓Presenter通過抽象方式控制View哩照,例如Presenter調(diào)用view.showLoading()方法來顯示加載效果挺物,但Presenter不應(yīng)該控制View的具體實(shí)現(xiàn),例如動(dòng)畫飘弧,所以Presenter不應(yīng)該調(diào)用view.startAnimation()這樣的方法识藤。
二.Mosby簡介
設(shè)計(jì)目標(biāo):讓你能用清晰的Model-View-Presenter架構(gòu)來構(gòu)建Android app。
注意:Mosby是一個(gè)庫(library)次伶,不是一個(gè)框架(framework)痴昧。
思考:什么是library?什么是framework冠王?它們的區(qū)別是什么赶撰?
Mosby的內(nèi)核是一個(gè)基于委托模式(delegation)的很精簡的庫。你可以使用委托(delegation)和組合(composition)將Mosby集成到你的開發(fā)技術(shù)棧中柱彻。這樣你就能避免框架(framework)帶來的限制和約束豪娜。
思考:什么是委托模式?委托和繼承的區(qū)別是什么绒疗?使用委托有什么好處侵歇?
依賴:
-
Mosby被分成模塊,你可以選擇你需要的功能:
dependencies { compile 'com.hannesdorfmann.mosby:mvp:2.0.1' compile 'com.hannesdorfmann.mosby:viewstate:2.0.1' }
三.Hello MVP World
先來用Mosby MVP庫來實(shí)現(xiàn)一個(gè)最簡單的功能吓蘑,頁面有兩個(gè)Button和一個(gè)TextView惕虑,需求如下:
- 點(diǎn)擊Hello按鈕,顯示紅色文本 "Hello" + 隨機(jī)數(shù)磨镶;
- 點(diǎn)擊Goodbye按鈕溃蔫,顯示藍(lán)色文本 "Goodbye" + 隨機(jī)數(shù);
這里假設(shè)隨機(jī)數(shù)的生成過程涉及到復(fù)雜的業(yè)務(wù)邏輯計(jì)算琳猫,是一個(gè)耗時(shí)操作伟叛,需要2s時(shí)間。
<b>第一步</b>我們用一個(gè)AsyncTask來實(shí)現(xiàn)這個(gè)模擬的業(yè)務(wù)邏輯脐嫂,在自定義的AsyncTask中统刮,要定義一個(gè)監(jiān)聽器,用來傳遞業(yè)務(wù)邏輯執(zhí)行結(jié)果:
public class GreetingGeneratorTask extends AsyncTask<Void, Void, Integer>{
// Callback - listener
public interface GreetingTaskListener{
void onGreetingGenerated(String greetingText);
}
......
// 模擬計(jì)算過程账千,返回一個(gè)隨機(jī)值侥蒙。
@Override
protected Integer doInBackground(Void... params) {
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
return (int)(Math.random() * 100);
}
@Override
protected void onPostExecute(Integer randomInt) {
listener.onGreetingGenerated(baseText + " " + randomInt);
}
}
<b>第二步</b>定義視圖接口,視圖接口需要繼承MvpView:
public interface HelloWorldView extends MvpView{
void showHello(String greetingText);
void showGoodbye(String greetingText);
}
注意這里的MvpView是所有視圖的頂層接口匀奏,它是一個(gè)空接口鞭衩,沒有定義任何方法。
<b>第三步</b>實(shí)現(xiàn)Presenter,Presenter需要執(zhí)行業(yè)務(wù)邏輯论衍,并針對不同的執(zhí)行結(jié)果調(diào)用視圖的對應(yīng)方法瑞佩。
Presenter的頂層接口是MvpPresenter,它有兩個(gè)方法:
public interface MvpPresenter<V extends MvpView> {
/**
* 將View附著到Presenter上
*/
public void attachView(V view);
/**
* 在視圖被摧毀時(shí)調(diào)用坯台。典型場景是Activity.onDestroy()和 Fragment.onDestroyView()方法
*/
public void detachView(boolean retainInstance);
}
Mosby提供了MvpPresenter接口的基類實(shí)現(xiàn)炬丸,在這里我們繼承MvpBasePresenter:
public class HelloWorldPresenter extends MvpBasePresenter<HelloWorldView>{
private GreetingGeneratorTask greetingTask;
private void cancelGreetingTaskIfRunning(){
if (greetingTask != null){
greetingTask.cancel(true);
}
}
public void greetHello(){
cancelGreetingTaskIfRunning();
greetingTask = new GreetingGeneratorTask("Hello", new GreetingGeneratorTask.GreetingTaskListener() {
@Override
public void onGreetingGenerated(String greetingText) {
if (isViewAttached()){
getView().showHello(greetingText);
}
}
});
greetingTask.execute();
}
......
@Override
public void detachView(boolean retainInstance) {
super.detachView(retainInstance);
if (!retainInstance){
cancelGreetingTaskIfRunning();
}
}
}
注意在detachView方法中取消后臺(tái)任務(wù)的處理。
<b>第四步</b>實(shí)現(xiàn)Activity捂人,讓我們的Activity繼承MvpActivity御雕,并實(shí)現(xiàn)HelloWorldView接口。
MvpActivity有兩個(gè)泛型滥搭,分別是Presenter和View的具體類型:
public class HelloWorldActivity extends MvpActivity<HelloWorldView, HelloWorldPresenter> implements HelloWorldView{
......
}
繼承MvpActivity后酸纲,只有一個(gè)抽象方法createPresenter()需要實(shí)現(xiàn):
public HelloWorldPresenter createPresenter() {
return new HelloWorldPresenter();
}
HelloWorldView還有兩個(gè)方法需要實(shí)現(xiàn):
@Override
public void showHello(String greetingText) {
greetingTextView.setTextColor(Color.RED);
greetingTextView.setText(greetingText);
}
@Override
public void showGoodbye(String greetingText) {
greetingTextView.setTextColor(Color.BLUE);
greetingTextView.setText(greetingText);
}
點(diǎn)擊按鈕后,使用Presenter來完成相關(guān)操作:
@OnClick(R.id.helloButton)
public void onHelloButtonClicked(){
presenter.greetHello();
}
@OnClick(R.id.goodbyeButton)
public void onGoodbyeButtonClicked(){
presenter.greetGoodbye();
}
四.MvpPresenter的基類
Presenter默認(rèn)實(shí)現(xiàn)一:使用弱引用保存視圖引用瑟匆,在調(diào)用getView()之前必須判斷isViewAttached()闽坡。
public class MvpBasePresenter<V extends MvpView> implements MvpPresenter<V> {
private WeakReference<V> viewRef;
@Override public void attachView(V view) {
viewRef = new WeakReference<V>(view);
}
@Nullable public V getView() {
return viewRef == null ? null : viewRef.get();
}
public boolean isViewAttached() {
return viewRef != null && viewRef.get() != null;
}
@Override public void detachView(boolean retainInstance) {
if (viewRef != null) {
viewRef.clear();
viewRef = null;
}
}
}
Presenter默認(rèn)實(shí)現(xiàn)二:使用Null Object Pattern,在調(diào)用getView()時(shí)無需判斷愁溜。
public class MvpNullObjectBasePresenter<V extends MvpView> implements MvpPresenter<V> {
private V view;
@Override public void attachView(V view) {
this.view = view;
}
@NonNull public V getView() {
if (view == null) {
throw new NullPointerException("MvpView reference is null. Have you called attachView()?");
}
return view;
}
@Override public void detachView(boolean retainInstance) {
if (view != null) {
Type[] types =
((ParameterizedType) getClass().getGenericSuperclass()).getActualTypeArguments();
Class<V> viewClass = (Class<V>) types[0];
view = NoOp.of(viewClass);
}
}
}
思考:什么是空對象模式(Null Object Pattern)疾嗅?
五.LCE視圖
在開發(fā)Android應(yīng)用過程中,我們會(huì)發(fā)現(xiàn)很多頁面有相似的結(jié)構(gòu)和UI邏輯冕象,所以我們常常在寫重復(fù)代碼代承。如果能抽象出相似頁面的View接口,然后封裝頁面的基類渐扮,就能讓開發(fā)方便很多论悴。Mosby就給我們提供了一個(gè)這樣的視圖模板,叫做LCE View墓律。
LCE代表Loading-Content-Error(加載-內(nèi)容-錯(cuò)誤)膀估,此視圖有三種狀態(tài):顯示加載中,顯示數(shù)據(jù)內(nèi)容耻讽,或者顯示錯(cuò)誤視圖察纯。例如在如下的場景中:
假設(shè)我們要在ListView中顯示一個(gè)國家列表,國家列表的數(shù)據(jù)是從網(wǎng)絡(luò)獲取的针肥,是一個(gè)耗時(shí)操作饼记。在加載過程中,我們要顯示一個(gè)ProgressBar慰枕,如果加載出錯(cuò)握恳,我們要顯示一條錯(cuò)誤信息。另外捺僻,還要用SwipeRefreshLayout來讓用戶可以下拉刷新。
LCE View的接口定義如下:
public interface MvpLceView<M> extends MvpView {
/**
* 顯示加載視圖,加載視圖的id必須為R.id.loadingView
*/
public void showLoading(boolean pullToRefresh);
/**
* 顯示內(nèi)容視圖匕坯,內(nèi)容視圖的id必須為R.id.contentView
*
* <b>The content view must have the id = R.id.contentView</b>
*/
public void showContent();
/**
* 顯示錯(cuò)誤視圖束昵,錯(cuò)誤視圖必須是TextView,id必須是R.id.errorView
*/
public void showError(Throwable e, boolean pullToRefresh);
/**
* 設(shè)置將在showContent()中顯示的數(shù)據(jù)
*/
public void setData(M data);
/**
* 加載數(shù)據(jù)葛峻,此方法中常需要調(diào)用Presenter的對應(yīng)方法锹雏。因此此方法不可在Presenter
* 中使用,避免循環(huán)調(diào)用术奖。
* 參數(shù)pullToRefresh代表此次加載是否由下拉刷新觸發(fā)礁遵。
*/
public void loadData(boolean pullToRefresh);
}
思考:LCE視圖中考慮了下拉刷新,但沒有考慮上拉加載采记,如果服務(wù)器是分頁接口佣耐,需要添加上拉加載,應(yīng)該怎樣定義視圖接口唧龄?
六.MvpLceActivity和MvpLceFragment
Mosby封裝了LCE視圖的基類兼砖,現(xiàn)在我們用MvpLceActivity或MvpLceFragment來實(shí)現(xiàn)上面所說的加載國家列表的場景。
<b>第一步:</b>完成界面布局既棺,注意id必須使用上面指定的名稱讽挟,錯(cuò)誤視圖只能是一個(gè)TextView:
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
......>
<include layout="@layout/loading_view" />
<include layout="@layout/error_view" />
<android.support.v4.widget.SwipeRefreshLayout
android:id="@+id/contentView"
......>
<ListView ....../>
</android.support.v4.widget.SwipeRefreshLayout>
</FrameLayout>
<b>第二步:</b>繼承MvpLceView實(shí)現(xiàn)自己的視圖接口,此處需要指定泛型丸冕,作為數(shù)據(jù)類型:
public interface CountriesView extends MvpLceView<List<Country>>{
}
<b>第三步:</b>實(shí)現(xiàn)Presenter耽梅,在這里我們做了一個(gè)接口和一個(gè)實(shí)現(xiàn):
接口定義:
public interface CountriesPresenter extends MvpPresenter<CountriesView>{
void loadCountries(final boolean pullToRefresh);
}
具體實(shí)現(xiàn):
public class SimpleCountriesPresenter extends MvpNullObjectBasePresenter<CountriesView>
implements CountriesPresenter{
......
@Override
public void loadCountries(final boolean pullToRefresh) {
getView().showLoading(pullToRefresh);
......
countriesLoader = new CountriesAsyncLoader(++failingCounter % 2 != 0, new CountriesAsyncLoader.CountriesLoaderListener() {
@Override
public void onSuccess(List<Country> countries) {
getView().setData(countries);
getView().showContent();
}
@Override
public void onError(Exception e) {
getView().showError(e, pullToRefresh);
}
});
countriesLoader.execute();
}
......
}
上面代碼中就使用到了MvpLceView中除loadData()外的全部四個(gè)方法。
<b>第四步:</b>實(shí)現(xiàn)Activity或者Fragment胖烛,先以Activity為例眼姐,需要繼承MvpLceActivity:
public class CountriesActivity extends MvpLceActivity<SwipeRefreshLayout, List<Country>, CountriesView, CountriesPresenter>
implements SwipeRefreshLayout.OnRefreshListener, CountriesView{}
MvpLceActivity中定義了四個(gè)泛型,分別是ContentView的類型洪己,Data的類型妥凳,視圖接口的類型和Presenter的類型。此處ContentView使用的是SwipeRefreshLayout答捕。
繼承MvpLceActivity后有兩個(gè)方法需要實(shí)現(xiàn):
@Override
protected String getErrorMessage(Throwable e, boolean pullToRefresh) {
if (pullToRefresh) {
return "Error while loading countries";
} else {
return "Error while loading countries. Click here to retry";
}
}
@NonNull
@Override
public CountriesPresenter createPresenter() {
return new SimpleCountriesPresenter();
}
實(shí)現(xiàn)CountriesView接口后逝钥,重寫如下幾個(gè)方法:
@Override
public void setData(List<Country> data) {
adapter.clear();
adapter.addAll(data);
adapter.notifyDataSetChanged();
}
@Override
public void showContent() {
super.showContent();
contentView.setRefreshing(false);
}
@Override
public void showError(Throwable e, boolean pullToRefresh) {
super.showError(e, pullToRefresh);
contentView.setRefreshing(false);
}
@Override
public void loadData(boolean pullToRefresh) {
presenter.loadCountries(pullToRefresh);
}
實(shí)現(xiàn)OnRefreshListener接口后,需要實(shí)現(xiàn)一個(gè)方法:
@Override
public void onRefresh() {
loadData(true);
}
如果要用Fragment拱镐,方法基本上一樣艘款,只需繼承MvpLceFragment,唯一的區(qū)別是沃琅,Activity的初始化在onCreate()中完成哗咆,F(xiàn)ragment的初始化在onViewCreated()中完成:
在Activity中:
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.countries_list);
ButterKnife.bind(this);
contentView.setOnRefreshListener(this);
adapter = new ArrayAdapter<>(this, android.R.layout.simple_list_item_1);
listView.setAdapter(adapter);
loadData(false);
}
在Fragment中:
@Override
public void onViewCreated(View view, @Nullable Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
ButterKnife.bind(this, view);
contentView.setOnRefreshListener(this);
contentView.setOnRefreshListener(this);
adapter = new ArrayAdapter<>(getContext(), android.R.layout.simple_list_item_1);
listView.setAdapter(adapter);
loadData(false);
}
七.ViewState簡介
在Android開發(fā)中有一個(gè)很麻煩的問題,就是在界面被銷毀益眉、被重建的過程中保存和恢復(fù)視圖狀態(tài)晌柬。界面被系統(tǒng)回收和重建常常發(fā)生在這兩個(gè)場景中:
Configuration變化姥份,例如屏幕在橫豎屏之間切換,語言環(huán)境變化等年碘。
界面切到后臺(tái)(例如用戶按Home鍵)澈歉,Android在內(nèi)存過低時(shí)自動(dòng)回收此Activity,在界面重新顯示時(shí)重建Activity屿衅。
思考:兩種Activity被回收和重建的場景埃难,有什么區(qū)別?
Mosby提供了一個(gè)ViewState特性來解決這一問題涤久。ViewState是一個(gè)接口涡尘,只有一個(gè)apply方法:
public interface ViewState<V extends MvpView> {
/**
* Called to apply this viewstate on a given view.
*
* @param view The {@link MvpView}
* @param retained true, if the components like the viewstate and the presenter have been
* retained
* because the {@link Fragment#setRetainInstance(boolean)} has been set to true
*/
public void apply(V view, boolean retained);
}
例如,上面講過的MvpLceFragment响迂,如果想在橫豎屏切換過程中保存和恢復(fù)視圖狀態(tài)考抄,只需改成繼承MvpLceViewStateFragment,實(shí)現(xiàn)如下一個(gè)方法即可:
@Override
public LceViewState<List<Country>, CountriesView> createViewState() {
setRetainInstance(true);
return new RetainingLceViewState<>();
}
這是針對Mosby提供的LceView的ViewState栓拜,如果是我們的自定義視圖座泳,也可以實(shí)現(xiàn)自己的ViewState。整個(gè)ViewState特性的實(shí)現(xiàn)原理和應(yīng)用方法比較復(fù)雜幕与,這里不做過多介紹挑势。
八.思考
1.MVC,MVP和MVVM之間有什么區(qū)別和聯(lián)系啦鸣?
2.什么是委托模式潮饱?委托和繼承的區(qū)別是什么?使用委托有什么好處诫给?
3.什么是library香拉?什么是framework?它們的區(qū)別是什么中狂?
提示:library和framework的關(guān)鍵區(qū)別是“控制反轉(zhuǎn)”(Inversion of Control)凫碌。當(dāng)你調(diào)用library中的方法時(shí),你掌握控制權(quán)胃榕。但使用framework時(shí)盛险,控制是倒轉(zhuǎn)的:由framework來調(diào)用你的代碼。
4.什么是空對象模式(Null Object Pattern)勋又?
5.LCE視圖中考慮了下拉刷新苦掘,但沒有考慮上拉加載,如果服務(wù)器是分頁接口楔壤,需要添加上拉加載鹤啡,應(yīng)該怎樣定義視圖接口?
6.兩種Activity被回收和重建的場景蹲嚣,有什么區(qū)別递瑰?
提示:參考下面兩個(gè)方法:
Fragmemt.setRetainInstance(boolean retain)
Activity.onRetainNonConfigurationInstance()