Android主流框架RxJava+Retrofit+MVP
简介
最近公司新创立一个项目,准备开始前期工作,搭建框架,正在想着要怎么搭建更好的框架,以便轻松应付后续需求。想着最近比较流行的RxJava+Retrofit+MVP框架,自己也准备写一个通用的前端项目框架,撸起袖子准备开干。
备注:该项目会一直维护在github上,去往github看。
RxJava
RxJava 在 GitHub 主页上的自我介绍是 "a library for composing asynchronous and event-based programs using observable sequences for the Java VM"(一个在 Java VM 上使用可观测的序列来组成异步的、基于事件的程序的库)。这就是 RxJava ,概括得非常精准。当然这是比较官网的解释,也是一头雾水,以我的理解,其实就是观察者模式,观察者与被观察者,类似Android里面的button点击事件。用来实现异步操作,Android里面有很多方法实现异步操作,RxJava的优势就是很简洁,代码看着很舒服,像我们这种有代码洁癖的人来说简直就是福音,更重要的是随着项目的开展,产品的需求迭代,依然能保持简洁。当然网上有很多RxJava的博文,一些大神讲解的也是非常的细,比我讲的那是好多了,我就不一一说了,推荐几篇文章。
RxJava:http://gank.io/post/560e15be2dca930e00da1083#toc_1。
Retrofit
retrofit是一款针对Android网络请求的开源框架,它与okhttp一样出自Square公司。Rotrofit2.0的网络框架全部交给了okhttp来实现,Android N之后Apache的httpclient已经被Google从SDK中移除,Okhttp则成功上位。Retrofit的网络请求实现风格与URLconnection和httpClient有较大的差别。创建请求的时候需要先创建基于注解的服务接口(不了解的可以先了解一下注解),进行网络请求的时候再通过Retrofit.creat()方法来创建请求。以我的理解其实就是对Okhttp进行了一层的封装,而且retrofit可以很好的搭配RxJava使用,所以说retrofit和RxJava更配哦。。Retrofit:https://blog.csdn.net/carson_ho/article/details/73732076
MVP
MVP模式是MVC模式在Android上的一种变体,要介绍MVP就得先介绍MVC。在MVC模式中,Activity应该是属于View这一层。而实质上,它既承担了View,同时也包含一些Controller的东西在里面。这对于开发与维护来说不太友好,耦合度大高了。把Activity的View和Controller抽离出来就变成了View和Presenter,这就是MVP模式。以我的理解其实MVP模式跟MVC差不多,MVP分离的更细,有利于扩展,特别是项目需求不停的更改时,就能理解到MVP是多么的好用,更重要的代码看上去也非常的简洁。
MVP:https://blog.csdn.net/briblue/article/details/52839242
项目搭建
介绍完一堆理论,最重要的就是撸起袖子写代码,这才是关键,很多人很喜欢看技术博文,但是自己很少动手自己写,很快就会遗忘,很多问题只有自己去动手写才能发现。话不多说,开撸。引入我们需要的JAR包
在gradle引入jar包
//Retrofit compile 'com.squareup.retrofit2:retrofit:2.4.0' compile 'com.squareup.retrofit2:adapter-rxjava2:2.4.0' compile 'com.squareup.retrofit2:converter-gson:2.4.0' //RxJava compile 'io.reactivex.rxjava2:rxjava:2.1.12' //Rxlife用于管理RxJava的订阅和解除 compile 'com.trello.rxlifecycle2:rxlifecycle:2.2.1' compile 'com.trello.rxlifecycle2:rxlifecycle-components:2.2.1' //RxAndroid compile 'io.reactivex.rxjava2:rxandroid:2.0.2'
public interface ApiService { /** * 广告banner * @return */ @GET("/ace-app/bannerInfo/{id}") Observable>> getBannerInfo(@Path("id") String id); /** * 测试数据接口 * @param q * @param tag * @param start * @param end * @return */ @GET("book/search") Observable getBooks(@Query("q") String q, @Query("tag") String tag, @Query("start") String start, @Query("end") String end);}
因为结合RxJava使用,需要返回被观察者Observable
创建Retrofit辅助类,用于网络请求
/** * Retrofit 辅助类 * @author Veer * @email 276412667@qq.com * @date 18/7/2 */public class RetrofitHelper { private static String TGA = "RetrofitHelper"; private long CONNECT_TIMEOUT = 60L; private long READ_TIMEOUT = 30L; private long WRITE_TIMEOUT = 30L; private static RetrofitHelper mInstance = null; private Retrofit mRetrofit = null; public static RetrofitHelper getInstance(){ synchronized (RetrofitHelper.class){ if (mInstance == null){ mInstance = new RetrofitHelper(); } } return mInstance; } private RetrofitHelper(){ init(); } private void init() { resetApp(); } private void resetApp() { mRetrofit = new Retrofit.Builder() .baseUrl(Contacts.DEV_BASE_URL) .client(getOkHttpClient()) .addConverterFactory(GsonConverterFactory.create()) .addCallAdapterFactory(RxJava2CallAdapterFactory.create()) .build(); } /** * 获取OkHttpClient实例 * * @return */ private OkHttpClient getOkHttpClient() { OkHttpClient okHttpClient = new OkHttpClient.Builder() .retryOnConnectionFailure(true) .connectTimeout(CONNECT_TIMEOUT, TimeUnit.SECONDS) .readTimeout(READ_TIMEOUT, TimeUnit.SECONDS) .writeTimeout(WRITE_TIMEOUT, TimeUnit.SECONDS) .addInterceptor(new RqInterceptor()) .addInterceptor(new LogInterceptor()) .build(); return okHttpClient; } /** * 请求拦截器 */ private class RqInterceptor implements Interceptor { @Override public Response intercept(Interceptor.Chain chain) throws IOException { Request request = chain.request() .newBuilder() .addHeader("X-APP-TYPE","android") .build(); Response response = chain.proceed(request); return response; } } /** * 日志拦截器 */ private class LogInterceptor implements Interceptor { @Override public Response intercept(Interceptor.Chain chain) throws IOException { Request request = chain.request(); String url = request.url().toString(); String params = requestBodyToString(request.body()); Response response = chain.proceed(request); String responseString = JsonHandleUtils.jsonHandle(response.body().string()); String time = DateUtils.getNowDateFormat(DateUtils.DATE_FORMAT_2); String log = "\n\n*****请求时间*****:\n" + time+"\n*******路径*******:\n" + url + "\n*******参数*******:\n" + params + "\n*******报文*******:\n" + responseString+"\n \n"; Log.d(TGA,log); return chain.proceed(request); } } private String requestBodyToString(final RequestBody request) { try { final RequestBody copy = request; final Buffer buffer = new Buffer(); if (copy != null){ copy.writeTo(buffer); } else{ return ""; } return buffer.readUtf8(); } catch (final IOException e) { return "did not work"; } } public ApiService getServer(){ return mRetrofit.create(ApiService.class); }}
这里面代码比较多,了解Okhttp的同学应该看得懂,Retrofit也是基于Okhttp的,我使用了单例进行了一层的处理,方便的我们的接口请求,这里比较重要的是拦截器,我们在配置Okhttp的时候可以配置一些拦截器,做一些请求头和日志等处理。看不懂的同学也不用担心,我会把项目源码贴出来,GitHub上也会一直维护。
配置完了Retrofit,现在最重要的就是搭建MVP,搭建MVP前期工作一定要做好,方便我们后续的扩展,前期可能会写很多接口,如果想后续不被产品的需求磨得脑壳疼的话,这些都是免不了的。
/** * 基础配置约定 * @author Veer * @email 276412667@qq.com * @date 18/7/2 */public interface BaseContract { interface BasePresenter{ /** * view挂载 * * @param view */ void attachView(T view); /** * View卸载 */ void detachView(); } interface BaseView{ /** * 显示进度中 */ void showLoading(); /** * 隐藏进度 */ void hideLoading(); /** * 显示请求成功 * * @param message */ void showSuccess(String message); /** * 失败重试 * * @param message */ void showFailed(String message); /** * 显示当前网络不可用 */ void showNoNet(); /** * 重试 */ void onRetry(); /** * 绑定生命周期 * * @param * @return */ LifecycleTransformer bindToLife(); }}
/** * 书本配置约定 * @author Veer * @email 276412667@qq.com * @date 18/7/2 */public interface BookContract { interface View extends BaseContract.BaseView{ void setBook(BookModel model); } interface Presenter extends BaseContract.BasePresenter{ void getBook(FrameLayout4Loading frameLayout4Loading,String p, String tag, String start, String end); }}
这里包括了P V,P用来处理业务逻辑,V更新视图。activity中的代码就少了很多了
/** * 描述 * * @author Veer * @email 276412667@qq.com * @date 18/7/2 */@Route(path= ActivityContracts.ACTIVITY_BOOK)public class BookActivity extends BaseActivity implements BookContract.View{ @BindView(R.id.tv_book) TextView mTvBook; @BindView(R.id.loading) FrameLayout4Loading mFrameLayout4Loading; @Override public void setBook(BookModel model) { mTvBook.setText(model.getBooks().toString()); } @Override protected void initView() { mFrameLayout4Loading.setRefreashClickListener(new View.OnClickListener() { @Override public void onClick(View v) { mPresenter.getBook(mFrameLayout4Loading,"三国演义","","0","1"); } }); mPresenter.getBook(mFrameLayout4Loading,"三国演义","","0","1"); } @Override protected BookPresenter initPresenter() { return new BookPresenter(); } @Override protected int getActivityLayoutID() { return R.layout.activity_book; }}
业务逻辑都在P中处理
public class BookPresenter extends BasePresenter implements BookContract.Presenter{ @Override public void getBook(final FrameLayout4Loading frameLayout4Loading, String p, String tag, String start, String end) {// mView.showLoading(); frameLayout4Loading.showLoadingView(); RetrofitHelper.getInstance().getServer() .getBooks(p,tag,start,end) .compose(RxSchedulers.applySchedulers()) .compose(mView.bindToLife()) .subscribe(new Consumer() { @Override public void accept(BookModel bookModel) throws Exception {// mView.hideLoading(); frameLayout4Loading.hideLoadingView(); mView.setBook(bookModel); } }, new Consumer() { @Override public void accept(Throwable throwable) throws Exception {// mView.hideLoading(); frameLayout4Loading.hideLoadingView(); frameLayout4Loading.showDefaultExceptionView(); } }); }}
因为在大部分项目中都要网络请求,都需要加载框,统一封装了一个控件,用于加载框显示。
大部分加载框都有用到动画,可以使用Lottie 非常好用的一个开源动画,适用于android ios h5。可参考 Lottie的使用。
我就贴一些主要代码:
/*** * 加载页面 * @author Veer * @email 276412667@qq.com * @date 18/7/3 */public class FrameLayout4Loading extends FrameLayout {/*** 没有数据显示 */public static final int ViewType_EmptyView = -1;/*** 请求中 */public static final int ViewType_Loading = 0;/*** 加载异常, 在无合适的异常类型使用的情况下使用此异常 */public static final int ViewType_DefaultException = 1;/*** 无网络 */public static final int ViewType_NoNetwork = 4;/*** 网络超时 */public static final int ViewType_Timeout = 5;/*** 数据异常, 服务器返回数据有误(比如反序列化失败) */public static final int ViewType_ServerException = 3;/** 其他错误 */public static final int EXP_DEFAULT_FAIL = 10001;/** 无法连接到网络,请检查网络配置 */public static final int EXP_NETWORK_NOTAVAILABLE = 90001;/** 超时了,请您重试 */public static final int EXP_REQUEST_TIMEOUT = 90003;/** 服务出错啦,请稍后重试 */public static final int EXP_SERVICE_FAIL = 90004;/** 服务调用出错,但不带重试按钮只显示信息 */public static final int EXP_SERVICE_FAIL_INFO = 150101;private static Animation loadingAnim;private SparseArray defaultLayout = new SparseArray(7);public SparseArray cachedLayout = new SparseArray(7);private LayoutInflater mInflater;private OnClickListener mRefreshClickListener;private ImageView icon;private TextView tip;private TextView subTip;private boolean viewUp = false;private LottieAnimationView mLottieAnimationView;private Context mContext;public boolean isViewUp() {return viewUp;}public void setViewUp(boolean viewUp) {this.viewUp = viewUp;}public FrameLayout4Loading(Context context) {super(context);init(context, null);}public FrameLayout4Loading(Context context, AttributeSet attrs) {super(context, attrs);init(context, attrs);}public FrameLayout4Loading(Context context, AttributeSet attrs, int defStyle) {super(context, attrs, defStyle);init(context, attrs);}private void init(Context context, AttributeSet attrs) {this.mContext = context;if (isInEditMode())return;if (loadingAnim==null){loadingAnim = AnimationUtils.loadAnimation(context, R.anim.round_loading);}mInflater = (LayoutInflater) getContext().getSystemService(Context.LAYOUT_INFLATER_SERVICE);if (attrs != null) {TypedArray a = context.obtainStyledAttributes(attrs,R.styleable.FrameLayout4Loading);if (a != null) {setDefaultView(ViewType_Loading, a.getResourceId(R.styleable.FrameLayout4Loading_loadingView,R.layout.common_loading_indicator));setDefaultView(ViewType_NoNetwork, a.getResourceId(R.styleable.FrameLayout4Loading_loadingView,R.layout.common_error_default_style));setDefaultView(ViewType_Timeout, a.getResourceId(R.styleable.FrameLayout4Loading_loadingView,R.layout.common_error_default_style));setDefaultView(ViewType_DefaultException, a.getResourceId(R.styleable.FrameLayout4Loading_loadingView,R.layout.common_error_default_style));setDefaultView(ViewType_ServerException, a.getResourceId(R.styleable.FrameLayout4Loading_defaultExceptionView,R.layout.common_error_default_style));setDefaultView(ViewType_EmptyView,R.layout.common_empty_view);a.recycle();}}}public void setDefaultView(int viewType, int resLayout) {defaultLayout.put(viewType, resLayout);}public int getDefaultViewResId(int viewType) {return defaultLayout.get(viewType);}public void showView(int viewType, Drawable background) {int count = defaultLayout.size();for (int i = 0; i < count; i++) {int key = defaultLayout.keyAt(i);if (key == viewType) {doShowView(key, background);} else {hideView(key, background);}}}private void hideView(int viewType, Drawable background) {View view = cachedLayout.get(viewType);if (view == null)return;if (background != null) {view.setBackground(background);}view.setVisibility(GONE);}/** * 没有数据时显示 * @param background 图片 * @param msg 描述文字 */public void doShowNoData(Drawable background, String msg) {doShowNoData(background,msg,null,null);}/** * 没有数据时显示 * @param background 图片 * @param msg 描述文字 * @param btnTxt 按钮文字 * @param click 点击事件 */public void doShowNoData(Drawable background, String msg, String btnTxt, OnClickListener click) {int resLayoutId = defaultLayout.get(ViewType_EmptyView);if (resLayoutId <= 0)throw new IllegalStateException("layout is not set for " + ViewType_EmptyView);View view = cachedLayout.get(ViewType_EmptyView);if (view == null || click != null) {view = mInflater.inflate(resLayoutId, null);try {ImageView image = (ImageView)view.findViewById(R.id.empty_icon_iv);image.setBackground(background);TextView empText = (TextView) view.findViewById(R.id.empty_tip_tv);empText.setText(msg);if (click!=null){TextView empty_btn = (TextView) view.findViewById(R.id.empty_btn);empty_btn.setVisibility(View.VISIBLE);empty_btn.setOnClickListener(click);empty_btn.setText(btnTxt);}} catch (Exception e) {e.printStackTrace();}cachedLayout.put(ViewType_EmptyView, view);addView(view, new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT,Gravity.CENTER));initView(view, ViewType_EmptyView);initListener(view);}view.setVisibility(VISIBLE);view.bringToFront();}private void doShowView(int viewType, Drawable background) {int resLayoutId = defaultLayout.get(viewType);if (resLayoutId <= 0)throw new IllegalStateException("layout is not set for " + viewType);View view = cachedLayout.get(viewType);if (view == null) {view = mInflater.inflate(resLayoutId, null);try {TextView errorText = (TextView) view.findViewById(R.id.loading_error_text);if ( viewType == ViewType_Timeout|| viewType == ViewType_NoNetwork) {errorText.setText("亲爱的,您的网络开小差了哦");}else if(viewType == ViewType_DefaultException||viewType == ViewType_ServerException){errorText.setText("系统繁忙,请稍后再试!");}} catch (Exception e) {e.printStackTrace();}if (background != null) {if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {view.setBackground(background);} else {view.setBackgroundDrawable(background);}}cachedLayout.put(viewType, view);addView(view, new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT,Gravity.CENTER));initView(view, viewType);initListener(view);}view.setVisibility(VISIBLE);view.bringToFront();}private void initView(View v, int viewType) {if (ViewType_EmptyView == viewType) {icon = (ImageView) v.findViewById(R.id.empty_icon_iv);tip = (TextView) v.findViewById(R.id.empty_tip_tv);subTip = (TextView) v.findViewById(R.id.empty_sub_tip_tv);} else if (ViewType_Loading == viewType) {mLottieAnimationView = v.findViewById(R.id.loading_iv);//设置加载速度mLottieAnimationView.setSpeed(10);}}private void initListener(View view) {View refreshBtn = view.findViewById(R.id.loading_refreash_btn);if (refreshBtn != null && mRefreshClickListener != null) {refreshBtn.setOnClickListener(new OnClickListener() {@Overridepublic void onClick(View v) {mRefreshClickListener.onClick(v);}});}}/** * 显示加载动态图 */public void showLoadingView() {showLoadingView(null);}/** * 显示加载动态图 * @param backgroundId 背景图ID */public void showLoadingView(int backgroundId) {showLoadingView(getResources().getDrawable(backgroundId));}/** * 显示加载动态图 * @param background 背景图 */public void showLoadingView(Drawable background) {showView(ViewType_Loading, background);}/** * 隐藏加载动态图 */public void hideLoadingView() {hideLoadingView(null);}public void hideLoadingView(int backgroundId) {hideLoadingView(getResources().getDrawable(backgroundId));}public void hideLoadingView(Drawable background) {hideView(ViewType_Loading, background);}public void showDefaultExceptionView() {showExceptionView(EXP_DEFAULT_FAIL);}/** * 显示空页面 */public void showEmptyView() {showEmptyView(null);}public void showEmptyView(int backgroundId) {showEmptyView(getResources().getDrawable(backgroundId));}public void showEmptyView(Drawable background) {showView(ViewType_EmptyView, background);}/** * 显示异常页面 * @param errorCode */public void showExceptionView(int errorCode) {showView(getViewTypeByErrorCode(errorCode), null);}public void showExceptionView(int errorCode, int backgroundId) {showExceptionView(errorCode, getResources().getDrawable(backgroundId));}public void showExceptionView(int errorCode, Drawable background) {showView(getViewTypeByErrorCode(errorCode), background);}public void setExceptionViewIconVisibility(int visibility) {View view = cachedLayout.get(ViewType_DefaultException);view.findViewById(R.id.listview_error_pic).setVisibility(visibility);}public FrameLayout4Loading setEmptyViewIcon(T obj) {if (obj == null || icon == null)return null;if (obj instanceof Integer)icon.setImageResource((Integer) obj);elseViewCompat.setBackground(icon, (Drawable) obj);return this;}public FrameLayout4Loading setEmptyViewTip(T obj) {if (obj == null || tip == null)return null;if (obj instanceof Integer)((TextView) tip).setText(getResources().getString((Integer) obj));else if (obj instanceof CharSequence)((TextView) tip).setText((CharSequence) obj);return this;}public FrameLayout4Loading setEmptyViewTip(T obj, T obj1) {if (obj == null || tip == null)return null;if (obj instanceof Integer)((TextView) tip).setText(getResources().getString((Integer) obj));else if (obj instanceof CharSequence)((TextView) tip).setText((CharSequence) obj);if (obj1 == null || subTip == null) {return null;}subTip.setVisibility(View.VISIBLE);if (obj1 instanceof Integer)((TextView) subTip).setText(getResources().getString((Integer) obj1));else if (obj instanceof CharSequence)((TextView) subTip).setText((CharSequence) obj1);return this;}public void hideAllMask() {hideAllMask(null);}public void hideAllMask(int backgroundId) {hideAllMask(getResources().getDrawable(backgroundId));}public void hideAllMask(Drawable background) {int count = cachedLayout.size();for (int i = 0; i < count; i++) {int key = cachedLayout.keyAt(i);hideView(key, background);}}private static int getViewTypeByErrorCode(int errorCode) {switch (errorCode) {case EXP_NETWORK_NOTAVAILABLE :return ViewType_NoNetwork;case EXP_REQUEST_TIMEOUT :return ViewType_Timeout;case EXP_SERVICE_FAIL :return ViewType_ServerException;case EXP_DEFAULT_FAIL :case EXP_SERVICE_FAIL_INFO :default :return ViewType_DefaultException;}}public void setRefreashClickListener(OnClickListener listener) {mRefreshClickListener = listener;}}
然后项目中海使用了一些比较方便编程的框架:
阿里的ARouter :activity直接跳转的管理。
ButterKnife :控件注解。
Glide :图片框架。
这个项目框架后续我后一直更新完善:https://github.com/fuweiwei/RxJavaRetrofitMvp
源码下载
更多相关文章
- Android中网络传输不同内容
- Android(安卓)Lottie动画实战踩坑
- Android(安卓)UI框架概览
- Android(安卓)OpenGL ES
- Android基础—WebView(网页视图)基本用法
- Android(安卓)通过inputstream 加载非Drawable 文件夹下的 .9 pa
- Android(安卓)图片预览器加载微博长图,大图
- 解决ListView,GridView,Gallery的Adapter中的getView多次调...
- Java集合框架——Android中的ArrayList源码分析