Dependency Injection with Dagger 2

Dependency Injection with Dagger 2

原文:Dependency Injection with Dagger 2

创建单例

下面展示一个简单的Dagger来管理所有单例创建的例子:

Dagger2 使用

Dagger

Dagger 是一款依赖注入框架。

项目地址:dagger

官方文档:Dagger

依赖注入

依赖注入(Dependency Injection),简称DI,又叫控制反转(Inversion of Control),简称IOC。
当一个类的实例需要另另一个类的实例进行协助时,在传统的设计中,通常由调用者来创建被调用者的实例,然而依赖注入的方式,创建被调用者不再由调用者创建实例,创建被调用者的实例的工作由IOC容器来完成,然后注入到调用者。因此也被称为依赖注入。

作用:将各层的对象以松耦合的方式组织在一起,解耦,各层对象的调用完全面向接口。当系统重构或者修改的时候,代码的改写量将大大减少。

Android Studio 引入Dagger2

1
2
3
4
implementation 'com.google.dagger:dagger-android:2.17'
implementation 'com.google.dagger:dagger-android-support:2.17'
annotationProcessor 'com.google.dagger:dagger-compiler:2.17'
annotationProcessor 'com.google.dagger:dagger-android-processor:2.17'

Android 屏幕适配

屏幕适配

前言

由于Android系统的开放性,任何用户、开发者、硬件厂商、运营商都可以对Android系统和硬件进行定制,修改成他们想要的样子。 那么这种“碎片化”到达什么程度呢?

Android屏幕碎片化

以上每一个矩形都代表一种机型,且它们屏幕尺寸、屏幕分辨率大相径庭。随着Android设备的增多,设备碎片化、系统碎片化、屏幕尺寸碎片化、屏幕碎片化的程度也在不断加深。

当 Android 系统、屏幕尺寸、屏幕密度出现碎片化的时候,就很容易出现同一元素在不同手机上显示不同的问题。试想一下这么一个场景: 为 4.3 寸屏幕准备的 UI 设计图,运行在 5.0 寸的屏幕上,很可能在右侧和下侧存在大量的空白;而 5.0 寸的 UI 设计图运行到 4.3 寸的设备上,很可能显示不下。

为了保证用户获得一致的用户体验效果,使得某一元素在 Android 不同尺寸、不同分辨率的、不同系统的手机上具备相同的显示效果,能够保持界面上的效果一致,我们需要对各种手机屏幕进行适配!

基本概念

像素(px):

单位:px(pixel),像素就是手机屏幕的最小构成单元

分辨率

px(pixel),1px = 1像素点,手机在横向、纵向上的像素点数总和 一般描述成 宽高 ,即横向像素点个数 纵向像素点个数(如1080 x 1920)。

屏幕尺寸

单位 英寸(inch),手机对角线的物理尺寸

屏幕像素密度

单位:dpi,每英寸的像素点数。 例如每英寸内有 160 个像素点,则其像素密度为 160dpi。计算公式:像素密度 = 像素 / 尺寸 (dpi = px / in)

密度无关像素

density-independent pixel,叫dp或dip,与终端上的实际物理像素点无关。单位:dp,可以保证在不同屏幕像素密度的设备上显示相同的效果,是安卓特有的长度单位。dp 与 px 的转换:1dp = (dpi / 160 ) * 1px

独立比例像素

scale-independent pixel,叫sp或sip。单位:sp,字体大小专用单位 Android开发时用此单位设置文字大小。

DPI 的存在,不就是为了让大屏能显示更多的内容

适配方案

今日头条

参考项目地址:https://github.com/JessYanCoding/AndroidAutoSize

今日头条适配方案默认项目中只能以高或宽中的一个作为基准。

density 是 DisplayMetrics 中的成员变量,而 DisplayMetrics 实例通过 Resources#getDisplayMetrics 可以获得,而Resouces 通过 Activity 或者 Application 的 Context 获得。

如果每个 Viewdp 值是固定不变的,那我们只要保证每个设备的屏幕总 dp 宽度不变,就能保证每个 View 在所有分辨率的屏幕上与屏幕的比例都保持不变,从而完成等比例适配,并且这个屏幕总 dp 宽度如果还能保证和设计图的宽度一致的话,那我们在布局时就可以直接按照设计图上的尺寸填写 dp

屏幕的总 px 宽度 / density = 屏幕的总 dp 宽度

在这个公式中我们要保证 屏幕的总 dp 宽度设计图总宽度 一致,并且在所有分辨率的屏幕上都保持不变,我们需要怎么做呢?屏幕的总 px 宽度 每个设备都不一致,这个值是肯定会变化的,这时今日头条的公式就派上用场了

当前设备屏幕总宽度(单位为像素)/ 设计图总宽度(单位为 dp) = density

这个公式就是把上面公式中的 屏幕的总 dp 宽度 换成 设计图总宽度,原理都是一样的,只要 density 根据不同的设备进行实时计算并作出改变,就能保证 设计图总宽度 不变,也就完成了适配。

布局文件中 dp 的转换,最终都是调用 TypedValue#applyDimension(int unit, float value, DisplayMetrics metrics) 来进行转换:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public static float applyDimension(int unit, float value, DisplayMetrics metrics){
switch (unit) {
case COMPLEX_UNIT_PX:
return value;
case COMPLEX_UNIT_DIP:
return value * metrics.density;
case COMPLEX_UNIT_SP:
return value * metrics.scaledDensity;
case COMPLEX_UNIT_PT:
return value * metrics.xdpi * (1.0f/72);
case COMPLEX_UNIT_IN:
return value * metrics.xdpi;
case COMPLEX_UNIT_MM:
return value * metrics.xdpi * (1.0f/25.4f);
}
return 0;
}

这里用到的 DisplayMetrics 正是从 Resources 中获得的。图片的decode,也是通过 DisplayMetrics 中的值来计算的。

当然还有些其他 dp 转换的场景,基本都是通过 DisplayMetrics 来计算的,这里不再详述。因此,想要满足上述需求,我们只需要修改 DisplayMetrics 中和 dp 转换相关的变量即可。

适配后的 density = 设备真实宽(单位px) / 360,接下来只需要把我们计算好的 density 在系统中修改下即可,代码实现如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
private static float sNonCompatDensity;
private static float sNonCompatScaledDensity;

private static void setCustomDensity(@NonNull Activity activity, @NonNull final Application application) {
// 设计图宽度
int customWidth = 360;
final DisplayMetrics appDisplayMetrics = application.getResources().getDisplayMetrics();

if (sNonCompatDensity == 0) {
sNonCompatDensity = appDisplayMetrics.density;
sNonCompatScaledDensity = appDisplayMetrics.scaledDensity;
// 监听系统字体变化
application.registerComponentCallbacks(new ComponentCallbacks() {
@Override
public void onConfigurationChanged(Configuration newConfig) {
if (newConfig != null && newConfig.fontScale > 0) {
sNonCompatScaledDensity = application.getResources().getDisplayMetrics().scaledDensity;
}
}

@Override
public void onLowMemory() {

}
});
}

final float targetDensity = appDisplayMetrics.widthPixels / customWidth;
final float targetScaledDensity = targetDensity * (sNonCompatScaledDensity / sNonCompatDensity);
final int targetDensityDpi = (int) (160 * targetDensity);

appDisplayMetrics.density = targetDensity;
appDisplayMetrics.scaledDensity = targetScaledDensity;
appDisplayMetrics.densityDpi = targetDensityDpi;

DisplayMetrics activityDisplayMetrics = activity.getResources().getDisplayMetrics();
activityDisplayMetrics.density = targetDensity;
activityDisplayMetrics.scaledDensity = targetScaledDensity;
activityDisplayMetrics.densityDpi = targetDensityDpi;
}

