1. 程式人生 > >android設計模式之mvp詳解

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的區別
三種互動圖如下
mvp模式


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