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