优点

  • 使用成本非常低,操作非常简单,使用该方案后在页面布局时不需要额外的代码和操作,这点可以说完虐其他屏幕适配方案。
  • 侵入性非常低,该方案和项目完全解耦,在项目布局时不会依赖哪怕一行该方案的代码,而且使用的还是 Android 官方的 API,意味着当你遇到什么问题无法解决,想切换为其他屏幕适配方案时,基本不需要更改之前的代码,整个切换过程几乎在瞬间完成,会少很多麻烦,节约很多时间,试错成本接近于 0。
  • 可适配三方库的控件和系统的控件(不止是是 ActivityFragmentDialogToast 等所有系统控件都可以适配),由于修改的 density 在整个项目中是全局的,所以只要一次修改,项目中的所有地方都会受益。
  • 不会有任何性能的损耗

缺点

会影响第三方控件

解决方案:

  1. 我们自身作出修改,去适应三方库的设计图尺寸,我们将项目自身的设计图尺寸修改为这个三方库的设计图尺寸,就能完成项目自身和三方库的适配
  2. Activity 为单位,取消当前 Activity 的适配效果,改用其他的适配方案

SmallestWidth

这个方案的的使用方式和我们平时在布局中引用 dimens 无异,核心点在于生成 dimens.xml 文件,但是已经有大神帮我们做了这 一步

1
2
3
4
5
6
7
8
9
├── src/main
│ ├── res
│ ├── ├──values
│ ├── ├──values-800x480
│ ├── ├──values-860x540
│ ├── ├──values-1024x600
│ ├── ├──values-1024x768
│ ├── ├──...
│ ├── ├──values-2560x1440

如果有人还记得上面这种 宽高限定符屏幕适配方案 的话,就可以把 smallestWidth 限定符屏幕适配方案 当成这种方案的升级版,smallestWidth 限定符屏幕适配方案 只是把 dimens.xml 文件中的值从 px 换成了 dp,原理和使用方式都是没变的,这些在上面的文章中都有介绍,下面就直接开始剖析原理,smallestWidth 限定符屏幕适配方案 长这样

1
2
3
4
5
6
7
8
9
10
11
├── src/main
│ ├── res
│ ├── ├──values
│ ├── ├──values-sw320dp
│ ├── ├──values-sw360dp
│ ├── ├──values-sw400dp
│ ├── ├──values-sw411dp
│ ├── ├──values-sw480dp
│ ├── ├──...
│ ├── ├──values-sw600dp
│ ├── ├──values-sw640dp

其实 smallestWidth 限定符屏幕适配方案 的原理也很简单,开发者先在项目中根据主流屏幕的 最小宽度 (smallestWidth) 生成一系列 values-swdp 文件夹 (含有 dimens.xml 文件),当把项目运行到设备上时,系统会根据当前设备屏幕的 最小宽度 (smallestWidth) 去匹配对应的 values-swdp 文件夹,而对应的 values-swdp 文件夹中的 dimens.xml 文字中的值,又是根据当前设备屏幕的 最小宽度 (smallestWidth) 而定制的,所以一定能适配当前设备。

如果系统根据当前设备屏幕的 最小宽度 (smallestWidth) 没找到对应的 values-swdp 文件夹,则会去寻找与之 最小宽度 (smallestWidth) 相近的 values-swdp 文件夹,系统只会寻找小于或等于当前设备 最小宽度 (smallestWidth)values-swdp,这就是优于 宽高限定符屏幕适配方案 的容错率,并且也可以少生成很多 values-swdp 文件夹,减轻 App 的体积。

优点

  1. 非常稳定,极低概率出现意外
  2. 不会有任何性能的损耗
  3. 适配范围可自由控制,不会影响其他三方库
  4. 在插件的配合下,学习成本低

缺点

  1. 在布局中引用 dimens 的方式,虽然学习成本低,但是在日常维护修改时较麻烦
  2. 侵入性高,如果项目想切换为其他屏幕适配方案,因为每个 Layout 文件中都存在有大量 dimens 的引用,这时修改起来工作量非常巨大,切换成本非常高昂
  3. 无法覆盖全部机型,想覆盖更多机型的做法就是生成更多的资源文件,但这样会增加 App 体积,在没有覆盖的机型上还会出现一定的误差,所以有时需要在适配效果和占用空间上做一些抉择
  4. 如果想使用 sp,也需要生成一系列的 dimens,导致再次增加 App 的体积
  5. 不能自动支持横竖屏切换时的适配,如上文所说,如果想自动支持横竖屏切换时的适配,需要使用 values-wdp屏幕方向限定符 再生成一套资源文件,这样又会再次增加 App 的体积
  6. 不能以高度为基准进行适配,考虑到这个方案的名字本身就叫 最小宽度限定符适配方案,所以在使用这个方案之前就应该要知道这个方案只能以宽度为基准进行适配,为什么现在的屏幕适配方案只能以高度或宽度其中的一个作为基准进行适配

AndroidAutoSize

根据 今日头条屏幕适配方案 优化的屏幕适配框架。

其他方案

UI适配框架

例如 Android 屏幕适配方案,不过已经停止维护

宽高限定符适配

1
2
3
4
5
6
7
8
9
├── src/main
│ ├── res
│ ├── ├──values
│ ├── ├──values-800x480
│ ├── ├──values-860x540
│ ├── ├──values-1024x600
│ ├── ├──values-1024x768
│ ├── ├──...
│ ├── ├──values-2560x1440

Android 屏幕适配终结者

Android 屏幕适配终结者 ,也是基于头条的原理,不过是操作 pt,所以不是改 DisplayMetrics#density,而是 DisplayMetrics#xdpi,由于适配不会失效

参考资料

RxJava系列(一) 入门

RxJava 入门

  1. 入门(概念、基础使用)
  2. 进阶(操作符(map、flatmap、zip、defer、contatMap等等),背压等)
  3. 实战(网络请求等)
  4. 源码解析(变换、线程调度,源码探讨)

概念

下面是摘自 ReactiveX 官网 的一段话。

ReactiveX is a library for composing asynchronous and event-based programs by using observable sequences.(一个在 Java VM 上使用可观测的序列来组成异步的、基于事件的程序的库)

其中提到了几个概念,观察者模式、响应式编程。

响应式编程

概念:响应式编程是一种通过异步和数据流来构建事物关系的编程模型。

一般的编码模式中,“人”在其中扮演了过重的角色,关心程序中的每一部分。某种意义上这是一种顺序性思维的编程,我要做什么,然后做什么,最后做什么,按部就班编写就好了。具体如下图:

顺序性思维

而响应式编程,全都是事物与事物之间的关系,解脱了”人”,之后一个事物发生变化另一个事物就自动响应。如下:

响应式编程

个人感觉响应式编程就是用异步数据流进行编程。流是响应式的核心,可以基于任何东西创建数据流,响应式编程就是根据数据流的流向进行一系列的操作。

观察者模式

观察者模式(Observer Pattern):定义了对象间的一种一对多的依赖关系,当一个对象状态发生改变时,其相关依赖对象皆得到通知并被自动更新。观察者模式又叫做发布-订阅(Publish/Subscribe)模式、模型-视图(Model/View)模式、源-监听器(Source/Listener)模式或从属者(Dependents)模式。

观察者模式面向的需求是:A 对象(观察者)对 B 对象(被观察者)的某种变化高度敏感,需要在 B 变化的一瞬间做出反应。举个例子,新闻里喜闻乐见的警察抓小偷,警察需要在小偷伸手作案的时候实施抓捕。在这个例子里,警察是观察者,小偷是被观察者,警察需要时刻盯着小偷的一举一动,才能保证不会漏过任何瞬间。程序的观察者模式和这种真正的『观察』略有不同,观察者不需要时刻盯着被观察者(例如 A 不需要每过 2ms 就检查一次 B 的状态),而是采用 注册(Register) 或者称为 订阅(Subscribe) 的方式,告诉被观察者:我需要你的某某状态,你要在它变化的时候通知我。 Android 开发中一个比较典型的例子是点击监听器 OnClickListener 。对设置 OnClickListener 来说, View 是被观察者, OnClickListener 是观察者,二者通过 setOnClickListener() 方法达成订阅关系。订阅之后用户点击按钮的瞬间,Android Framework 就会将点击事件发送给已经注册的 OnClickListener 。采取这样被动的观察方式,既省去了反复检索状态的资源消耗,也能够得到最高的反馈速度。当然,这也得益于我们可以随意定制自己程序中的观察者和被观察者,而警察叔叔明显无法要求小偷『你在作案的时候务必通知我』。

