android設計模式之mvp詳解
1,mvp模式介紹
mvp全稱model,view,presenter,目前mvp在 android應用開發中越來越屌,大家對mvp模式討論也越來越多,如果做了n年開發以後你還是簡單的呼叫api,簡單的堆程式碼,就太丟丟了,mvp能夠有效的降低view的複雜性,避免業務邏輯被塞進view,使得view變成一個混亂的大泥坑,mvp模式會解除view和model的耦合,同時又帶來良好的擴充套件性,可測試性,保證了系統的整潔性,靈活性,可能對於簡單的app來說mvp稍顯麻煩,各種各樣的介面和概念,使得整個app充滿著零散的介面,但是對於比較複雜的app來說,mvp模式是一種良好的架構模式,她能夠非常好的組織app架構,讓app變得靈活!
mvp模式可以分離顯示層和邏輯層,他們之間通過介面進行通訊,降低耦合,理想化的mvp模式可以實現統一分邏輯程式碼搭配不同的顯示介面,因為他們之間並不依賴具體,而是依賴抽象,這使得presenter可以運用於任何實現了view介面的ui,使之具有更廣泛的適用性,保證了靈活性!
我們知道在android上,業務邏輯和資料存取是緊耦合的,很多菜鳥很可能會將各種各樣的業務邏輯塞進某個Activiy,Fragment或者自定義的view中,使得這些元件單個型別相當臃腫,其中又含有一些非同步任務,導致某個類超過千行程式碼,當然,對於功能複雜的app來說,一個類超過千行程式碼並不是大驚小怪的事,我們所要指出的重點是業務邏輯與view元素嚴重耦合導致了型別膨脹的問題!
對於一個可擴充套件,穩定的app來說,我們需要定義分離各個層,主要是ui層,業務邏輯層和資料層,畢竟做產品的,pm隨時會腦洞大開,不知道會加入什麼邏輯,是從本地檢索獲取資料?還是遠端獲取?我們的ui,資料庫是否會被替換,例如:隨著app的升級,我們的ui可能會被重新設計,若UI發生了變化,此時由於業務邏輯耦合在view中,ui變化導致我們修改新的view控制元件,此時你就需要到原來的view中抽離具體的業務邏輯,這將是一件非常非常痛苦又蛋疼的事情!到最終你還是需要將業務邏輯抽離開來
mvp模式可以讓ui介面和資料分離,我們的app至少分為3層,這樣使得我們也可以對這三層進行獨立的單元測試(這裡吐槽一下,國內很少有單元測試),mvp模式可以讓我們從activity,fragment等view角色中分離大部分程式碼,使得每個型別的程式碼量大幅度減少,職責單一,易於維護
mvp並不是一個標準化的模式,它可以很多實現方式,我們也可以根據自己的需求和自己認為對的方式去修正mvp的實現方式,它可以隨著presenter的複雜程度變化,只要保證我們是通過presenter將view和model解耦合,降低型別的複雜度,各個模組可以獨立測試,獨立變化,這就是正確的方向,在android開發中,大多數人可能會把activity,fragment作為view角色來看待,因為他的職責是載入並處理一些簡單的與view相關的邏輯,她組織與管理view集合,我們可以把他看成是粗粒度的view,當然你也可以把他們看成presenter!
2,mvp模式的三個角色
1,presenter——互動中間人
presenter主要作為溝通view和model的橋樑,她從model層檢索資料後,返回給view層,使得view和model之間沒有耦合,也將業務邏輯從view角色上抽離出來
2,view——使用者介面
view通常是指activity,fragment或者某個view控制元件,她含有一個presenter成員變數,通常view需要實現一個介面邏輯,將view上的操作通過會轉交給presenter進行實現,最後,presenter呼叫view邏輯介面將結果返回給view元素
3,model——資料的存取
對於一個結構化的app來說,model角色主要是提供資料的存取功能,presenter需要通過model層存取,model就像是一個數據倉庫,更直白的說,model是封裝了資料庫dao或者網路獲取資料的角色,或者兩種資料獲取方式的集合
3,與mvc,mvvm的區別
三種互動圖如下
1,mvc特點
(1) 使用者可以向view傳送指令,再由view直接要求model改變狀態
(2) 使用者也可以直接向controller傳送指令,再由controller傳送給view
(3) controller起到事件路由的作用,同時業務邏輯全部部署在controller
可以看出mvc的耦合性還是相對較高,view可以直接訪問model,導致3者 之間構成迴路,因此,mvp和mvc的主要區別是,mvp中的view不能直接訪問model需要通過presenter發出請求,view和model不能直接通訊
2,mvvm特點
mvvm與mvp非常相似,唯一的區別是view和model進行雙向繫結,(data-bingding),兩者之間有一方發生變化則反應到另一方上,而mvp與mvvm的主要區別是,mvp中的view更新需要通過presenter,而mvvm則不需要,因為view和model進行了雙向繫結,資料的修改回直接反映到view角色上,而view的修改也會導致資料的變更,此時,viewmodel的角色需要做的只是業務邏輯的處理,以及修改view或者model的狀態,mvvm的模式有點像listview和adapter,資料集的關係,這個adapter就是viewmodel的角色,她與view進行了繫結,又與資料集進行了繫結,當資料集發生變化時,呼叫adapter的notifydatasetchanged之後view直接更新,他們之間沒有直接的耦合(這裡吐槽一下,很多逗比認為這個模式是mvc)
3,mvp的實現
下面我們通過一個簡單的客戶端例項來直觀體會下mvp在開發中的運用,
如圖,是一個簡單的新聞客戶端,進入應用之後,首先會從服務端下拉最新的20篇文章,然後將每個文章的簡介顯示到列表上,當用戶點選某項資料時進入到另一個頁面,該頁面載入這篇文章的詳細內容,因此,我們的業務邏輯大概有下列2項
(1)向伺服器請求資料,並存儲到資料庫中
(2)從資料庫中載入文章列表
我們的主介面(HomeFragment)就是一個RecyclerView和進度條,在載入資料時顯示進度條,載入完成之後隱藏,網路請求使用的是Volley,我們先從Presenter相關的型別入手,使用者需要從網路端獲取文章,因此,需要一個數據獲取介面,我們可以從本地資料庫獲取快取的資料,因為,需要一個從資料庫載入快取的介面,這個presneter我們命名為HomePresenter,
public class HomePresenter extends BasePresenter {
// model 介面, 代表了實體類介面角色
private IHomeModel homeModel;
//view介面,代表了view介面角色
private IHomeView view;
private boolean isProgressActive = true;
public HomePresenter(IHomeView homeView) {
if (homeView == null) {
throw new IllegalArgumentException("Constructor parameters cannot be null!");
}
this.homeModel = new HomeModel();
this.view = homeView;
}
//獲取bannner圖,也就是我們的業務邏輯
public void loadBanners() {
productManager.getPromoList(new PromotionRequest(), new BaseModel.OnDataLoadListener<PromotionRespond>() {
@Override
public void onSuccess(PromotionRespond respond) {
if (respond.getData() != null)
// 資料載入完,呼叫view的showPromition函式將資料傳遞給view顯示
view.showPromotion(respond.getData());
}
@Override
public void onFail(MsgRespond respond) {
}
@Override
public void onNetworkError(String msg) {
}
@Override
public void onFinish() {
}
});
}
// 獲取產品資訊,
public void loadProduct() {
if (isProgressActive) {
view.showProgressView(true);
}
productManager.getProductList(new ProductRequest(), new BaseModel.OnDataLoadListener<ProductRespond>() {
@Override
public void onSuccess(ProductRespond respond) {
if (respond == null) {
return;
}
// 展示資料
view.showTotalAmount(respond.getTotalReg());
List<Product> products = new ArrayList<Product>();
Result result = respond.getBorrowResult();
for (Project project : result.getHJTYB().getList()) {
Product product = project.getProduct();
product.setServerTime(new Date(respond.getServiceTime()));
products.add(product);
}
for (Project project : result.getDING().getList()) {
Product product = project.getProduct();
if (product.isHot()) {
product.setExtraRates(respond.getAwardRate());
product.setServerTime(new Date(respond.getServiceTime()));
products.add(product);
break;
}
}
// 展示產品資料
view.showProduct(products);
isProgressActive = false;
}
@Override
public void onFail(MsgRespond respond) {
}
@Override
public void onNetworkError(String msg) {
view.showDialog(view.getContext().getString(R.string.msg_network_error));
}
@Override
public void onFinish() {
view.showProgressView(false);
view.loadCompleted();
}
}, IConstants.RequestTag.TAG_HOME);
}
// 獲取產品詳情
public void getDetail(String ecodedId) {
final DialogFragment dialogFragment = view.showProgressDialog("獲取產品詳情...", false);
ProductDetailRequest request = new ProductDetailRequest();
request.setId(ecodedId);
productManager.getProductDetail(request, new BaseModel.OnDataLoadListener<ProductDetailRespond>() {
@Override
public void onSuccess(ProductDetailRespond respond) {
if (respond != null) {
view.getContext().startActivity(new Intent(view.getContext(), ProductDetailActivity.class).putExtra(IConstants.Extra.EXTRA_PRODUCT_DETAIL_RESPOND, respond));
}
}
@Override
public void onFail(MsgRespond respond) {
view.showDialog(respond.getMessage());
}
@Override
public void onNetworkError(String msg) {
view.showDialog(view.getContext().getResources().getString(R.string.msg_network_error));
}
@Override
public void onFinish() {
if(dialogFragment==null){
return;
}
dialogFragment.dismiss();
}
});
}
在HomePresenter中持有了view和model的引用,分別為IHomeModel和IHomeView,另外還有一個productManager物件,IHomeView就是主介面的邏輯介面,代表了view的角色,用於presenter回撥view的操作,具體程式碼如下:
public interface IHomeView extends IBaseView{
// 顯示banner
void showPromotion(List banners);
//顯示產品
void showProduct(List products);
//顯示所有使用者
void showTotalAmount(long amount);
//顯示對話方塊
DialogFragment showProgressDialog(String msg, boolean Cancelable);
// 顯示進度條
void showProgressView(boolean b);
// 隱藏註冊按鈕
void hidePromotionText(boolean isLogin);
// 隱藏頭部重新整理
void loadCompleted();
}
IHomeModel則是對資料的操作,用於儲存網路上的資料,以及從資料庫中載入的資料快取,
public interface IHomeModel {
void getPromoList(BaseModel.OnDataLoadListener listener);
void getProducts(BaseModel.OnDataLoadListener listener);
}
HomeFragment需要實現IHomeView介面,並且需要建立於presenter的關係,HomeFragment的邏輯業務都交給presenter處理,處理結果將通過IHomeView介面回撥給HomeFragment,下面是HomeFragment的具體程式碼
public class HomeFragment extends BaseFragment implements IHomeView, PullToRefreshView.OnHeaderRefreshListener {
@Bind(R.id.tv_viewpager)
SimpleImageBanner scrollViewPager;
@Bind(R.id.tv_promotiom)
TextView promotionView;
@Bind(R.id.tv_sum)
TextView investSumView;
@Bind(R.id.view_header)
View headView;
@Bind(R.id.progress_view)
CircularProgressView progressView;
@Bind(R.id.lv_product)
ListView lvProduct;
@Bind(R.id.refreshView)
PullToRefreshView refreshView;
MainProductListAdapter adapter;
HomePresenter presenter = new HomePresenter(this);
AutoScrollPagerAdapter pagerAdapter;
List products = new ArrayList();
public HomeFragment() {
// Required empty public constructor
}
@Override
public int getLayout() {
// 初始化佈局
return R.layout.fragment_home;
}
@Override
public void setupViews(View root) {
// 初始化控制元件等
presenter.registerEventBus();
if (StringUtils.isEmpty(preferenceKeyManager.KEY_TOKEN().get())) {
promotionView.setVisibility(View.VISIBLE);
}
promotionView.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
startActivity(new Intent(getContext(), VerifyPhoneActivity.class));
}
});
// 設定監聽器
refreshView.setOnHeaderRefreshListener(this);
// refreshView.setOnFooterLoadListener(this);
// 設定進度條的樣式
refreshView.getHeaderView().setHeaderProgressBarDrawable(
getActivity().getResources().getDrawable(R.drawable.progress_circular));
refreshView.getFooterView().setFooterProgressBarDrawable(
getActivity().getResources().getDrawable(R.drawable.progress_circular));
// 初始化頭佈局
initHeadView();
lvProduct.addHeaderView(headView);
// 請求bannner
presenter.loadBanners();
// 請求產品
presenter.loadProduct();
}
private void setSumText(long sum) {
// 設定總人數
String s1 = "已有 ";
String s2 = CurrencyUtils.formatCurrency(sum);
SpannableString spannableString = new SpannableString(s2);
ForegroundColorSpan span = new ForegroundColorSpan(ContextCompat.getColor(getContext(), R.color.txt_red_theme));
spannableString.setSpan(span, 0, s2.length(), Spannable.SPAN_INCLUSIVE_EXCLUSIVE);
spannableString.setSpan(new RelativeSizeSpan(1.3f), 0, s2.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
String s3 = " 人數";
SpannableStringBuilder stringBuilder = new SpannableStringBuilder(s1);
stringBuilder
.append(spannableString)
.append(s3);
investSumView.setText(stringBuilder);
}
private void setListView() {
// 設定listview
adapter = new MainProductListAdapter(getContext(), products);
lvProduct.setAdapter(adapter);
lvProduct.setOnItemClickListener(new AdapterView.OnItemClickListener() {
@Override
public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
Product product = (Product) view.getTag();
presenter.getDetail(product.getEncodedID());
}
});
}
private void initHeadView() {
headView = LayoutInflater.from(getActivity()).inflate(R.layout.layout_home_header, null);
scrollViewPager = (SimpleImageBanner) headView.findViewById(R.id.auto_loop_view);
investSumView = (TextView) headView.findViewById(R.id.tv_invest_sum);
}
@Override
public void showPromotion(final List<Banner> banners) {
// pagerAdapter.removeAllItem();
List lists = new ArrayList();
for (final Banner banner : banners) {
lists.add(new BannerItem(banner.getPic(), banner.getTitle()));
}
scrollViewPager.setSource(lists).startScroll();
scrollViewPager.setOnItemClickL(new SimpleImageBanner.OnItemClickL() {
@Override
public void onItemClick(int position) {
Banner banner = banners.get(position);
startActivity(new Intent(getContext(), BrowserActivity.class)
.putExtra(IConstants.Extra.EXTRA_WEBVIEW_URL, banner.getPath()));
}
});
}
@Override
public void showProduct(List<Product> products) {
this.products.clear();
this.products.addAll(products);
setListView();
}
@Override
public void showTotalAmount(long amount) {
setSumText(amount);
}
@Override
public void showProgressView(boolean b) {
if (progressView == null) {
return;
}
progressView.setVisibility(b ? View.VISIBLE : View.GONE);
}
@Override
public void showProgressDialog(boolean open) {
}
@Override
public void showDialog(String s) {
showMsgDialog(s, true);
}
@Override
public void hidePromotionText(boolean isLogin) {
promotionView.setVisibility(isLogin ? View.GONE : View.VISIBLE);
}
@Override
public void loadCompleted() {
refreshView.onHeaderRefreshFinish();
}
@Override
public void onDestroy() {
super.onDestroy();
presenter.cancelRequest();
presenter.unregisterEventBus();
}
@Override
public void onDestroyView() {
super.onDestroyView();
ButterKnife.unbind(this);
}
@Override
public void onHeaderRefresh(AbPullToRefreshView abPullToRefreshView) {
presenter.loadProduct();
}
}
HomeFragment實現了IHomeView介面,並且在setupViews函式中將自身傳遞給了HomePresenter,此時作為view角色的HomeFragment就於presenter建立了聯絡,而由於presenter又有IHomeModel的成員變數,因此model-view-presenter的關係此時已經建立
此時,我們就可以通過presenter處理業務邏輯,例如,在setupViews函式的最後一句是呼叫presenter的getBanners函式,該函式的作用就是從伺服器上下拉最新的banner資訊,當請求成功之後,呼叫IHomeView的showPromotion函式將資料傳遞給view,也就是HomeFragment物件,因為HomeFragment實現了IHomeView介面,因此呼叫的就是HomeFragment類中的showPromotion函式,在該函式中,我們將資料新增到ListView的headview中
通過這個用例我們看到,presenter對於view是完全解耦的,presenter依賴的是IhomeView的抽象,而不是HomeFragment這個類,當ui發生變化時,只需要更新ui實現了Ihomeview以及相關邏輯即可與presenter迅速的協作起來,成本非常低,而由於presenter將業務邏輯從HomeFragment抽離出來,是的homeframgent變得非常輕量級,homefragment此時的作用只是做一些view的初始化工作,指責單一,功能簡單,便於維護,presenter和view的低耦合使得系統能夠應對ui的易變性問題,也使得系統的view模組變的更易於維護,對於app 來說另一個問題就是資料模型和view的關係,mvp中的view和model不能直接通訊,他們的互動都是通過presenter,從上述的程式碼中我們可以看到,homepresenter中不光只有ihomeview,還持有一個ihomemodel物件,這個ihomemodel自然就是model角色,他負責處理資料,例如將資料儲存到資料庫中,從資料庫載入快取資料等,ihomemodel同樣也是被輕易的替換,需要注意的是,在我們的示例中對於homepresenter並沒有進行介面抽象,而是使用了具體,因為業務邏輯相對穩定,在此我們直接使用具體類即可,當然,如果你覺得你的業務邏輯相對來說易於變化,使用presenter介面來應對最好不過了,
由此可見model-view-presenter三者之間的關係都是鬆耦合的,presenter持有view,model的引用都是抽象,這樣當ui發生變化時,我們只需要替換view即可,而資料庫引擎需要替換時,我們只需呀重新構建一個實現ihomemodel介面的實現類相關存取邏輯即可,這樣使得view,model,presenter三者之間可以獨立的變化,測試也非常方便,可擴充套件性,靈活性都很高!
5,mvp與activity,fragment的生命週期
綜上所述,mvp有很多優點,例如易於維護,易於測試,鬆耦合,複用高,但是,由於presenter經常性的需要執行一些耗時操作,比如,我們上述的網路請求,而presenter持有了homefragment的引用,如果在請求結束之前homefragment被銷燬了,那麼由於網路請求還沒有回來,導致presenter一直持有homefragment物件,使得homefragment物件無法回收,此時就發生了記憶體洩漏
我們解決可以採用弱引用和activity,fragment的生命週期來解決這個問題,首先建立一個presenter的抽象,我們命名為basepresenter,他是一個泛型類,泛型型別為view角色要實現的介面,具體程式碼如下
public abstract class BasePresenter implements Serializable {
protected Reference mViewRef; // view介面型別的弱飲用
public void attachView(T view){
mViewRef = new WeakReference(view); // 建立關聯
}
protected T getView(){
return mViewRef.get();
}
public boolean isViewAttached(){
return mViewRef != null && mViewRef.get() != null;
}
public void detachView(){
if (mViewRef != null){
mViewRef.clear();
mViewRef = null;
}
}
}
basepresenter有4個方法,分別建立關聯,解除關聯,判斷是否與view建立了關聯,獲取view,view型別通過basepresenter的泛型傳遞進來,p resenter對這個view持有弱飲用,通常情況下這個view型別應該實現了某個特定介面的activity或fragment
建立一個basefragment,通過這個類的生命週期來控制他與presenter的關係
public abstract class BaseFragment