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