RxJava 简介

我所理解的RxJava的核心优势应该是它可以对复杂逻辑进行拆分成为一个一个的Observable后,RxJava的各种操作符予这些解耦的Observable能够合理的进行再组织的能力,并且它给予了你足够丰富的再组织能力。这种分拆再组织的能力是十分强大的,只有运用好RxJava这种强大的能力,才能真正意义上使你原来非常复杂的揉在一团的逻辑代码变得清晰、简洁,本质上是因为RxJava给你提供了这种强大方便的组织能力,我觉得有点像一种编程模式,你可以放心的将复杂的逻辑拆块,最后RxJava给你提供了丰富的组织、变换、串联、控制这些块的能力,只有这个时候你才会真正觉得这是个好东西,而不应该是跟风使用,但是心里也说不清楚为什么要使用。

RxJava 有三个基本概念: Observable (被观察者),Observer (观察者),subscribe (订阅)。ObservableObserver 通过 subscribe() 方法实现订阅关系,从而 Observable 可以在需要的时候发出事件来通知 Observer

Github 链接:

RxJava https://github.com/ReactiveX/RxJava

RxAndroid https://github.com/ReactiveX/RxAndroid

gradle 依赖:

implementation 'io.reactivex.rxjava2:rxjava:2.x.y'

implementation 'io.reactivex.rxjava2:rxandroid:2.x.y'

RxJava 使用

创建被观察者

1
2
3
4
5
6
7
8
9
Observable<String> observable = Observable
.create(new ObservableOnSubscribe<String>() {
@Override
public void subscribe(ObservableEmitter<String> emitter) {
emitter.onNext("Hello");
emitter.onNext("World");
emitter.onComplete();
}
});

可以看到,这里传入了一个 ObservableOnSubscribe 对象作为参数。ObservableOnSubscribe 会被存储在返回的 Observable 对象中,当 Observable 被订阅的时候,subscribe 方法就会自动被调用,事件序列就会依照设定依次触发(对于上面的代码,就是观察者 observer 将会被调用三次 onNext() 和一次 onComplete())。

此外,还可以通过其他方法来创建被观察者。

  • just(T...): 将传入的参数依次发送出来。

    1
    Observable observable = Observable.just("Hello", "World");
  • fromArray(T...items) : 将传入的数组或 Iterable 拆分成具体对象后,依次发送出来。

    1
    2
    String[] words = {"Hello", "Hi", "Aloha"};
    Observable<String> observable = Observable.fromArray(words);

Observable 外还有 Flowable 等被观察者类型。

创建观察者

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
Observer<String> observer = new Observer<String>() {
@Override
public void onSubscribe(Disposable d) {

}

@Override
public void onNext(String s) {

}

@Override
public void onError(Throwable e) {

}

@Override
public void onComplete() {

}
};

在观察者中进行响应事件对应的相关操作。

订阅

1
observable.subscribe(observer);

这里的写法是被观察者订阅了观察者,而不是观察者订阅被观察者,是为了保证流式API调用风格。

1
2
3
4
5
observable
.observeOn(AndroidSchedulers.mainThread())
.subscribeOn(Schedulers.io())
.filter(s -> s != null)
.subscribe(observer);

上面就是一个非常简易的RxJava流式API的调用:同一个调用主体一路调用下来,一气呵成。

RxJava 的这个实现,是一条从上到下的链式调用,没有任何嵌套,这在逻辑的简洁性上是具有优势的。

整个流程如下图所示:

流程图

