最近公司新创立一个项目,准备开始前期工作,搭建框架,正在想着要怎么搭建更好的框架,以便轻松应付后续需求。想着最近比较流行的RxJava+Retrofit+MVP框架,自己也准备写一个通用的前端项目框架,撸起袖子准备开干。
备注:该项目会一直维护在github上,去往github看。
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。
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
在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 [email protected]
* @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 [email protected]
* @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 [email protected]
* @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 [email protected]
* @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 [email protected]
* @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() {
@Override
public 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);
else
ViewCompat.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