通用库
在开发android应用时,一般会使用一些现有库来缩短开发周期,将代码进行模块化;
使用框架虽然可能会增加最终代码量,但在开发过程中会非常方便。
android从出生到现在已经很多年了,因此有大量的库可供使用,android使用的技能不会特别多,但比较杂,虽然所有的代码都可以通过手✖完成,但这个过程肯定会相当痛苦。
很多库使用率都很高,通过github上的star数量可以分辨出哪些库比较主流,通过更新频率也可以查看哪些库依然在维护。现整理一些比较好的库,同时通过这些项目来搭建出一般android项目可以直接通用的框架代码。
一、框架需设计的库
虽然说一般框架不需要太多的内容,不过要实现大部分的功能,还是需要引用较多的资源库的,这里进行列举,然后接下来进行简单的说明:
- 基本通用包(系统自带或者support库提供):
- multidex:dex分包处理
- constraint
- palette:着色
- cardview:卡片布局
- design
- 数据库:
- litepal
- 自动注入/变量赋值框架:
- dagger:为成员变量等自动赋值
- butterknife:依据xml自动生成变量并赋值,点击监听
- 线程任务处理:
- Rxjava(RxAndroid)
- 网络任务:
- retrofit
- okhttp:http核心库
- 数据类型转换:
- Gson:json数据处理
- 图片处理:
- Glide:图片下载裁剪
- zxing-library:二维码图片识别或生成
- takephoto:从设备中选择图片或拍照获得图片
- 工具类:
- utilcode:这是工具库集合,提供各种常用的操作处理
- statusbarutil:状态栏工具类
- 调试时检测优化:
- leakcanary-android
- 日志工具
- Logger:打印日志
- 弹出框:
- pickerview:时间选择器、条件选择器
- 权限处理:
- permissionsdispatcher:无反射动态请求权限
- 自定义view:
- flowlayout:tag组
- xrecyclerview:可上拉加载下拉刷新的recyclerview
- convenientbanner:轮播插件
- BottomNavigationViewEx:底部导航栏(多tab视图)
- roundedimageview:圆角头像,可存在border
- gridPasswordView:密码框
- badge:角标库
- 界面跳转:
- ARouter:阿里推出的库,可实现路由跳转、拦击、降级、参数自动注入
- 其他:
- pushsdk:友盟统计、消息推送(module形式)
- rxdownload:基于Rxjava的用于软件更新的库
- logging-interceptor:网络请求时打印信息
- adapter-rxjava:将网络请求变为Rxjava监听形式
- converter-gson:网络返回数据(若为gson格式)转为bean
大部分的库都已经列举出来了;
其中一些库如自定义View等内容可以参考github源码,也可以在项目中找到对应使用的位置
对于友盟推送等内容,也已经在代码中集成;同时在BaseApplication类中进行了初始化
动态权限库自身在AndroidStudio中是有插件的,并且源码中启动Activity就是示例;
LocationPickerView也提供了自定义View的模版。
其中有些库功能很强大,但使用起来会进行其他配置,因此单独说明:
二、网络请求模块
该模块由Rxjava、RxAndroid、logging-interceptor、adapter-rxjava、converter-gson、retrofit、okhttp等项目包组成,将网络请求、数据转换、异常处理、日志输出等功能内聚到一起,整个架构可用简图概括(只是草图):
这样用户只需要将网络请求的字段传入,并定义可以处理的结构以及异常情况下的处理,就可以省略中间的具体的过程。
具体实现如下:
@Provides
@Singleton
HttpInterface provideHttpInterface() {
//网络请求的Host
String baseUrl = BaseApplication.app.getBaseNetUrl();
//生成JSON转换的库
Gson gson = new GsonBuilder()
.serializeNulls()
.setDateFormat("yyyy:MM:dd HH:mm:ss")
.create();
GsonConverterFactory gsonConverterFactory = GsonConverterFactory.create(gson);
//生成RxJava转换的adapter
RxJava2CallAdapterFactory rxJava2CallAdapterFactory = RxJava2CallAdapterFactory.create();
//生成OkHttp网络传输的客户端
HashMap<String, List<Cookie>> cookieStore = new HashMap<>();
OkHttpClient okHttpClient = new OkHttpClient.Builder()
.cookieJar(new CookieJar() {
@Override
public void saveFromResponse(HttpUrl url, List<Cookie> cookies) {
cookieStore.put(url.host(), cookies);
}
@Override
public List<Cookie> loadForRequest(HttpUrl url) {
List<Cookie> cookies = cookieStore.get(url.host());
return cookies != null ? cookies : new ArrayList<>();
}
})
.addInterceptor(chain -> {
Request request = chain.request()
.newBuilder()
.addHeader("SDK", String.valueOf(Build.VERSION.SDK_INT ))
.build();
return chain.proceed(request);
})
.addInterceptor(new HttpLoggingInterceptor().setLevel(HttpLoggingInterceptor.Level.BODY))
.addNetworkInterceptor(new StethoInterceptor())
.connectTimeout(2000, TimeUnit.MILLISECONDS)
.readTimeout(2000, TimeUnit.MILLISECONDS)
.writeTimeout(2000,TimeUnit.MILLISECONDS)
.build();
//最后组合成Retrofit对象
Retrofit retrofit = new Retrofit.Builder()
.addConverterFactory(gsonConverterFactory)
.addCallAdapterFactory(rxJava2CallAdapterFactory)
.baseUrl(baseUrl)
.client(okHttpClient)
.build();
//将注解后的interface请求接口转换为真正可用的网络请求对象
return retrofit.create(HttpInterface.class);
}
可以看到,最后其实是生成了HttpInterface对象。这个HttpInterface对象是自定义的,自身是接口类型,用于定义网络请求所需的参数,以及可以处理的返回数据的类型,如假如需要请求一次网络:
public interface HttpInterface {
/**
* 登录
*
* @param username 用户名
* @param pwd 密码
* @return 登录返回对象
*/
@POST("login/doLogin")
@FormUrlEncoded
Observable<BaseHttpBean<Object>> doLogin(@Field("username") String username,
@Field("pwd") String pwd);
}
如上一般,只需要定义参数,以及可解析的数据对象即可。
然后发起网络请求:
/**
* 举例请求网络数据
*/
@OnClick(R.id.fab)
public void onViewClicked() {
httpInterface.doLogin("name", "password")
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(new Consumer<BaseHttpBean<Object>>() {
@Override
public void accept(BaseHttpBean<Object> objectBaseHttpBean) throws Exception {
// TODO: 2018/3/10 成功
}
}, new Consumer<Throwable>() {
@Override
public void accept(Throwable throwable) throws Exception {
// TODO: 2018/3/10 失败
}
});
}
网络请求的超时判断、数据 转换等操作,观察者方法执行时,就已经完成,如果需要添加其他的网络任务,只需要在HttpInterface接口中添加对应的方法即可。
三、自动注入模块
通过dagger可以把已经注解的成员变量进行赋值,只要提供对应类型的提供方即可;
利用dagger框架也可以很方便的实现MVP模式,同时可以省略View层与Presenter层的赋值操作,只要有需要对应变量的地方,直接通过@Inject注解即可。
Butterknife则专注于 对xml布局文件中,变量的生成,以及控件的点击监听,同时ButterKnife还在Androidstudio上实现了plugin,使用起来比较方便。直接百度即可,这里不额外说明;
以刚才的网络请求模块,可能需要在多处调用HttpInterface的实例,这时就可以 通过dagger来完成这一功能:
1、为dagger定义变量作用范围
一般按照模版,定义两个注解即可:
定义Activity作用范围:
/**
* 功能----定义每个activity的生命周期,供dagger框架使用
* <p>
* Created by MNLIN on 2017/9/22.
*/
@Scope
@Documented
@Retention(RetentionPolicy.RUNTIME)
public @interface PerActivity {
}
定义Fragment作用范围:
/**
* 功能----fragment对应dagger的生命周期控制
* <p>
* Created by MNLIN on 2017/9/23.
*/
@Scope
@Documented
@Retention(RetentionPolicy.RUNTIME)
public @interface PerFragment {
}
2、定义作用范围内需要的变量,或全局变量
Activity和Fragment作用范围内的变量是局部的;
Application作用范围是全局的(Activity和Fragment需设置依赖Application的compont);
这点是通过compont组件来保证的;
这里只写出全局模式变量的注入方法:
/**
* 功能----应用的组件
* <p>
* Created by MNLIN on 2017/9/22.
*/
@Singleton
@Component(modules = ApplicationModule.class)
public interface ApplicationComponent {
void inject(BaseApplication application);
HttpInterface initHttpInterface();
}
可以看到,在ApplicationComponent中声明了HttpInterface,这样所有的位置只要通过@Inject进行注解,就可以获取对应实例。
而该实例的创建则是通过module提供的,这点对于Activity,Fragment,Application相同:
/**
* 功能----Application的module,为ApplicationComponent提供对象生成器
* Created by MNLIN on 2017/9/22
*/
@Singleton
@Module
public class ApplicationModule {
String tag = "";
private BaseApplication application;
public ApplicationModule(BaseApplication application) {
this.application = application;
}
@Provides
@Singleton
HttpInterface provideHttpInterface() {
//网络请求的Host
String baseUrl = BaseApplication.app.getBaseNetUrl();
//生成JSON转换的库
Gson gson = new GsonBuilder()
.serializeNulls()
.setDateFormat("yyyy:MM:dd HH:mm:ss")
.create();
GsonConverterFactory gsonConverterFactory = GsonConverterFactory.create(gson);
//生成RxJava转换的adapter
RxJava2CallAdapterFactory rxJava2CallAdapterFactory = RxJava2CallAdapterFactory.create();
//生成OkHttp网络传输的客户端
HashMap<String, List<Cookie>> cookieStore = new HashMap<>();
OkHttpClient okHttpClient = new OkHttpClient.Builder()
.cookieJar(new CookieJar() {
@Override
public void saveFromResponse(HttpUrl url, List<Cookie> cookies) {
cookieStore.put(url.host(), cookies);
}
@Override
public List<Cookie> loadForRequest(HttpUrl url) {
List<Cookie> cookies = cookieStore.get(url.host());
return cookies != null ? cookies : new ArrayList<>();
}
})
.addInterceptor(chain -> {
Request request = chain.request()
.newBuilder()
.addHeader("SDK", String.valueOf(Build.VERSION.SDK_INT ))
.build();
return chain.proceed(request);
})
.addInterceptor(new HttpLoggingInterceptor().setLevel(HttpLoggingInterceptor.Level.BODY))
.addNetworkInterceptor(new StethoInterceptor())
.connectTimeout(2000, TimeUnit.MILLISECONDS)
.readTimeout(2000, TimeUnit.MILLISECONDS)
.writeTimeout(2000,TimeUnit.MILLISECONDS)
.build();
//最后组合成Retrofit对象
Retrofit retrofit = new Retrofit.Builder()
.addConverterFactory(gsonConverterFactory)
.addCallAdapterFactory(rxJava2CallAdapterFactory)
.baseUrl(baseUrl)
.client(okHttpClient)
.build();
//将注解后的interface请求接口转换为真正可用的网络请求对象
return retrofit.create(HttpInterface.class);
}
}
通过这样一个流程,就可以完成前期设定工作,当然还需要最后一步:
3、在基类中初始化
之前只是进行了准备,但是真正注入还是需要在代码执行时候来启动,因此在BaseActivity、BaseFragment、BaseApplication中需要添加初始化代码:
这里只显示Application部分:BaseApplication类:
//注入dagger框架
applicationComponent = DaggerApplicationComponent.builder().applicationModule(new ApplicationModule(this)).build();
applicationComponent.inject(this);
具体详细设置可以参考源码;
四、跳转拦截模块——ARouter
这个模块虽然基本功能看上去和平时使用startActivity的方式差不了多少,但是真正带入后会发现很多精巧的地方,表面上看起来是提供路由跳转的其他一种方式,只是高端一些,不过真正使用起来会发现出奇的好。
1、定义字符串,供路由跳转
一般来说service使用的情况比Activity和Fragment要少的多,因此如果需要使用service的话,可以前往ARouter的GITHUB进行查看。
ARouter框架提供了:路由跳转,碎片生成,降级,自定义序列化等服务;不过用的虽然不多,但比较精巧的则是权限判断;
引用ARouter自己的说明:
6.为目标页面声明更多信息
// 我们经常需要在目标页面中配置一些属性,比方说"是否需要登陆"之类的
// 可以通过 Route 注解中的 extras 属性进行扩展,这个属性是一个 int值,换句话说,单个int有4字节,也就是32位,可以配置32个开关
// 剩下的可以自行发挥,通过字节操作可以标识32个开关,通过开关标记目标页面的一些属性,在拦截器中可以拿到这个标记进行业务逻辑判断
@Route(path = "/test/activity", extras = Consts.XXXX)
具体功能可以查看项目代码:
以下给出引导部分:
/**
* 功能----路径跳转activity/fragment
*
* <p>
* Created by MNLIN on 2017/11/24.
*/
public final class ARouterConst {
/**
* 无权限
* 登录
* 绿色通道(若设定则无法跳转,相当于禁止功能)
* activity启动:清除任务栈
*/
public static final int FLAG_NONE = 0x00000000;
public static final int FLAG_LOGIN = 0x00000003;
public static final int FLAG_FORCE_ACCESS = 0x00000040;
public static final int FLAG_ACTIVITY_CLEAR_TOP = 0x00000200;
/**
* activity/fragment
*/
public static final String Activity_SelectFunctionActivity = "/activity/SelectFunctionActivity";
public static final String Activity_SearchFilterActivity = "/activity/SearchFilterActivity";
public static final String Fragment_WalletFragment = "/fragment/WalletFragment";
}
这里下方以Activity_或者Fragment_开头的常量提供ARouter路由进行最基本的功能跳转,而上方则定义了项目中可能会用到的“权限”;
假设现在有一场景,活动A向活动B进行跳转,此时要求用户已经登录,否则的话就需要中断跳向B的操作,直接跳转到活动C;
一般情况下可能我们会在B中进行判断,若是未登录的话,则主动将页面路由到C,可是这样的话,实际并没有阻止跳转操作,若是C中一开始就进行危险操作,则可能会直接崩溃。
或者说可以在A中进行判断,但如此一来,若是向B跳转的活动过多,就需要在多个活动中编写同样的代码,当然这可以编写工具类,但若是页面过多,这种逻辑会很麻烦.
而ARouter则相当于在跳转过程中进行拦截,所有不符合条件的跳转都将另行处理:
/**
* function : 跳转拦截器(权限拦截)
* <p>
* 比较经典的应用就是在跳转过程中处理登陆事件,这样就不需要在目标页重复做登陆检查
* 拦截器会在跳转之间执行,多个拦截器会按优先级顺序依次执行
*
* @author MNLIN
*/
@Interceptor(priority = 2, name = "ARouter跳转拦截器")
public class ARouterInterceptor implements IInterceptor {
@Override
public void process(Postcard postcard, InterceptorCallback callback) {
Logger.v("######界面跳转 : " + postcard.toString());
//当前所有的权限
String[] permissions = new String[]{
"登录",
"绿色通道",//若目标此flag设定,则表示禁止跳转
"栈单例模式"//若目标设置此flag,则添加singTask标志
};
int[] FLAGS_ALL = new int[]{
ARouterConst.FLAG_LOGIN,
ARouterConst.FLAG_FORCE_ACCESS,
ARouterConst.FLAG_ACTIVITY_CLEAR_TOP
};
//当前所有权限对应的boolean值;为false则对应权限设为 ARouterConst.FLAG_NONE
boolean[] FLAGS_ALL_VALUE = new boolean[]{
DefaultPreferenceUtil.getInstance().hasLogin(),
false,
false
};
//当前所有的权限
int currentFlags = Integer.MIN_VALUE;
for (int position = 0; position < FLAGS_ALL.length; position++) {
currentFlags |= FLAGS_ALL_VALUE[position] ? FLAGS_ALL[position] : ARouterConst.FLAG_NONE;
}
Logger.v("######当前所有权限 : " + Integer.toBinaryString(currentFlags));
//目标界面需要的权限
int requireFlags = postcard.getExtra() | Integer.MIN_VALUE;
Logger.v("######目标所需权限 : " + Integer.toBinaryString(requireFlags));
//如果需要的权限都已存在,则直接跳转,不做处理
if ((requireFlags & currentFlags) == requireFlags) {
callback.onContinue(postcard);
return;
}
//如果发现不一致,说明某些权限不存在,则需要依次判断哪个权限不存在
for (int position = 0; position < FLAGS_ALL.length; position++) {
if ((requireFlags & FLAGS_ALL[position]) != 0 && (currentFlags & FLAGS_ALL[position]) == 0) {
// TODO: 2018/1/20 没有对应的f权限
boolean consume = false;
switch (position) {
case 0: //未登录
consume = dispatchLogin(postcard, callback);
break;
case 1:
break;
case 2: //栈单例模式
consume = dispatchSingleTask(postcard, callback);
break;
default: {
callback.onInterrupt(new RuntimeException("没有 " + permissions[position] + " 权限"));
}
}
if (!consume) {
callback.onInterrupt(new RuntimeException("界面无法跳转"));
}
return;
}
}
//权限定义错误
RxBus.getInstance().post(new BaseEvent(Const.SHOW_TOAST, "未知权限"));
}
/**
* 请求单例启动
*
* 清除栈上其他活动
*/
private boolean dispatchSingleTask(Postcard postcard, InterceptorCallback callback) {
postcard.withFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TOP);
callback.onContinue(postcard);
return true;
}
/**
* 处理未登录操作
*/
private boolean dispatchLogin(Postcard postcard, InterceptorCallback callback) {
RxBus.getInstance().post(new BaseEvent(Const.SHOW_LOGIN_DIALOG, null));
return false;
}
@Override
public void init(Context context) {
// 拦截器的初始化,会在sdk初始化的时候调用该方法,仅会调用一次
}
/**
* 更换意图的跳转路径
* 然后进行跳转处理
*
* @param postcard 意图
* @param des 目的 string
*/
private void replaceDes(Postcard postcard, String des) {
//动态的修改postcard信息,更换跳转路径
Postcard newPostcard = ARouter.getInstance().build(des);
LogisticsCenter.completion(newPostcard);
postcard.setPath(newPostcard.getPath()).setGroup(newPostcard.getGroup()).setDestination(newPostcard.getDestination());
}
}
如代码所示,可以分为四个步骤:
- 获取当前应用拥有的所有权限
- 获取目标页面所需的权限
- 进行判断,若是目标页面所需权限当前已拥有,则跳转成功;否则进行步骤4
- 按顺序判断是什么权限未满足,通过 dispatch*** 方法进行处理
同时 项目中放置了很多方便操作的工具类以及代码生成器;
如在 build.gradle 中添加了生成MVP模式类的代码:
/**
* Desc: generate mvp architecture code automatically
*
* @Des For example: gradle mvp -D domain="Test" -D path="/activity"
* def domainParam = System.getProperty('domain')
* def pathParam = System.getProperty('path')
*
* */
task mvp_activity(type: GenerateMVPActivity) {
group 'personal'
description 'generate java code for MVP-Activity architecture'
def domainParam = "Test"
def pathParam = "/activity"
if (domainParam && pathParam) {
domain domainParam
uiPath pathParam
}
}
使用时将 domainParam 重新赋值,然后通过任务就可以生成相应的代码。
任务执行可以通过AndroidStudio右侧的任务列表进行:
也可以在该任务上: 右键 => Assign ShortCut => 添加快捷键
以后每次生成代码,只需要修改domainParam 字段值,然后使用快捷键生成代码即可,不过要注意: 不能使用相同名字的Activity及Fragment,会相互覆盖;
功能一般而言足够普通项目使用,只需要在使用时进行:修改包名,设置友盟KEY 等操作后就可以添加业务处理了。