结合流程图的相应代码实例如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
//创建被观察者,是事件传递的起点
Observable.just("On","Off","On","On")
//这就是在传递过程中对事件进行过滤操作
.filter(new Func1<String, Boolean>() {
@Override
public Boolean call(String s) {
return s!=null;
}
})
//实现订阅
.subscribe(
//创建观察者,作为事件传递的终点处理事件
new Subscriber<String>() {
@Override
public void onCompleted() {
Log.d("DDDDDD","结束观察...\n");
}

@Override
public void onError(Throwable e) {
//出现错误会调用这个方法
}
@Override
public void onNext(String s) {
//处理事件
Log.d("DDDDD","handle this---"+s)
}
);

注意:当调用订阅操作(即调用Observable.subscribe()方法)的时候,被观察者才真正开始发出事件。

线程调度

至此,在 RxJava 的默认规则中,事件的发出和消费都是在同一个线程的。

在 RxJava 中,通过 Scheduler 来指定每一段代码应该运行在什么样的线程。下表展示了RxJava中可用的调度器种类:

调度器类型 效果
Schedulers.computation( ) 用于计算任务,如事件循环或和回调处理,不要用于IO操作(IO操作请使用Schedulers.io());默认线程数等于处理器的数量
Schedulers.from(executor) 使用指定的Executor作为调度器
Schedulers.immediate( ) 在当前线程立即开始执行任务
Schedulers.io( ) 用于IO密集型任务,如异步阻塞IO操作,这个调度器的线程池会根据需要增长;对于普通的计算任务,请使用Schedulers.computation();Schedulers.io( )默认是一个CachedThreadScheduler,很像一个有线程缓存的新线程调度器
Schedulers.newThread( ) 为每个任务创建一个新线程
Schedulers.trampoline( ) 当其它排队的任务完成后,在当前线程排队开始执行
AndroidSchedulers.mainThread() Android 主线程

subscribeOn(): 指定 subscribe() 所发生的线程,或者叫做事件产生的线程。

observeOn(): 指定 Observer 所运行在的线程。或者叫做事件消费的线程。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//new Observable.just()执行在新线程
Observable.just(getFilePath())
//指定在新线程中创建被观察者
.subscribeOn(Schedulers.newThread())
//将接下来执行的线程环境指定为io线程
.observeOn(Schedulers.io())
//map就处在io线程
.map(mMapOperater)
//将后面执行的线程环境切换为主线程,
//但是这一句依然执行在io线程
.observeOn(AndroidSchedulers.mainThread())
//指定线程无效,但这句代码本身执行在主线程
.subscribeOn(Schedulers.io())
//执行在主线程
.subscribe(mSubscriber);

注意:

  • subscribeOn() 它指示 Observable 在一个指定的调度器上创建(只作用于被观察者创建阶段)。只能指定一次,如果指定多次则以第一次为准
  • observeOn() 指定在事件传递(加工变换)和最终被处理(观察者)的发生在哪一个调度器。可指定多次,每次指定完都在下一步生效。

=======================================================

进阶(操作符(map、flatmap、zip、defer、contatMap等等),背压等)

操作符 说明
Create 使用一个函数从头开始创建一个Observable
Defer 直到有观察者订阅时才创建Observable,并且为每个观察者创建一个新的Observable
Empty 创建一个不发射任何数据但是正常终止的Observable
Never 创建一个不发射数据也不终止的Observable
Throw 创建一个不发射数据以一个错误终止的Observable
From 将其它种类的对象和数据类型转换为Observable
Interval 创建一个按固定时间间隔发射整数序列的Observable
Just 创建一个发射指定值的Observable
Range 创建一个发射特定整数序列的Observable
Repeat 创建一个发射特定数据重复多次的Observable
Start 返回一个Observable,它发射一个类似于函数声明的值
Timer 创建一个Observable,它在一个给定的延迟后发射一个特殊的值
操作符 说明
操作符 说明

=======================================================

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
private void testRxJava() {
Observable.create(new ObservableOnSubscribe<String>() {
@Override
public void subscribe(ObservableEmitter<String> emitter) {

}
}).subscribe(new Observer<String>() {
@Override
public void onSubscribe(Disposable d) {

}

@Override
public void onNext(String s) {

}

@Override
public void onError(Throwable e) {

}

@Override
public void onComplete() {

}
})

// 伪代码
Observable.create( ObservableOnSubscribe(ObservableEmitter -> *) ){
ObservableCreate<T>(observableOnSubscribe)
}
.subscribe(Observer){
subscribeActual(observer){
parent = new CreateEmitter<T>(observer)
observer.onSubscribe(parent);
observableOnSubscribe.subscribe(parent);
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43

// subscribeOn 是改变上流的线程调度,所以只有第一个 subscribeOn 是有用的
// observeOn 是改变下流的线程调度,所以每一个 observeOn 对它下流的 操作都是有用的
ObservableCreate subscribeActual(SubscribeOnObserver){ ObservableOnSubscribe.subscribe(observableEmitter) }
ObservableSubscribeOn subscribeActual(ObserveOnObserver)
ObservableObserveOn subscribeActual(MapObserver)
ObservableMap subscribeActual(SubscribeOnObserver)
ObservableSubscribeOn subscribeActual(ObserveOnObserver)
ObservableObserveOn subscribeActual(MergeObserver)
ObservableFlatMap subscribeActual(Observer)

subscribe(Observer)



Observable.create(...)
.subscribeOn(Schedulers.newThread())
.observeOn(AndroidSchedulers.mainThread())
.map(s -> s + "!")
.subscribeOn(Schedulers.newThread())
.observeOn(AndroidSchedulers.mainThread())
.flatMap(Function<String, ObservableSource<String>>) s -> Observable.just(s + "!"))
.subscribe(new Observer<String>() {
@Override
public void onSubscribe(Disposable d) {

}

@Override
public void onNext(String s) {

}

@Override
public void onError(Throwable e) {

}

@Override
public void onComplete() {

}
});

参考资料:

Android流行ORM框架

Android ORM框架

Room

1
2
3
4
def room_version = "1.1.1"
implementation "android.arch.persistence.room:runtime:$room_version"
annotationProcessor "android.arch.persistence.room:compiler:$room_version"
implementation "android.arch.persistence.room:rxjava2:$room_version"
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
@Entity(tableName = "user")
public class User {

@PrimaryKey
private int id;

@ColumnInfo(name = "first_name")
private String firstName;

@ColumnInfo(name = "last_name")
private String lastName;

@Ignore
private String nickName;

public int getId() {
return id;
}

public void setId(int id) {
this.id = id;
}

public String getFirstName() {
return firstName;
}

public void setFirstName(String firstName) {
this.firstName = firstName;
}

public String getLastName() {
return lastName;
}

public void setLastName(String lastName) {
this.lastName = lastName;
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@Dao
public interface UserDao {

@Query("select * from user")
List<User> getAll();

@Query("select * from user where id in (:ids)")
List<User> queryUserByIds(int[] ids);

@Query("select * from user where first_name like :first and last_name like :last limit 1")
User findByName(String first, String last);

@Insert(onConflict = REPLACE)
void insertAll(User... users);

@Delete
void delete(User user);

@Query("SELECT * from user where id = :id LIMIT 1")
public Flowable<User> loadUserById(int id);
}
1
2
3
4
5
6
@Database(entities = {User.class}, version = 1)
public abstract class AppDatabase extends RoomDatabase {

public abstract UserDao userDao();

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
private fun testRoom() {
val db = Room.databaseBuilder(applicationContext,
AppDatabase::class.java, "database-name").build()

var all = db.userDao().all

val MIGRATION_1_2 = object : Migration(1, 2) {
override fun migrate(database: SupportSQLiteDatabase) {
database.execSQL("CREATE TABLE `Fruit` (`id` INTEGER, " + "`name` TEXT, PRIMARY KEY(`id`))")
}
}
val MIGRATION_2_3 = object : Migration(2, 3) {
override fun migrate(database: SupportSQLiteDatabase) {
database.execSQL("ALTER TABLE Book "
+ " ADD COLUMN pub_year INTEGER")
}
}
Room.databaseBuilder(applicationContext, AppDatabase::class.java, "database-name")
.addMigrations(MIGRATION_1_2, MIGRATION_2_3).build()
}

GreenDao

Realm

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// product
buildscript {
repositories {
...
maven {url 'http://oss.jfrog.org/artifactory/oss-snapshot-local'}
}
dependencies {
...
classpath "io.realm:realm-gradle-plugin:5.8.0-SNAPSHOT"
}
}

allprojects {
repositories {
...
maven {url 'http://oss.jfrog.org/artifactory/oss-snapshot-local'}
}
}


// module
apply plugin: 'realm-android'
1
2
3
4
5
6
7
8
9
10
class BaseApplication : Application() {

override fun onCreate() {
super.onCreate()
Realm.init(this);
val config = RealmConfiguration.Builder().build()
Realm.setDefaultConfiguration(config)
}

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
// 新建一个对象,并进行存储
private fun testCreateRealm() {
val realm = Realm.getDefaultInstance()
realm.beginTransaction()
val user = realm.createObject(User::class.java)
user.firstName = "lalla"
user.lastName = "aaaa"
realm.commitTransaction()
}

// 复制一个对象到Realm数据库
private fun copyToRealm() {
val realm = Realm.getDefaultInstance()

val user = User()
user.firstName = "lalla"
user.lastName = "aaaa"

realm.beginTransaction()
realm.copyToRealm(user)
realm.commitTransaction()
}

// 使用事务块
private fun testExecuteTransaction() {
val realm = Realm.getDefaultInstance()
val user = realm.createObject(User::class.java)
user.firstName = "lalla"
user.lastName = "aaaa"

realm.executeTransaction {
it.copyToRealm(user)
}
}

private fun testDelete() {
val realm = Realm.getDefaultInstance()
val users = realm.where(User::class.java).findAll()
realm.executeTransaction {
val user = users[1]
user?.deleteFromRealm()
//删除第一个数据
users.deleteFirstFromRealm()
//删除最后一个数据
users.deleteLastFromRealm()
//删除位置为1的数据
users.deleteFromRealm(1)
//删除所有数据
users.deleteAllFromRealm()
}
}

private fun testUpdate() {
val realm = Realm.getDefaultInstance()
val user = realm.where(User::class.java).equalTo("id", "1").findFirst()
realm.beginTransaction()
user?.firstName = "1111"
realm.commitTransaction()
}

private fun testQuery() {
val realm = Realm.getDefaultInstance()
val users = realm.where(User::class.java).findAll()

var average = users.average("age")
var max = users.max("age")
var sort = users.sort("age")

val user = realm.where(User::class.java).equalTo("id", "1").findFirst()
val user1 = realm.where(User::class.java).between("age", 1, 10).findFirst()
val user2 = realm.where(User::class.java).lessThan("id", 1).findFirst()
val user3 = realm.where(User::class.java).contains("id", "1").findFirst()
}

DBFlow

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
// product
allprojects {
repositories {
...
maven { url "https://www.jitpack.io" }
}
}

// module
dependencies {
// if Java use this. If using Kotlin do NOT use this.
annotationProcessor "com.github.Raizlabs.DBFlow:dbflow-processor:${dbflow_version}"

// Use if Kotlin user.
kapt "com.github.Raizlabs.DBFlow:dbflow-processor:${dbflow_version}"

implementation "com.github.Raizlabs.DBFlow:dbflow-core:${dbflow_version}"
implementation "com.github.Raizlabs.DBFlow:dbflow:${dbflow_version}"

// sql-cipher database encryption (optional)
implementation "com.github.Raizlabs.DBFlow:dbflow-sqlcipher:${dbflow_version}"
implementation "net.zetetic:android-database-sqlcipher:${sqlcipher_version}@aar"

// kotlin extensions
implementation "com.github.Raizlabs.DBFlow:dbflow-kotlinextensions:${dbflow_version}"

// RXJava 2 support
implementation "com.github.Raizlabs.DBFlow:dbflow-rx2:${dbflow_version}"

// RXJava 2 Kotlin Extensions Support
implementation "com.github.Raizlabs.DBFlow:dbflow-rx2-kotlinextensions:${dbflow_version}"
}
1
2
3
4
5
6
7
8
class BaseApplication : Application() {

override fun onCreate() {
super.onCreate()
FlowManager.init(this)
}

}
1
2
3
4
5
6
7
8
9
@Database(version = VERSION, name = NAME)
public class DBFlowDataBase {

//数据库名称
static final String NAME = "RuomizDataBase";
//数据库版本
static final int VERSION = 1;

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@Table(database = DBFlowDataBase.class)
public class DBFlowModel extends BaseModel {

@PrimaryKey(autoincrement = true)
public int id;

@Column
public String name;

@Column
public int age;

@Column
public String address;

@Column
public String phone;

}

编译之后

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
private fun testDBFlow() {
val dbFlowModel = DBFlowModel()
dbFlowModel.name = "Ruomiz"
dbFlowModel.address = "beijing"
dbFlowModel.age = 100
dbFlowModel.phone = "13333333333"
dbFlowModel.save()
dbFlowModel.update()
dbFlowModel.delete()
dbFlowModel.insert()

//根据name 单个查询
val dbFlowModel1 = Select().from(DBFlowModel::class.java).where(DBFlowModel_Table.name.`is`(name)).querySingle()

//单个查询
val dbFlowDataBase = Select().from(DBFlowDataBase::class.java).querySingle()
//返回所有结果
val dbFlowDataBases = Select().from(DBFlowDataBase::class.java).queryList()
}

OrmLite

下载地址:http://ormlite.com/releases/

需要下载最新的 core.jar 和 android.jar 并添加依赖

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
@DatabaseTable(tableName = "user")
public class User {

@DatabaseField(generatedId = true)//表示id为主键且自动生成
private int id;

@DatabaseField(columnName = "first_name")
private String firstName;

@DatabaseField(columnName = "last_name")
private String lastName;

public int getId() {
return id;
}

public void setId(int id) {
this.id = id;
}

public String getFirstName() {
return firstName;
}

public void setFirstName(String firstName) {
this.firstName = firstName;
}

public String getLastName() {
return lastName;
}

public void setLastName(String lastName) {
this.lastName = lastName;
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
public class DatabaseHelper extends OrmLiteSqliteOpenHelper {

private static final String TABLE_NAME = "sqlite-test.db";
/**
* userDao ,每张表对于一个
*/
private Dao<User, Integer> userDao;

public DatabaseHelper(Context context, String databaseName,
CursorFactory factory, int databaseVersion) {
super(context, databaseName, factory, databaseVersion);
}

@Override
public void onCreate(SQLiteDatabase sqLiteDatabase, ConnectionSource connectionSource) {
try {
TableUtils.createTable(connectionSource, User.class);
} catch (SQLException e) {
e.printStackTrace();
}

}

@Override
public void onUpgrade(SQLiteDatabase sqLiteDatabase, ConnectionSource connectionSource,
int oldVersion, int newVersion) {
try {
TableUtils.dropTable(connectionSource, User.class, true);
onCreate(sqLiteDatabase, connectionSource);
} catch (SQLException e) {
e.printStackTrace();
}
}

public void insertUser(User user) throws SQLException {
getUserDao().create(user);
}

public void deleteUserById(int id, User user) throws SQLException {
getUserDao().deleteById(id);
getUserDao().delete(user);
}

public void updateData() throws SQLException {
UpdateBuilder<User, Integer> updateBuilder = getUserDao().updateBuilder();
updateBuilder.setWhere(updateBuilder.where().eq("name", "jack").and().gt("age", 19));
updateBuilder.updateColumnValue("name", "Jack");
updateBuilder.updateColumnValue("phone", "1111765765");
updateBuilder.update();
}

public void query() throws SQLException {
List<User> users = getUserDao().queryForAll();

User user = getUserDao().queryForId(3);

List<User> jsckUser = getUserDao().queryForEq("name", "Jack");
}

/**
* 获得userDao
*/
public Dao<User, Integer> getUserDao() throws SQLException {
if (userDao == null) {
userDao = getDao(User.class);
}
return userDao;
}

/**
* 释放资源
*/
@Override
public void close() {
super.close();
userDao = null;
}

}

分析及对比

懒加载 fragment

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
public abstract class BaseLazyFragment extends Fragment {

protected View mRootView;
protected Context mContext;
protected boolean isVisible;
private boolean isPrepared;
private boolean isFirst = true;

@Override
public void onActivityCreated(@Nullable Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
isPrepared = true;
initPrepare();
}

@Override
public void setUserVisibleHint(boolean isVisibleToUser) {
super.setUserVisibleHint(isVisibleToUser);
if (getUserVisibleHint()) {
isVisible = true;
lazyLoad();
} else {
isVisible = false;
onInvisible();
}
}

@Override
public void onResume() {
super.onResume();
if (getUserVisibleHint()) {
setUserVisibleHint(true);
}
}

@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
mContext = getActivity();
}

@Nullable
@Override
public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
if (mRootView == null) {
mRootView = initView(inflater, container, savedInstanceState);
}

return mRootView;
}

//--------------------------------method---------------------------//

/**
* 懒加载
*/
protected void lazyLoad() {
if (!isPrepared || !isVisible || !isFirst) {
return;
}
initData();
isFirst = false;
}

//--------------------------abstract method------------------------//

/**
* 在onActivityCreated中调用的方法,可以用来进行初始化操作。
*/
protected abstract void initPrepare();

/**
* fragment被设置为不可见时调用
*/
protected abstract void onInvisible();

/**
* 这里获取数据,刷新界面
*/
protected abstract void initData();

/**
* 初始化布局,请不要把耗时操作放在这个方法里,这个方法用来提供一个
* 基本的布局而非一个完整的布局,以免ViewPager预加载消耗大量的资源。
*/
protected abstract View initView(LayoutInflater inflater,
@Nullable ViewGroup container,
@Nullable Bundle savedInstanceState);

}

多层嵌套后的 Fragment 懒加载实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
import android.os.Bundle;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.v4.app.Fragment;
import android.support.v4.app.FragmentManager;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;

import java.util.List;

/**
* @author wangshijia
* @date 2018/2/2
* Fragment 第一次可见状态应该在哪里通知用户 在 onResume 以后?
*/
public abstract class LazyLoadBaseFragment extends BaseLifeCircleFragment {

protected View rootView = null;


private boolean mIsFirstVisible = true;

private boolean isViewCreated = false;

private boolean currentVisibleState = false;

@Override
public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
super.onCreateView(inflater, container, savedInstanceState);

if (rootView == null) {
rootView = inflater.inflate(getLayoutRes(), container, false);
}
initView(rootView);
return rootView;
}


@Override
public void setUserVisibleHint(boolean isVisibleToUser) {
super.setUserVisibleHint(isVisibleToUser);
// 对于默认 tab 和 间隔 checked tab 需要等到 isViewCreated = true 后才可以通过此通知用户可见
// 这种情况下第一次可见不是在这里通知 因为 isViewCreated = false 成立,等从别的界面回到这里后会使用 onFragmentResume 通知可见
// 对于非默认 tab mIsFirstVisible = true 会一直保持到选择则这个 tab 的时候,因为在 onActivityCreated 会返回 false
if (isViewCreated) {
if (isVisibleToUser && !currentVisibleState) {
dispatchUserVisibleHint(true);
} else if (!isVisibleToUser && currentVisibleState) {
dispatchUserVisibleHint(false);
}
}
}

@Override
public void onActivityCreated(@Nullable Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);

isViewCreated = true;
// !isHidden() 默认为 true 在调用 hide show 的时候可以使用
if (!isHidden() && getUserVisibleHint()) {
dispatchUserVisibleHint(true);
}

}

@Override
public void onHiddenChanged(boolean hidden) {
super.onHiddenChanged(hidden);
LogUtils.e(getClass().getSimpleName() + " onHiddenChanged dispatchChildVisibleState hidden " + hidden);

if (hidden) {
dispatchUserVisibleHint(false);
} else {
dispatchUserVisibleHint(true);
}
}

@Override
public void onResume() {
super.onResume();
if (!mIsFirstVisible) {
if (!isHidden() && !currentVisibleState && getUserVisibleHint()) {
dispatchUserVisibleHint(true);
}
}
}

@Override
public void onPause() {
super.onPause();
// 当前 Fragment 包含子 Fragment 的时候 dispatchUserVisibleHint 内部本身就会通知子 Fragment 不可见
// 子 fragment 走到这里的时候自身又会调用一遍 ?
if (currentVisibleState && getUserVisibleHint()) {
dispatchUserVisibleHint(false);
}
}


/**
* 统一处理 显示隐藏
*
* @param visible
*/
private void dispatchUserVisibleHint(boolean visible) {
//当前 Fragment 是 child 时候 作为缓存 Fragment 的子 fragment getUserVisibleHint = true
//但当父 fragment 不可见所以 currentVisibleState = false 直接 return 掉
// 这里限制则可以限制多层嵌套的时候子 Fragment 的分发
if (visible && isParentInvisible()) return;

//此处是对子 Fragment 不可见的限制,因为 子 Fragment 先于父 Fragment回调本方法 currentVisibleState 置位 false
// 当父 dispatchChildVisibleState 的时候第二次回调本方法 visible = false 所以此处 visible 将直接返回
if (currentVisibleState == visible) {
return;
}

currentVisibleState = visible;

if (visible) {
if (mIsFirstVisible) {
mIsFirstVisible = false;
onFragmentFirstVisible();
}
onFragmentResume();
dispatchChildVisibleState(true);
} else {
dispatchChildVisibleState(false);
onFragmentPause();
}
}

/**
* 用于分发可见时间的时候父获取 fragment 是否隐藏
*
* @return true fragment 不可见, false 父 fragment 可见
*/
private boolean isParentInvisible() {
Fragment parentFragment = getParentFragment();
if (parentFragment instanceof LazyLoadBaseFragment ) {
LazyLoadBaseFragment fragment = (LazyLoadBaseFragment) parentFragment;
return !fragment.isSupportVisible();
}else {
return false;
}
}

private boolean isSupportVisible() {
return currentVisibleState;
}

/**
* 当前 Fragment 是 child 时候 作为缓存 Fragment 的子 fragment 的唯一或者嵌套 VP 的第一 fragment 时 getUserVisibleHint = true
* 但是由于父 Fragment 还进入可见状态所以自身也是不可见的, 这个方法可以存在是因为庆幸的是 父 fragment 的生命周期回调总是先于子 Fragment
* 所以在父 fragment 设置完成当前不可见状态后,需要通知子 Fragment 我不可见,你也不可见,
* <p>
* 因为 dispatchUserVisibleHint 中判断了 isParentInvisible 所以当 子 fragment 走到了 onActivityCreated 的时候直接 return 掉了
* <p>
* 当真正的外部 Fragment 可见的时候,走 setVisibleHint (VP 中)或者 onActivityCreated (hide show) 的时候
* 从对应的生命周期入口调用 dispatchChildVisibleState 通知子 Fragment 可见状态
*
* @param visible
*/
private void dispatchChildVisibleState(boolean visible) {
FragmentManager childFragmentManager = getChildFragmentManager();
List<Fragment> fragments = childFragmentManager.getFragments();
if (!fragments.isEmpty()) {
for (Fragment child : fragments) {
if (child instanceof LazyLoadBaseFragment && !child.isHidden() && child.getUserVisibleHint()) {
((LazyLoadBaseFragment) child).dispatchUserVisibleHint(visible);
}
}
}
}

public void onFragmentFirstVisible() {
LogUtils.e(getClass().getSimpleName() + " 对用户第一次可见");

}

public void onFragmentResume() {
LogUtils.e(getClass().getSimpleName() + " 对用户可见");
}

public void onFragmentPause() {
LogUtils.e(getClass().getSimpleName() + " 对用户不可见");
}

@Override
public void onDestroyView() {
super.onDestroyView();
isViewCreated = false;
mIsFirstVisible = true;
}


/**
* 返回布局 resId
*
* @return layoutId
*/
protected abstract int getLayoutRes();


/**
* 初始化view
*
* @param rootView
*/
protected abstract void initView(View rootView);
}

对于可见状态的生命周期调用顺序,父 Fragment总是优先于子 Fragment,而对于不可见事件,内部的 Fragment 生命周期总是先于外层 Fragment。

Android 命名规范

Android 命名规范

标识符命名法

标识符命名法最要有四种:

  1. 驼峰(Camel)命名法:又称小驼峰命名法,除首单词外,其余所有单词的第一个字母大写。
  2. 帕斯卡(pascal)命名法:又称大驼峰命名法,所有单词的第一个字母大写
  3. 下划线命名法:单词与单词间用下划线做间隔。
  4. 匈牙利命名法:广泛应用于微软编程环境中,在以Pascal命名法的变量前附加小写序列说明该变量的类型。 量的取名方式为:<scope_> + <prefix_> + <qualifier> 范围前缀,类型前缀,限定词。

尽可能的用最少的字符而又能完整的表达标识符的含义。

英文缩写原则:

  1. 较短的单词可通过去掉“元音”形成缩写
  2. 较长的单词可取单词的头几个字母形成缩写
  3. 此外还有一些约定成俗的英文单词缩写

下面为常见的英文单词缩写:

名称 缩写
icon ic
color cl
divider di
selector sl
background bg
image img
password pwd
position pos
TextView tv
ImageView iv
EditText et

注意: 单词缩写原则:不要用缩写,除非该缩写是约定俗成的。

命名规范

采用反域名命名规则,全部使用小写字母。一级包名为com,二级包名为xx(可以是公司或则个人的随便),三级包名根据应用进行命名,四级包名为模块名或层级名。

包名 此包中包含
包名.activities Activity类
包名.base 自定义基类
包名.adapter Adapter类
包名.tools 公共工具方法类
包名.bean 包中包含:实体类
包名.view (或 widget) 自定义的View类等
包名.service Service服务

采用大驼峰命名法,尽量避免缩写,除非该缩写是众所周知的,比如 HTML , URL,如果类名称中包含单词缩写,则单词缩写的每个字母均应大写。

描述 例如
activity Aty或者Activity为后缀标识 WelcomeAty.或者WelcomeActivity
Adapter Adapte 为后缀标识 NewsAdp或NewsAdapter
基础类 以Base开头 BaseActivity,BaseFragment
公共方法类 Tools或Manager为后缀标识 ThreadPoolManager, LogTools
Service 以Service为后缀标识 TimeService

方法

动词或动名词,采用小驼峰命名法例如: onCreate(), run()

变量

采用小驼峰命名法。类中控件名称必须与xml布局id保持一致。

用统一的量词通过在结尾处放置一个量词,就可创建更加统一的变量,它们更容易理解,也更容易搜索。例如,请使用 strCustomerFirststrCustomerLast ,而不要使用 strFirstCustomerstrLastCustomer

常量

全部大写,采用下划线命名法.例如:MIN_WIDTH

资源文件

全部小写,采用下划线命名法,加前缀区分

名称 功能
btn_xx 按钮图片使用btn_整体效果(selector)
btn_xx_normal 按钮图片使用btn_正常情况效果
btn_xx_press 按钮图片使用btn_点击时候效果
bg_head 背景图片使用bg_功能_说明
ic_more_help 图标图片使用icon_功能_说明

资源ID

大小写规范与方法名一致,采用小驼峰命名法。命名规范为“资源控件的缩写 名”+“变量名”。注意:页面控件名称应该和控件id名保持一致

layout中的id命名

命名模式为:view缩写_模块名称_view的逻辑名称

控件 缩写
RelativeView rv
TextView tv
Button btn
ImageView iv
ProgressBar pb

注意: 如果layout文件很复杂,建议将layout分成多个模块,每个模块定义一个moduleViewHolder,其成员变量包含所属view

styles.xml

将layout中不断重现的style提炼出通用的style通用组件,放到styles.xml中

Android 动态权限申请

简介

权限列表

Android6.0以上把权限分为普通权限和危险权限,所以危险权限是需要动态申请,给予用户提示的。

看到上面的 permissions,会发现一个问题,危险权限都是一组一组的。

分组对权限机制的申请是有一定影响的。例如app运行在android 6.x的机器上,对于授权机制是这样的。如果你申请某个危险的权限,假设你的app早已被用户授权了同一组的某个危险权限,那么系统会立即授权,而不需要用户去点击授权。比如你的app对 READ_CONTACTS 已经授权了,当你的app申请 WRITE_CONTACTS 时,系统会直接授权通过。

此外,对于申请时的弹窗上面的文本说明也是对整个权限组的说明,而不是单个权限。

动态权限申请

1.首先要保证在AndroidManifest中写明需要的权限。

1
2
3
<uses-permission android:name="android.permission.READ_CONTACTS" />
<uses-permission android:name="android.permission.WRITE_CONTACTS" />
<uses-permission android:name="android.permission.CAMERA"/>

2.权限申请示例 以获取定位权限为例。

  1. 点击按钮,检查并申请权限
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    btn.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View view) {
    if (Build.VERSION.SDK_INT >23) {
    if (ContextCompat.checkSelfPermission(MainActivity.this,
    Manifest.permission.ACCESS_COARSE_LOCATION)
    == PackageManager.PERMISSION_GRANTED) {
    //授予权限
    getLoation();
    }else{
    //未获得权限
    requestPermissions(new String[]{Manifest.permission.ACCESS_COARSE_LOCATION}
    ,REQUEST_CODE_LOCATION);
    }
    }
    }
    });

如果有权限,执行获取位置逻辑,如果没权限,则进行请求权限。

  1. 权限申请结果回调
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    @Override
    public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
    if (requestCode == REQUEST_CODE_LOCATION){
    if (grantResults[0] == PackageManager.PERMISSION_GRANTED){
    getLoation();
    } else{
    if (shouldShowRequestPermissionRationale( Manifest.permission.ACCESS_COARSE_LOCATION)){
    new AlertDialog.Builder(this)
    .setMessage("申请定位权限,才能为你推送更准确的信息")
    .setPositiveButton("确定", new DialogInterface.OnClickListener() {
    @Override
    public void onClick(DialogInterface dialog, int which) {
    //申请定位权限
    requestPermissions(MainActivity.this,
    new String[]{Manifest.permission.ACCESS_COARSE_LOCATION}, REQUEST_CODE_LOCATION);
    }
    }).show();
    }
    }
    return;
    }
    super.onRequestPermissionsResult(requestCode, permissions, grantResults);
    }

如果同意,执行获取位置逻辑,如果拒绝,重写 shouldShowRequestPermissionRationale 方法,返回 true,向用户弹窗给出一个获取权限的提示,点击后再次申请权限。

1
2
3
4
5
6
7
public boolean shouldShowRequestPermissionRationale(@NonNull String permission) {
if (permission.equals(Manifest.permission.ACCESS_COARSE_LOCATION) ) {
return true;
} else {
return super.shouldShowRequestPermissionRationale(permission);
}
}

重写shouldShowRequestPermissionRationale,在申请位置权限时,返回true,给用户解释。

完整代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
public class PermissionActivity extends AppCompatActivity implements View.OnClickListener {

private static final String TAG = "PermissionActivity";
public static final int REQUEST_CAMERA = 1;
private ConstraintLayout constraintLayout;

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_permission);

initView();
}

private void initView() {
constraintLayout = findViewById(R.id.mainLayout);
findViewById(R.id.btn_camera).setOnClickListener(this);
findViewById(R.id.btn_contact).setOnClickListener(this);
}

@RequiresApi(api = Build.VERSION_CODES.M)
private boolean checkPermission(String permission) {
int i = checkSelfPermission(permission);
return i != PackageManager.PERMISSION_DENIED;
}

private void showSnackbar(String message) {
Snackbar.make(constraintLayout, message, Snackbar.LENGTH_SHORT).show();
}

@Override
public void onClick(View v) {
switch (v.getId()) {
case R.id.btn_camera:
showCamera();
break;
case R.id.btn_contact:
break;
}
}

private void showCamera() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
if (checkPermission(Manifest.permission.CAMERA)) {
showSnackbar("已授权,可以打开");
} else {
if (shouldShowRequestPermissionRationale(Manifest.permission.CAMERA)) {
Snackbar.make(constraintLayout, "Camera permission is needed", Snackbar.LENGTH_SHORT)
.setAction("确定", new View.OnClickListener() {
@Override
public void onClick(View v) {
requestPermissions(new String[]{Manifest.permission.CAMERA}, REQUEST_CAMERA);
}
}).show();
} else {
requestPermissions(new String[]{Manifest.permission.CAMERA}, REQUEST_CAMERA);
}
}
} else {
showSnackbar("可以直接打开");
}
}

