天天看點

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的差別

三種互動圖如下

android設計模式之mvp詳解
android設計模式之mvp詳解
android設計模式之mvp詳解

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在開發中的運用,

android設計模式之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