前言
一直想整理一个自己app框架,现在刚好不是很忙就整理一下,尚不成熟还有待改进
大纲
1.整体结构:MVP模式+模块化
2.网络框架:Retrofit+Rxjava
3.屏幕适配方案:头条的AndroidAutoSize
4.分享框架:Mob的ShareSDK
5.其他:base、常用工具类以及简易的自定义控件等
6.常见问题
7.使用说明
项目链接
https://github.com/UncleQing/QingFrame
1.整体结构
主要以MVP模式+模块化为基础展开
MVP参考:https://www.jianshu.com/p/3e981d261e90
模块化参考:https://www.jianshu.com/p/748bf621a9a0
目录结构如下
app:做为项目主体,依赖library-common,也可以再定义一个module-main做为主体
library-common:做为所有共通资源以及管理类库,现在设计的思想是其他module理论上都依赖common,然后再根据不同业务实现业务逻辑,好处是资源便于管理,缺点是导致common过于臃肿,可能不突出模块化的优势。
另外根据业务分的module应该还可以单独运行,例如module-login可以被app依赖做为一个module,也可以自己单独为一个可执行application,这部分暂时没有实现,日后有待更新
common依赖列举
//ARouter
annotationProcessor 'com.alibaba:arouter-compiler:1.1.4'
api 'com.alibaba:arouter-api:1.3.1'
//retrofit
api 'com.squareup.retrofit2:retrofit:2.4.0'
api 'com.squareup.retrofit2:converter-gson:2.4.0'
api 'com.squareup.retrofit2:adapter-rxjava2:2.4.0'
//okhttp
api 'com.squareup.okhttp3:okhttp:3.11.0'
api 'com.squareup.okhttp3:logging-interceptor:3.9.1'
//rxjava and rxandroid
api 'io.reactivex.rxjava2:rxandroid:2.1.0'
api 'io.reactivex.rxjava2:rxjava:2.2.0'
//gson
api 'com.google.code.gson:gson:2.8.2'
//分包工具
api 'com.android.support:multidex:1.0.3'
//butterknife
api 'com.jakewharton:butterknife:8.8.1'
annotationProcessor 'com.jakewharton:butterknife-compiler:8.8.1'
//AndroidAutoSize
api 'me.jessyan:autosize:1.1.0'
//glide
api 'com.github.bumptech.glide:okhttp3-integration:1.4.0@aar'
api 'com.github.bumptech.glide:glide:4.3.1'
//圆形图片
api 'de.hdodenhof:circleimageview:2.2.0'
//AgentWeb
api 'com.just.agentweb:agentweb:4.0.2'
2.网络框架
Retrofit+Rxjava,当下最流行的网络请求框架,简单、好用、时髦,没说的,当然也有不少大牛是用自己实现的,以后也要自己实现一个
基本上这些东西吧,interceptor大概制定了四个DownloadInterceptor用于获取下载进度拦截器
/**
* 下载拦截器,用于进度条显示
*/
public class DownloadInterceptor implements Interceptor {
@Override
public Response intercept(Chain chain) throws IOException {
Request request = chain.request();
Response response = chain.proceed(request);
String value = request.header("Upgrade");
if (!TextUtils.isEmpty(value) || "upgrade".equals(value)) {
//只有是下载的时候会包含这个Upgrade头信息
return response.newBuilder()
.body(new ProgressResponseBody(response.body()))
.build();
}
return response;
}
}
NetCheckInterceptor用于增加网络断开的拦截
/**
* 网络检测 拦截器
*/
public class NetCheckInterceptor implements Interceptor {
@Override
public Response intercept(Chain chain) throws IOException {
if (!NetworkUtils.isNetworkConnected(AppUtils.getApp())) {
throw new UnknownHostException("no network is connected");
}
return chain.proceed(chain.request().newBuilder().build());
}
}
RequestInterceptor和ResponseInterceptor做为请求头和请求体的处理,需要根据实际服务器设置,代码就不贴了
HttpHeader设定传输的头信息
HttpResult做为最基础最外层的服务器传输数据结构
IApiService做为管理所有网络请求的接口
/**
* 所有网络请求接口管理类
* 根据不同请求配置接口名、参数
*/
public interface IApiService {
@POST("/test/ping")
Observable<HttpResult> testPing();
}
RetrofitApiService做为Retrofit核心类,初始化Retrofit、OKhttp等
public class RetrofitApiService {
private Retrofit mRetrofit;
private IApiService mApiService;
private static class Holder {
private static final RetrofitApiService INSTANCE = new RetrofitApiService();
}
public static RetrofitApiService getInstance() {
return Holder.INSTANCE;
}
private RetrofitApiService() {
OkHttpClient client = new OkHttpClient.Builder()
.addNetworkInterceptor(new HttpLoggingInterceptor().setLevel(HttpLoggingInterceptor.Level.BODY))
.addInterceptor(new NetCheckInterceptor()) //网络连接状态
.addInterceptor(new RequestInterceptor()) //requset
.addInterceptor(new ResponseInterceptor()) //response
.addNetworkInterceptor(new DownloadInterceptor()) //下载进度
.connectTimeout(RetrofitConfig.DEFAULT_TIMEOUT, TimeUnit.SECONDS)
.readTimeout(RetrofitConfig.DEFAULT_TIMEOUT, TimeUnit.SECONDS)
.writeTimeout(RetrofitConfig.DEFAULT_TIMEOUT, TimeUnit.SECONDS) //超时和重连,上同
.cache(new Cache(AppUtils.getApp().getExternalCacheDir(), RetrofitConfig.CACHE_SIZE))
.retryOnConnectionFailure(true) //错误重连
.build();
mRetrofit = new Retrofit.Builder()
.client(client)
.baseUrl(RetrofitConfig.BASE_URL)
.addConverterFactory(GsonConverterFactory.create())
.addCallAdapterFactory(RxJava2CallAdapterFactory.create())
.build();
mApiService = mRetrofit.create(IApiService.class);
}
public IApiService getApiService() {
return mApiService;
}
/**
* ----------------------------
* 根据不同业务请求创建
* ----------------------------
*/
/**
* 测试模块-测试接口
* @return
*/
public Observable<HttpResult> testPing() {
return mApiService.testPing();
}
}
RetrofitConfig管理一些基本参数常量,服务器url、接口版本、超时、缓存大小等
3.屏幕适配方案
参照仿头条的AndroidAutoSize
4.分享框架
如果需要分享平台过多单一集成太过麻烦,推荐大家使用Mob的shareSDK集成框架。
优点:集成简单、使用方便、易于管理、客服强大
自定义一个简单popWindow即可,如果对UI没有要求直接用内部的OneKeyShare,剩下的交给框架去完成
NormalSharePop自定义popwindow,当前制作了qq、微信、朋友圈、微信收藏、新浪微博,可以根据实际需求修改
public class NormalSharePop extends PopupWindow implements OnClickListener{
private LinearLayout wechatBtn, wechatMomentsBtn, wechatFavoriteBtn, qqBtn, sinaWeiBoBtn;
private Button btnCancel;
private View cancelView;
private Context mContext;
private Platform plat;
/**
* 分享参数详解
* http://wiki.mob.com/%e4%b8%8d%e5%90%8c%e5%b9%b3%e5%8f%b0%e5%88%86%e4%ba%ab%e5%86%85%e5%ae%b9%e7%9a%84%e8%af%a6%e7%bb%86%e8%af%b4%e6%98%8e/
*/
private String title;
private String url;
private String siteUrl;
private String site;
private String text;
private String imageUrl;
private String imagePath;
private String titleUrl;
public NormalSharePop(Activity context, ShareBean shareBean) {
super(context);
mContext = context;
View mView = View.inflate(mContext, R.layout.pop_normalshare, null);
wechatBtn = mView.findViewById(R.id.normalshare_wechat);
wechatMomentsBtn = mView.findViewById(R.id.normalshare_wechat_moments);
wechatFavoriteBtn = mView.findViewById(R.id.normalshare_wechat_favorite);
qqBtn = mView.findViewById(R.id.normalshare_qq);
sinaWeiBoBtn = mView.findViewById(R.id.normalshare_sinaweibo);
btnCancel = mView.findViewById(R.id.normalshare_cancel);
cancelView = mView.findViewById(R.id.normalshare_cancel_view);
// 设置按钮监听
wechatBtn.setOnClickListener(this);
wechatMomentsBtn.setOnClickListener(this);
wechatFavoriteBtn.setOnClickListener(this);
qqBtn.setOnClickListener(this);
sinaWeiBoBtn.setOnClickListener(this);
btnCancel.setOnClickListener(this);
cancelView.setOnClickListener(this);
btnCancel.setOnClickListener(this);
//分享内容
title = shareBean.getTitle();
url = shareBean.getUrl();
siteUrl = shareBean.getSiteUrl();
site = shareBean.getSite();
text = shareBean.getText();
imageUrl = shareBean.getImageUrl();
imagePath = shareBean.getImagePath();
titleUrl = shareBean.getTitleUrl();
//设置PopupWindow的View
this.setContentView(mView);
//设置PopupWindow弹出窗体的宽
this.setWidth(ViewGroup.LayoutParams.MATCH_PARENT);
//设置PopupWindow弹出窗体的高
this.setHeight(ViewGroup.LayoutParams.MATCH_PARENT);
//设置PopupWindow弹出窗体可点击
this.setFocusable(true);
//设置SelectPicPopupWindow弹出窗体动画效果
this.setAnimationStyle(R.style.AnimationBottomFade);
//实例化一个ColorDrawable颜色为半透明
ColorDrawable dw = new ColorDrawable(0x60000000);
//设置SelectPicPopupWindow弹出窗体的背景
this.setBackgroundDrawable(dw);
//全屏遮罩
this.setClippingEnabled(false);
//底部状态栏计算
if (UIUtils.isNavigationBarShow(context)) {
int heigth = UIUtils.getNavigationBarHeight(context);
LinearLayout.LayoutParams lp = (LinearLayout.LayoutParams) btnCancel.getLayoutParams();
lp.bottomMargin = heigth;
btnCancel.setLayoutParams(lp);
}
}
@Override
public void onClick(View v) {
int i = v.getId();
if (i == R.id.normalshare_wechat) {
//微信通讯录
if (!CommonUtil.isWeixinAvilible(AppUtils.getApp())) {
ToastUtils.showToast(AppUtils.getApp(), "检查到您手机没有安装微信,请安装后使用该功能");
return;
}
Wechat.ShareParams wechatSP = new Wechat.ShareParams();
wechatSP.setShareType(Wechat.SHARE_WEBPAGE);
wechatSP.setTitle(title);
wechatSP.setText(text);
wechatSP.setUrl(url);
wechatSP.setImagePath(imagePath);
wechatSP.setImageUrl(imageUrl);
plat = ShareSDK.getPlatform(Wechat.NAME);
plat.share(wechatSP);
} else if (i == R.id.normalshare_wechat_moments) {
//微信朋友圈
if (!CommonUtil.isWeixinAvilible(AppUtils.getApp())) {
ToastUtils.showToast(AppUtils.getApp(), "检查到您手机没有安装微信,请安装后使用该功能");
return;
}
Wechat.ShareParams wechatSP2 = new Wechat.ShareParams();
wechatSP2.setShareType(WechatMoments.SHARE_WEBPAGE);
wechatSP2.setTitle(title);
wechatSP2.setText(text);
wechatSP2.setUrl(url);
wechatSP2.setImagePath(imagePath);
wechatSP2.setImageUrl(imageUrl);
plat = ShareSDK.getPlatform(WechatMoments.NAME);
plat.share(wechatSP2);
} else if (i == R.id.normalshare_wechat_favorite) {
//微信收藏
if (!CommonUtil.isWeixinAvilible(AppUtils.getApp())) {
ToastUtils.showToast(AppUtils.getApp(), "检查到您手机没有安装微信,请安装后使用该功能");
return;
}
Wechat.ShareParams wechatSP3 = new Wechat.ShareParams();
wechatSP3.setShareType(WechatFavorite.SHARE_WEBPAGE);
wechatSP3.setTitle(title);
wechatSP3.setText(text);
wechatSP3.setUrl(url);
wechatSP3.setImagePath(imagePath);
wechatSP3.setImageUrl(imageUrl);
plat = ShareSDK.getPlatform(WechatFavorite.NAME);
plat.share(wechatSP3);
} else if (i == R.id.normalshare_qq) {
//qq通讯录
if (!CommonUtil.isQQClientAvailable(AppUtils.getApp())) {
ToastUtils.showToast(AppUtils.getApp(), "检查到您手机没有安装QQ,请安装后使用该功能");
return;
}
QQ.ShareParams qqSP = new QQ.ShareParams();
qqSP.setShareType(QQ.SHARE_WEBPAGE);
qqSP.setTitle(title);
qqSP.setText(text);
qqSP.setUrl(url);
qqSP.setTitleUrl(titleUrl);
qqSP.setSite(site);
qqSP.setSiteUrl(siteUrl);
qqSP.setImagePath(imagePath);
qqSP.setImageUrl(imageUrl);
plat = ShareSDK.getPlatform(QQ.NAME);
plat.share(qqSP);
} else if (i == R.id.normalshare_sinaweibo) {
//新浪微博
SinaWeibo.ShareParams weiboSP = new SinaWeibo.ShareParams();
weiboSP.setShareType(SinaWeibo.SHARE_WEBPAGE);
weiboSP.setTitle(title);
//微博分享链接带入描述,不设置url,否则不能显示图片
weiboSP.setText(text + "\n" + url);
// weiboSP.setUrl(url);
weiboSP.setImagePath(imagePath);
weiboSP.setImageUrl(imageUrl);
plat = ShareSDK.getPlatform(SinaWeibo.NAME);
plat.share(weiboSP);
}else {
dismiss();
}
}
}
ShareBean分享数据结构,基本都罗列了,不用修改
5.其他
utils
比较多,简单介绍几个吧,详细请见项目内代码
dialog,封装一套管理dialog,使用dialogFragment显示,基本上一个项目大多数dialog都是共通的,有几个特殊的单独在里面定义即可
BaseDialog继承Dialog,可以设置dialog的宽高、文案信息等
public class BaseDialog extends Dialog {
private int height, width;
private View mView;
public BaseDialog(Builder builder) {
this(builder, 0);
}
public BaseDialog(Builder builder, int resStyle) {
super(builder.context, builder.resStyle);
init(builder);
}
private void init(Builder builder) {
this.height = builder.height;
this.width = builder.width;
this.mView = builder.view;
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(mView);
Window window = getWindow();
WindowManager.LayoutParams layoutParams = window.getAttributes();
layoutParams.gravity = Gravity.CENTER;
layoutParams.height = height;
layoutParams.width = width;
window.setAttributes(layoutParams);
}
public final static class Builder {
private Context context;
private int height, width;
private View view;
private int resStyle = -1;
public Builder(Context context) {
this.context = context;
}
public Builder layout(int resView) {
view = LayoutInflater.from(context).inflate(resView, null);
return this;
}
public Builder height(int val) {
height = val;
return this;
}
public Builder width(int val) {
width = val;
return this;
}
public Builder style(int resStyle) {
this.resStyle = resStyle;
return this;
}
public Builder addViewOnclick(int viewRes, View.OnClickListener listener) {
View btn = view.findViewById(viewRes);
btn.setVisibility(View.VISIBLE);
btn.setOnClickListener(listener);
return this;
}
public Builder setText(int viewRes, int msgRes) {
View view1 = view.findViewById(viewRes);
if (view1 instanceof Button) {
Button button = (Button) view1;
button.setText(msgRes);
} else if (view1 instanceof TextView) {
TextView textView = (TextView) view1;
textView.setText(msgRes);
} else {
return this;
}
view1.setVisibility(View.VISIBLE);
return this;
}
public Builder setImage(int viewRes, String imgUrl) {
View view1 = view.findViewById(viewRes);
if (view1 instanceof ImageView) {
ImageView iv = (ImageView) view1;
GlideApp.with(context).load(imgUrl)
.diskCacheStrategy(DiskCacheStrategy.RESOURCE)
.into(iv);
} else {
return this;
}
view1.setVisibility(View.VISIBLE);
return this;
}
public Builder setImageBitmap(int viewRes, Bitmap bitmap) {
View view1 = view.findViewById(viewRes);
if (view1 instanceof ImageView) {
ImageView iv = (ImageView) view1;
iv.setImageBitmap(bitmap);
} else {
return this;
}
view1.setVisibility(View.VISIBLE);
return this;
}
public Builder setText(int viewRes, String msg) {
TextView textView = view.findViewById(viewRes);
textView.setVisibility(View.VISIBLE);
textView.setText(msg);
return this;
}
public View getView() {
return view;
}
public BaseDialog build() {
if (resStyle == -1) {
return new BaseDialog(this);
} else {
return new BaseDialog(this, resStyle);
}
}
}
}
BaseDialogFragment做为实际显示的dialog主题,因为是fragment所以生命周期是同步主体的,google也推荐这种方式显示dialog
public class BaseDialogFragment extends DialogFragment {
/**
* 监听弹出窗是否被取消
*/
private OnDialogCancelListener mCancelListener;
/**
* 回调获得需要显示的 dialog
*/
private OnCallDialog mOnCallDialog;
public interface OnDialogCancelListener {
void onCancel();
}
public interface OnCallDialog {
Dialog getDialog(Context context);
}
public static BaseDialogFragment newInstance(OnCallDialog callDialog, boolean cancelable) {
return newInstance(callDialog, cancelable, null);
}
public static BaseDialogFragment newInstance(OnCallDialog callDialog, boolean cancelable, OnDialogCancelListener cancelListener) {
BaseDialogFragment instance = new BaseDialogFragment();
instance.setCancelable(cancelable);
instance.mCancelListener = cancelListener;
instance.mOnCallDialog = callDialog;
return instance;
}
@NonNull
@Override
public Dialog onCreateDialog(Bundle savedInstanceState) {
if (null == mOnCallDialog) {
super.onCreate(savedInstanceState);
}
return mOnCallDialog.getDialog(getActivity());
}
@Override
public void onStart() {
super.onStart();
Dialog dialog = getDialog();
if (dialog != null) {
// 在 5.0 以下的版本会出现白色背景边框,若在 5.0 以上设置则会造成文字部分的背景也变成透明
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.KITKAT) {
// 目前只有这两个 dialog 会出现边框
if (dialog instanceof ProgressDialog || dialog instanceof DatePickerDialog) {
getDialog().getWindow().setBackgroundDrawable(new ColorDrawable(Color.TRANSPARENT));
}
}
Window window = getDialog().getWindow();
WindowManager.LayoutParams windowParams = window.getAttributes();
window.setAttributes(windowParams);
}
}
@Override
public void onCancel(DialogInterface dialog) {
super.onCancel(dialog);
if (mCancelListener != null) {
mCancelListener.onCancel();
}
}
}
DialogFragmentHelper做为dialog管理类,已经定义几个常用dialog,可以根据实际需求再定义自己属于自己项目的dialog
定义一个dialog需要一个tag,然后定义方法,参数必须传入FragmentManager,剩下根据需要传入宽高、title、点击事件等,
然后在getDialog中创建dialog....大致模仿下面即可
/**
* ------------------------------------------------------
* 根据自己项目定义dialog
* ------------------------------------------------------
*/
/**
* 信息+取消btn+确定btn
*/
private static final String CONFIRM_TAG = TAG_HEAD + ":confirm";
public static void showNoTitleConfirmDialog(FragmentManager fragmentManager, final String message
, boolean cancelable, final View.OnClickListener positiveListener) {
sBaseDialogFragment = BaseDialogFragment.newInstance(new BaseDialogFragment.OnCallDialog() {
@Override
public Dialog getDialog(Context context) {
BaseDialog.Builder builder = new BaseDialog.Builder(context);
int width = UIUtils.dp2px(context, 280);
int height = UIUtils.dp2px(context, 150);
BaseDialog dialog = builder.layout(R.layout.dialog_no_title_confirm).style(BASE_THEME).height(height).width(width)
.setText(R.id.tv_diaglog_msg, message)
.addViewOnclick(R.id.tv_dialog_cancel, new View.OnClickListener() {
@Override
public void onClick(View v) {
sBaseDialogFragment.dismissAllowingStateLoss();
}
})
.addViewOnclick(R.id.tv_dialog_ok, new View.OnClickListener() {
@Override
public void onClick(View view) {
positiveListener.onClick(view);
sBaseDialogFragment.dismissAllowingStateLoss();
}
}).build();
return dialog;
}
}, cancelable);
sBaseDialogFragment.show(fragmentManager, CONFIRM_TAG);
}
未完待续