@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
switch (requestCode) {
case REQUEST_CAMERA:
if (grantResults.length > 0) {
if (grantResults[0] == PackageManager.PERMISSION_GRANTED) {
showSnackbar("已授权");
} else {
showSnackbar("拒绝授权");
}
}
break;
}
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
}
}

另外,在看一些 权限申请 的三方库的时候,发现了一件很有意思的事情。通常代码很难追踪到 Activity 的生命周期,一些库就新建一个 Fragment 在其生命周期的各个回调方法里去调用相应的方法。很棒。

参考文章

Lottie 详解

简介

Lottie 是 Airbnb 开源的一个面向 iOS、Android、React Native 的动画库,能分析 Adobe After Effects 导出的动画,并且能让原生 App 像使用静态素材一样使用这些动画,完美实现动画效果。

现在使用各平台的 native 代码实现一套复杂的动画是一件很困难并且耗时的事,我们需要为不同尺寸的屏幕加载不同的素材资源,还需要写大量难维护的代码,而 Lottie 可以做到同一个动画文件在不同平台上实现相同的效果,极大减少开发时间,实现不同的动画,只需要设置不同的动画文件即可,极大减少开发和维护成本。

官方效果图:

官方效果图

使用

Lottie 支持多平台,使用同一个 JSON 动画文件,可在不同平台实现相同的效果。其中,Android 通过 Airbnb 的开源项目 lottie-android 实现,最低支持 API 16。

