【知识必备】浅淡MVP在Android项目中的实战演习,让代码结构更简单~
一、写在前面
讲道理,这次是真的笔者很久都没有更新blog了,主要最近维护的框架问题也是层出不穷,而且对技术交流群的解答也让我身心疲惫,所以在这里跟关注我的人说声抱歉,没有定期给你们带来福利,那么这里就给大家带来一个重磅福利:爱吖妹纸——Retrofit & RxJava & MVP & Butterknife 的完整App.
讲到最近让我身心疲惫的问题解答,无疑是让我在开源的路上越走越远,虽然我不是技术大牛,却依然被一些很简单的问题轮番轰炸,其实笔者的内心真的是拒绝的。不得不说,写给技术群内的你和群主,为什么你提问,而总没人回你!写的挺好。
二、概述
废话也不多说,对于MVP(Model View Presenter),我相信大多数人都能说出一些的,“MVC的演化版本”,“让Model和View完全解耦”等等,但用过MVP的人一定会觉得,在Android中,代码很清晰,不过多了很多类。对于大多数人而言,在看MVP的Demo的时候,一眼便是慢慢的nice,然而让自己来写个例子,却很头疼写不出来。但的确MVC模式写起来更加像是顺水推舟。只需要把自己的业务逻辑一股脑的放进Activity就成功完事儿。
不得不说,之前我们项目中的确也是用的MVC在编写的。很简单的会发现随便一个Activity代码都是几百上千行,甚至还有一万行以上的。看起来的确那么一回事儿,但是细想这个View对于布局文件,其实能做的事情特别少,实际上关于该布局文件中的数据绑定的操作,事件处理的操作都在Activity中,造成了Activity既想View又像Controller,鄙弃代码上的不美观来说,对于后面的阅读代码真的是吃力。
不信?你瞧瞧。
也许业务逻辑比较简单的功能用MVC没什么,但是想没想过,如果你产品后面改需求怎么办?是的,你接受产品需求的强奸,但还是只有忍辱偷生。在日渐复杂的业务逻辑上,你的Activity和Fragment代码越来越多,最终导致代码爆炸,难以维护。
网上浏览一圈,发现讲MVP的文章比比皆是,可见MVP的欢迎度,但大多数文章都只是讲理论,稍微好点的会附带一个简单的登录的Demo。然而,一个简单的demo很难让初次接触MVP模式的人掌握它的使用。所以爱吖妹纸应运而生。
三、爱吖妹纸的优势?
爱吖妹纸是运用 MVP,Retrofit,RxJava 等主流框架整合的干货 App,项目资源来源于代码家的干货集中营。代码量不多,但基本涉及了各个方面,界面采用design风格,所以也是学习design的良药。你还在等什么,猛戳链接吧!
https://github.com/nanchen2251/AiYaGirl
四、什么是 MVP
当然不能跑题,前面对 MVP 做了简单的概述,下面还是用一个简单的图表示一下。
如上图所示,在项目中 View 和 Model 并不直接交互,而是使用 Presenter 作为 View 和 Model 之间的桥梁。其中 Presenter 中同时持有 View 层以及 Model 层的 Interface 的引用,而 View 层持有 Presenter 层 Interface 的引用,当 View 层某个页面需要展示某些数据的时候,首先会调用Presenter 层的某个接口,然后 Presenter 层会调用 Model 层请求数据,当 Model 层数据加载成功之后会调用 Presenter 层的回调方法通知 Presenter 层数据加载完毕,最后 Presenter 层再调用 View 层的接口将加载后的数据展示给用户。这就是 MVP 模式的核心过程。
这样分层的好处就是大大减少了Model与View层之间的耦合度。一方面可以使得View层和Model层单独开发与测试,互不依赖。另一方面Model层可以封装复用,可以极大的减少代码量。当然,MVP还有其他的一些优点,这里不再赘述。
五、随便看一个功能?
这里就给大家随便看看干货板块的功能吧。
布局相当简单。
1 <?xml version="1.0" encoding="utf-8"?> 2 <android.support.v4.widget.SwipeRefreshLayout 3 xmlns:android="http://schemas.android.com/apk/res/android" 4 xmlns:app="http://schemas.android.com/apk/res-auto" 5 android:id="@+id/swipe_refresh_layout" 6 android:layout_width="match_parent" 7 android:layout_height="match_parent"> 8 9 10 <com.nanchen.aiyagirl.widget.RecyclerViewWithFooter.RecyclerViewWithFooter 11 android:id="@+id/recyclerView" 12 android:layout_width="match_parent" 13 android:layout_height="match_parent"/> 14 15 16 </android.support.v4.widget.SwipeRefreshLayout>
干货模块,也就是一个Fragment,里面有一个RecyclerView,支持下拉刷新和上拉加载数据。所以我们的 Presenter 和 View 只需要定义一下简单的方法。
1)加载数据的过程中显示加载的进度条;
2)加载数据成功提醒 Adapter 刷新数据;
3)加载失败谈窗提醒用户相关信息;
4)加载结束隐藏进度条;
1 package com.nanchen.aiyagirl.module.category; 2 3 import com.nanchen.aiyagirl.base.BasePresenter; 4 import com.nanchen.aiyagirl.base.BaseView; 5 import com.nanchen.aiyagirl.model.CategoryResult; 6 7 /** 8 * Author: nanchen 9 * Email: liushilin520@foxmail.com 10 * Date: 2017-04-14 10:14 11 */ 12 13 public interface CategoryContract { 14 15 interface ICategoryView extends BaseView{ 16 17 void getCategoryItemsFail(String failMessage); 18 19 void setCategoryItems(CategoryResult categoryResult); 20 21 void addCategoryItems(CategoryResult categoryResult); 22 23 void showSwipeLoading(); 24 25 void hideSwipeLoading(); 26 27 void setLoading(); 28 29 String getCategoryName(); 30 31 void noMore(); 32 } 33 34 interface ICategoryPresenter extends BasePresenter{ 35 36 void getCategoryItems(boolean isRefresh); 37 } 38 }
编写 Presenter 实现类。
1 package com.nanchen.aiyagirl.module.category; 2 3 import com.nanchen.aiyagirl.config.GlobalConfig; 4 import com.nanchen.aiyagirl.model.CategoryResult; 5 import com.nanchen.aiyagirl.module.category.CategoryContract.ICategoryView; 6 import com.nanchen.aiyagirl.module.category.CategoryContract.ICategoryPresenter; 7 import com.nanchen.aiyagirl.net.NetWork; 8 9 import rx.Observer; 10 import rx.Subscription; 11 import rx.android.schedulers.AndroidSchedulers; 12 import rx.schedulers.Schedulers; 13 14 /** 15 * ICategoryPresenter 16 * <p> 17 * Author: nanchen 18 * Email: liushilin520@foxmail.com 19 * Date: 2017-04-14 11:16 20 */ 21 22 public class CategoryPresenter implements ICategoryPresenter { 23 24 private ICategoryView mCategoryICategoryView; 25 private int mPage = 1; 26 private Subscription mSubscription; 27 28 public CategoryPresenter(ICategoryView androidICategoryView) { 29 mCategoryICategoryView = androidICategoryView; 30 } 31 32 @Override 33 public void subscribe() { 34 getCategoryItems(true); 35 } 36 37 @Override 38 public void unSubscribe() { 39 if (mSubscription != null && !mSubscription.isUnsubscribed()){ 40 mSubscription.unsubscribe(); 41 } 42 } 43 44 @Override 45 public void getCategoryItems(final boolean isRefresh) { 46 if (isRefresh) { 47 mPage = 1; 48 mCategoryICategoryView.showSwipeLoading(); 49 } else { 50 mPage++; 51 } 52 mSubscription = NetWork.getGankApi() 53 .getCategoryData(mCategoryICategoryView.getCategoryName(), GlobalConfig.CATEGORY_COUNT,mPage) 54 .subscribeOn(Schedulers.io()) 55 .observeOn(AndroidSchedulers.mainThread()) 56 .subscribe(new Observer<CategoryResult>() { 57 @Override 58 public void onCompleted() { 59 60 } 61 62 @Override 63 public void onError(Throwable e) { 64 mCategoryICategoryView.hideSwipeLoading(); 65 mCategoryICategoryView.getCategoryItemsFail(mCategoryICategoryView.getCategoryName()+" 列表数据获取失败!"); 66 } 67 68 @Override 69 public void onNext(CategoryResult categoryResult) { 70 if (isRefresh){ 71 mCategoryICategoryView.setCategoryItems(categoryResult); 72 mCategoryICategoryView.hideSwipeLoading(); 73 mCategoryICategoryView.setLoading(); 74 }else { 75 mCategoryICategoryView.addCategoryItems(categoryResult); 76 } 77 } 78 }); 79 80 } 81 }
编写Adapter,用于展示数据。
1 package com.nanchen.aiyagirl.module.category; 2 3 import android.content.Context; 4 import android.content.Intent; 5 import android.view.View; 6 import android.widget.ImageView; 7 8 import com.bumptech.glide.Glide; 9 import com.nanchen.aiyagirl.ConfigManage; 10 import com.nanchen.aiyagirl.R; 11 import com.nanchen.aiyagirl.base.adapter.CommonRecyclerAdapter; 12 import com.nanchen.aiyagirl.base.adapter.CommonRecyclerHolder; 13 import com.nanchen.aiyagirl.base.adapter.ListenerWithPosition; 14 import com.nanchen.aiyagirl.model.CategoryResult; 15 import com.nanchen.aiyagirl.model.CategoryResult.ResultsBean; 16 import com.nanchen.aiyagirl.module.web.WebViewActivity; 17 import com.nanchen.aiyagirl.utils.TimeUtil; 18 19 /** 20 * Author: nanchen 21 * Email: liushilin520@foxmail.com 22 * Date: 2017-04-14 10:21 23 */ 24 25 class CategoryRecyclerAdapter extends CommonRecyclerAdapter<CategoryResult.ResultsBean> implements ListenerWithPosition.OnClickWithPositionListener<CommonRecyclerHolder>{ 26 27 CategoryRecyclerAdapter(Context context) { 28 super(context, null, R.layout.item_category); 29 } 30 31 @Override 32 public void convert(CommonRecyclerHolder holder, ResultsBean resultsBean) { 33 if (resultsBean != null) { 34 ImageView imageView = holder.getView(R.id.category_item_img); 35 if (ConfigManage.INSTANCE.isListShowImg()) { // 列表显示图片 36 imageView.setVisibility(View.VISIBLE); 37 String quality = ""; 38 if (resultsBean.images != null && resultsBean.images.size() > 0) { 39 switch (ConfigManage.INSTANCE.getThumbnailQuality()) { 40 case 0: // 原图 41 quality = ""; 42 break; 43 case 1: // 44 quality = "?imageView2/0/w/400"; 45 break; 46 case 2: 47 quality = "?imageView2/0/w/190"; 48 break; 49 } 50 Glide.with(mContext) 51 .load(resultsBean.images.get(0) + quality) 52 .placeholder(R.mipmap.image_default) 53 .error(R.mipmap.image_default) 54 .into(imageView); 55 } else { // 列表不显示图片 56 Glide.with(mContext).load(R.mipmap.image_default).into(imageView); 57 } 58 } else { 59 imageView.setVisibility(View.GONE); 60 } 61 62 holder.setTextViewText(R.id.category_item_desc, resultsBean.desc == null ? "unknown" : resultsBean.desc); 63 holder.setTextViewText(R.id.category_item_author, resultsBean.who == null ? "unknown" : resultsBean.who); 64 holder.setTextViewText(R.id.category_item_time, TimeUtil.dateFormat(resultsBean.publishedAt)); 65 holder.setTextViewText(R.id.category_item_src, resultsBean.source == null ? "unknown" : resultsBean.source); 66 holder.setOnClickListener(this, R.id.category_item_layout); 67 } 68 } 69 70 @Override 71 public void onClick(View v, int position, CommonRecyclerHolder holder) { 72 // Toasty.info(mContext,"跳转到相应网页!", Toast.LENGTH_SHORT,true).show(); 73 Intent intent = new Intent(mContext, WebViewActivity.class); 74 intent.putExtra(WebViewActivity.GANK_TITLE, mData.get(position).desc); 75 intent.putExtra(WebViewActivity.GANK_URL, mData.get(position).url); 76 mContext.startActivity(intent); 77 } 78 }
最后当然是 Fragment。
package com.nanchen.aiyagirl.module.category; import android.os.Bundle; import android.support.v4.widget.SwipeRefreshLayout; import android.support.v4.widget.SwipeRefreshLayout.OnRefreshListener; import android.support.v7.widget.LinearLayoutManager; import com.nanchen.aiyagirl.R; import com.nanchen.aiyagirl.base.BaseFragment; import com.nanchen.aiyagirl.model.CategoryResult; import com.nanchen.aiyagirl.module.category.CategoryContract.ICategoryPresenter; import com.nanchen.aiyagirl.module.category.CategoryContract.ICategoryView; import com.nanchen.aiyagirl.widget.RecyclerViewDivider; import com.nanchen.aiyagirl.widget.RecyclerViewWithFooter.OnLoadMoreListener; import com.nanchen.aiyagirl.widget.RecyclerViewWithFooter.RecyclerViewWithFooter; import butterknife.BindView; import es.dmoral.toasty.Toasty; /** * 主页轮播下面的Fragment * <p> * Author: nanchen * Email: liushilin520@foxmail.com * Date: 2017-04-14 9:46 */ public class CategoryFragment extends BaseFragment implements ICategoryView, OnRefreshListener, OnLoadMoreListener { public static final String CATEGORY_NAME = "com.nanchen.aiyagirl.module.category.CategoryFragment.CATEGORY_NAME"; @BindView(R.id.recyclerView) RecyclerViewWithFooter mRecyclerView; @BindView(R.id.swipe_refresh_layout) SwipeRefreshLayout mSwipeRefreshLayout; private String categoryName; private CategoryRecyclerAdapter mAdapter; private ICategoryPresenter mICategoryPresenter; public static CategoryFragment newInstance(String mCategoryName) { CategoryFragment categoryFragment = new CategoryFragment(); Bundle bundle = new Bundle(); bundle.putString(CATEGORY_NAME, mCategoryName); categoryFragment.setArguments(bundle); return categoryFragment; } @Override protected int getContentViewLayoutID() { return R.layout.fragment_category; } @Override protected void init() { mICategoryPresenter = new CategoryPresenter(this); categoryName = getArguments().getString(CATEGORY_NAME); mSwipeRefreshLayout.setOnRefreshListener(this); mAdapter = new CategoryRecyclerAdapter(getActivity()); mRecyclerView.setLayoutManager(new LinearLayoutManager(getActivity())); mRecyclerView.addItemDecoration(new RecyclerViewDivider(getActivity(), LinearLayoutManager.HORIZONTAL)); mRecyclerView.setAdapter(mAdapter); mRecyclerView.setOnLoadMoreListener(this); mRecyclerView.setEmpty(); mICategoryPresenter.subscribe(); } @Override public void onDestroy() { super.onDestroy(); if (mICategoryPresenter != null) { mICategoryPresenter.unSubscribe(); } } @Override public void onRefresh() { mICategoryPresenter.getCategoryItems(true); } @Override public void onLoadMore() { mICategoryPresenter.getCategoryItems(false); } @Override public void getCategoryItemsFail(String failMessage) { if (getUserVisibleHint()) { Toasty.error(this.getContext(), failMessage).show(); } } @Override public void setCategoryItems(CategoryResult categoryResult) { mAdapter.setData(categoryResult.results); } @Override public void addCategoryItems(CategoryResult categoryResult) { mAdapter.addData(categoryResult.results); } @Override public void showSwipeLoading() { mSwipeRefreshLayout.setRefreshing(true); } @Override public void hideSwipeLoading() { mSwipeRefreshLayout.setRefreshing(false); } @Override public void setLoading() { mRecyclerView.setLoading(); } @Override public String getCategoryName() { return this.categoryName; } @Override public void noMore() { mRecyclerView.setEnd("没有更多数据"); } }
六、看看项目截图?
还是给大家看看项目截图,以免大家心慌。
七、写在最后
笔者也是希望继续在开源路上越走越远,还请大家支持,点击评论666!别忘了关注我的 github,随手点赞。
作 者:
南 尘
出 处: http://www.cnblogs.com/liushilin/
关于作者:专注于移动前端的项目开发。如有问题或建议,请多多赐教!欢迎加入Android交流群:118116509
版权声明:本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文链接。
特此声明:所有评论和私信都会在第一时间回复。也欢迎园子的大大们指正错误,共同进步。或者直接私信我
声援博主:如果您觉得文章对您有帮助,可以点击文章下部【推荐】或侧边【关注】。您的鼓励是作者坚持原创和持续写作的最大动力!
欢迎关注我的公众号,精讲面试、算法、Andrid、Java、Python,旨在打造全网最比心的公众号。