引入依赖

implementation 'com.airbnb.android:lottie:2.5.4'

添加 Adobe After Effects 导出的动画文件

Lottie 默认读取 Assets 中的文件,我们需要把动画文件 react.json 保存在 app/src/main/assets 文件里。在 官网 可以免费下载动画文件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
// react.json
{
"v": "4.6.0",
"fr": 29.9700012207031,
"ip": 0,
"op": 141.000005743048,
"w": 800,
"h": 800,
"ddd": 0,
"assets": [ ],
"layers": [
{
"ddd": 0,
"ind": 0,
"ty": 4,
"nm": "center_circle",
"ks": {...},
"ao": 0,
"shapes": [...],
"ip": 0,
"op": 900.000036657751,
"st": 0,
"bm": 0,
"sr": 1
},
{...},
{...},
{...}
]
}

使用Lottie

在布局文件中直接添加 Lottie 的 LottieAnimationView 控件,即可在界面显示 React logo 动画效果

1
2
3
4
5
6
7
<com.airbnb.lottie.LottieAnimationView
android:id="@+id/animation_view"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:lottie_fileName="react.json"
app:lottie_loop="true"
app:lottie_autoPlay="true" />

属性说明:

  • app:lottie_fileName: assets 文件夹下对应的动画文件的文件名
  • app:lottie_loop: 是否循环播放动画
  • app:lottie_autoPlay: 是否自动播放动画
  • app:lottie_cacheStrategy: 设置缓存策略。默认为none,可选值有strong和weak
  • app:lottie_colorFilter: 设置背景颜色
  • app:lottie_progress: 设置动画播放进度
  • app:lottie_imageAssetsFolder: 动画所需图片资源在assets中的绝对路径。如果没有图片资源,可以省略

代码使用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
LottieAnimationView animationView = (LottieAnimationView) findViewById(R.id.animation_view);

animationView.playAnimation(); //播放动画

animationView.setAnimation("data1.json", CacheStrategy.Strong); // 设置动画文件以及缓存策略

PorterDuffColorFilter colorFilter = new PorterDuffColorFilter(ContextCompat.getColor(this,android.R.color.holo_blue_light), PorterDuff.Mode.ADD);
animationView.addColorFilter(colorFilter); // 设置背景颜色

animationView.loop(true); // 循环播放

animationView.setProgress(0.5f); // 设置动画播放进度。需要注意值是float型。

// 监听动画的状态
animationView.addAnimatorListener(new Animator.AnimatorListener() {
@Override
public void onAnimationStart(Animator animation) {
// 开始
}

@Override
public void onAnimationEnd(Animator animation) {
// 结束
}

@Override
public void onAnimationCancel(Animator animation) {
// 取消
}

@Override
public void onAnimationRepeat(Animator animation) {
// 重复
}
});

// 动态监听动画演示过程,可以动态获取动画进度
animationView.addAnimatorUpdateListener(new AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {

}
});

animationView.pauseAnimation(); // 暂停动画

animationView.isAnimating() // 是否正在播放动画

animationView.resumeAnimation() // 恢复动画

animationView.cancelAnimation() // 取消动画

使用小技巧

加载SDCard动画文件

1
2
3
4
5
6
7
8
9
10
StringBuilder stringBuilder = new StringBuilder();
BufferedReader bufferedReader = new BufferedReader(new FileReader(new File(JSON_PATH + "react.json")));
String content = null;
while ((content = bufferedReader.readLine()) != null){
stringBuilder.append(content);
}
JSONObject jsonObject = new JSONObject(stringBuilder.toString());
animationView.setAnimation(jsonObject);
animationView.loop(true);
animationView.playAnimation();

加载SDCard图片

1
2
3
4
5
6
7
8
9
10
11
12
animationView.setImageAssetDelegate(new ImageAssetDelegate() {
@Override
public Bitmap fetchBitmap(LottieImageAsset asset) {
try {
FileInputStream fileInputStream = new FileInputStream(IMAGE_PATH + asset.getFileName());
return BitmapFactory.decodeStream(fileInputStream); ///把流转化为Bitmap图片
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
});

加载SDCard字体

1
2
3
4
5
6
animationView.setFontAssetDelegate(new FontAssetDelegate(){
public Typeface fetchFont(String fontFamily) {
Typeface customFont = Typeface.createFromFile(FONT_PATH + fontFamily);
return customFont;
}
});

缓存动画

1
2
3
4
5
/**
*Lottie内部有两个缓存map(强引用缓存,弱引用缓存),在动画文件加载完成后会根据设置的缓存策略缓存动画,方便下次使用。
*/
animationView.setAnimation(animation, LottieAnimationView.CacheStrategy.Strong); //强缓存
animationView.setAnimation(animation, LottieAnimationView.CacheStrategy.Weak); //弱缓存

Lottie实现原理

Lottie 的使用的资源是需要先通过 bodymovin( bodymovin 插件本身是用于网页上呈现各种AE效果的一个开源库) 将 Adobe After Effects (AE) 生成的aep动画工程文件转换为通用的json格式描述文件。Lottie则负责解析动画的数据,计算每个动画在某个时间点的状态,准确地绘制到屏幕上。

Lottie 对外通过控件 LottieAnimationView 暴露接口,控制动画。
LottieAnimationView 继承自 ImageView,通过当前时间绘制 canvas 显示到界面上。这里有两个关键类:LottieComposition 负责解析json描述文件,把json内容转成Java数据对象;LottieDrawable负责绘制,把LottieComposition转成的数据对象绘制成drawable显示到View上。顺序如下:

顺序

探究更多原理解析内容请在 Lottie–让动画如此简单 查看。

性能

内容来自 腾讯音乐技术团队 。

官方说明

  • 如果没有 mask 和 mattes,那么性能和内存非常好,没有 bitmap 创建,大部分操作都是简单的 cavas 绘制。
  • 如果存在 mattes,将会创建2~3个 bitmap。bitmap 在动画加载到 window 时被创建,被 window 删除时回收。所以不宜在 RecyclerView 中使用包涵 mattes 或者 mask 的动画,否则会引起 bitmap 抖动。除了内存抖动,mattes和mask中必要的 bitmap.eraseColor() 和 canvas.drawBitmap() 也会降低动画性能。对于简单的动画,在实际使用时性能不太明显。
  • 如果在列表中使用动画,推荐使用缓存 LottieAnimationView.setAnimation(String, CacheStrategy)

属性动画和Lottie动画对比

以下性能对比是以K歌内单个礼物动画效果

- 属性动画 lottie使用硬件加速 lottie未使用硬件加速
帧率
内存
cpu

总结

Lottie使用简单,易于上手,非常值得尝试。

优势

  • 开发效率高—代码实现简单,更换动画方便,易于调试和维护。
  • 数据源多样性—可从assets,sdcard,网络加载动画资源,能做到不发版本,动态更新
  • 跨平台—设计稿导出一份动画描述文件,android,ios,react native通用

劣势

  • 性能不够好—某些动画特效,内存和性能不够好;
  • 相对于属性动画,在展示大动画时,帧率较低

参考文章

RecyclerView IndexOutOfBoundsException

前言

最近使用 RecyclerView 的 notifyItemInsertednotifyItemRemoved 的时候,经常遇到这个异常。

1
2
3
4
5
6
7
8
9
java.lang.IndexOutOfBoundsException: Inconsistency detected. Invalid view holder adapter positionViewHolder{2064e5c6 position=2 id=-1, oldPos=2, pLpos:-1 scrap [attachedScrap] tmpDetached no parent}
at android.support.v7.widget.RecyclerView$Recycler.validateViewHolderForOffsetPosition(RecyclerView.java:4505)
at android.support.v7.widget.RecyclerView$Recycler.getViewForPosition(RecyclerView.java:4636)
at android.support.v7.widget.RecyclerView$Recycler.getViewForPosition(RecyclerView.java:4617)
at android.support.v7.widget.LinearLayoutManager$LayoutState.next(LinearLayoutManager.java:1994)
at android.support.v7.widget.LinearLayoutManager.layoutChunk(LinearLayoutManager.java:1390)
at android.support.v7.widget.LinearLayoutManager.fill(LinearLayoutManager.java:1353)
at android.support.v7.widget.LinearLayoutManager.onLayoutChildren(LinearLayoutManager.java:574)
...

查了一些资料,大多是说因为数据源改变了,但是 adapter 却没有及时调用 notifyItemRangeChanged

解决方案

方法一

自己写一个继承 LinearLayoutManager 的包装类,在 onLayoutChildren() 方法里 try-catch 捕获该异常。然后给 recyclerView 设置该 LayoutManager

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class WrapContentLinearLayoutManager extends LinearLayoutManager {
//... constructor
@Override
public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
try {
super.onLayoutChildren(recycler, state);
} catch (IndexOutOfBoundsException e) {
Log.e("probe", "meet a IOOBE in RecyclerView");
e.printStackTrace();
}
}
}
...

RecyclerView recyclerView = (RecyclerView)findViewById(R.id.recycler_view);

recyclerView.setLayoutManager(new WrapContentLinearLayoutManager(activity, LinearLayoutManager.HORIZONTAL, false));

方法二

在进行数据移除和数据增加时,务必要保证RecyclerView的Adapter中的数据集和移除/添加等操作后的数据集保持一致!

这里,前者是在该Adapter内部,不妨叫做内部数据集,后者是开发人员传过给Adapter的,不妨叫外部数据集。更新RecyclerView数据时,需要保证外部数据集和内部数据集实时保持一致。每一次对外部数据集做改动时,都需要紧接着主动对外部数据集和内部数据集做一次同步操作

外部数据集同步到内部数据集,使用如下的方法:

  • notifyItemRangeRemoved();
  • notifyItemRangeInserted();
  • notifyItemRangeChanged();
  • notifyDataSetChanged();

这里对 notifyDataSetChange() 做些说明:

使用该方法的更新内部数据集,没有默认的动画效果,同时更新数据的效率页不如上面的方法,官方不推荐使用这种方式更新数据集。

方法三

使用notifyDataSetChanged同步外部数据集和内部数据集。该方法简单,但是失去了动画效果,并且更新数据的性能低。

另外,如果对外部数据集做了二次以上的操作,却只调用notifyDataSetChanged同步一次,也很有可能会报上文所示的错误。

参考文章:

Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×

keyboard_arrow_up 回到顶端