android-gradle-深入浅出-五:build-type.md

  • Android
  • Gradle
  • Android Studio
  • Task
  • Build Type

默认情况下,Android插件自动为项目构建一个debug和一个release版本的应用。这两个版本的不同主要体现在在非开发机上的调试功能以及APK的签名方式。debug版本使用一个用公开的name/password创建的key来签名(这样构建的时候就不需要提示输入密码了)。release版本在构建的时候不会进行签名,而是稍后在做。
这个可以使用gradle中的BuildType对象来进行配置。默认情况下,2个BuildType的实例会被创建,一个debug,一个release。Android插件允许自定义这两个实例,当然你也可以创建其他的build type。配置由buildTypes这个DSL来完成:

1
2
3
4
5
6
7
8
9
10
11
12
13
android {
buildTypes {
debug {
applicationIdSuffix ".debug"
}
jnidebug.initWith(buildTypes.debug)
jnidebug {
packageNameSuffix ".jnidebug"
jniDebuggable true
}
}
}

上面的代码完成了下列配置:
1.配置默认的debug Build Type
设置debug版本的报名为<应用id>.debug,这样就可以在设备上同时安装debug和release版本了。
2.创建一个新的BuildType,名字是jnidebug,同时配置它是复制自debug Build Type。
配置jnidebug开启debug版本的JNI组件,添加一个不同的包名后缀。

创建一个新的的Build Types非常简单,只需要在buildTypes下面通过调用initWith或者使用闭包添加一个新的元素。下表是可以配置的属性以及默认值:

属性明 debug版本默认值 release或其他版本 默认值
debuggable true false
jniDebuggable false false
renderscriptDebuggable false false
renderscriptOptimLevel 3 3
applicationIdSuffix null null
versionNameSuffix null null
signingConfig android.signingConfigs.debug null
zipAlignEnabled false true
minifyEnabled false false
proguardFile N/A (set only) N/A (set only)
proguardFiles N/A (set only) N/A (set only)

出了这些属性,Build Types还可以用来配置代码和资源文件。针对每一个Build Type,一个新的对应的sourceSet会被创建,这个sourceSet使用一个默认的路径src//。这就意味着Build Type的名字不能是main或者androidTest(这是由插件强制的),同时每个Build Type的名字必须是唯一的。

和其他的source sets一样,Build Type中的source set路径也是可以配置的

1
2
3
android {
sourceSets.jnidebug.setRoot('foo/jnidebug')
}

另外,对每一个Build Type,一个新的assemble任务会被创建.前面我已经讲过assembleDebug和assembleRelease任务了,这里就可解释这两个任务的来源了.当debug和release Build Types被预先创建的时候,他们对应的assemble任务也会被一起创建.

上面的build.gradle代码片段还会创建一个assembleJnidebug任务,assemble任务也会添加对assembleJniDebug任务的依赖,正如它对assembleDebug和assembleRelease任务的依赖一样。

小提示:你可以使用 gradle aJ命令来运行assembleJnidebug任务。

可能是使用场景:
仅在debug模式下使用的权限,release模式不使用。
debug时使用不同的实现
debug模式使用不同的资源(比如当一个资源的值依赖于签名的时候)

BuildType中的 代码/资源文件 将会被按照下列方式处理:
manifest配置会被merge到app manifest中
代码不会被merge,会被当做另一个代码目录(source folder)来使用
资源文件会覆盖同名的main中的资源文件

android-gradle-深入浅出四:自定义构建过程之配置manifest

  • Android
  • Gradle
  • Android Studio
  • Task

Android Gradle插件提供了大量的DSL来自定义构建过程。

配置Manifest条目

DSL提供了配置以下Manifest条目的功能:
minSdkVersion
targetSdkVersion
versionCode
versionName
applicationId (更加方便有效的包名 — 参考)
测试app的包名
Instrumentation test runner
示例:

1
2
3
4
5
6
7
8
9
10
11
android {
compileSdkVersion 19
buildToolsVersion "19.0.0"
defaultConfig {
versionCode 12
versionName "2.0"
minSdkVersion 16
targetSdkVersion 16
}
}

android元素中的defaultConfig元素就是我们用来配置Manifest的地方。早期版本的Android插件使用packageName来配置manifest中的packageName属性,从0.11.0开始,使用applicationId来代替packageName。这样可以消除应用的包名(其实就是应用的id)和java的包名之间的混淆。

更强大的是build文件中描述的配置可以是动态的,比如可以从文件或者自定义的逻辑中获取版本名称。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
def computeVersionName() {
...
}
android {
compileSdkVersion 19
buildToolsVersion "19.0.0"
defaultConfig {
versionCode 12
versionName computeVersionName()
minSdkVersion 16
targetSdkVersion 16
}
}

注意:不要使用作用域中的getter方法名作为函数名,比如在defaultConfig{}作用域中调用getVersionName()将会自动调用defaultConfig.getVersionName(),而不会调用自定义的方法。
如果某个属性的值没有使用DSL设置,这个属性将会使用某些默认值,下表展示了默认值的处理过程。

属性名 DSL对象中的默认值 默认值
versionCode -1 value from manifest if present
versionName null value from manifest if present
minSdkVersion -1 value from manifest if present
targetSdkVersion -1 value from manifest if present
applicationId null value from manifest if present
testApplicationId null applicationId + “.test”
testInstrumentationRunner null android.test.InstrumentationTestRunner
signingConfig null null
proguardFile N/A (set only) N/A (set only)
proguardFiles N/A (set only) N/A (set only)

如果你想在build脚本中使用自定义的逻辑来查询这些属性,第二列中的值就很重要。比如,你可以编写如下的代码:

1
2
3
if (android.defaultConfig.testInstrumentationRunner == null) {
// assign a better default...
}

如果属性的值仍然是null,那么在构建的时候,就会使用第三列的默认值,但是DSL元素中并不包含这些默认值,因此你不能在程序中查询这些值。这样做的目的是仅在必要的时候(构建时)才会去解析manifest内容。

android-gradle-深入浅出三:build-task

  • Android
  • Gradle
  • Android Studio
  • Task

构建任务(Build Tasks)

java和Android通用的任务
在build文件中使用了Android或者Java插件之后就会自动创建一系列可以运行的任务。

Gradle中有如下一下默认约定的任务:

  1. assemble
    该任务包含了项目中的所有打包相关的任务,比如java项目中打的jar包,Android项目中打的apk
  2. check
    该任务包含了项目中所有验证相关的任务,比如运行测试的任务
  3. build
    该任务包含了assemble和check
  4. clean
    该任务会清空项目的所有的输出,删除所有在assemble任务中打的包

assemble, check 和 build 任务实际上并不做任何事情,它们其实只是为插件提供了一个钩子,真正的事情都是由插件来完成的。

这样的话,开发人员就不需要关心我到底运行的是一个java项目还是一个Android项目,也不用关心我到底使用了哪些gradle插件,因为我都可以调用这些约定的任务来完成构建。

比如使用findbugs插件会创建一个新的任务,并且使得check任务依赖于这个新建的任务,这样每次执行check任务的时候,都会执行这个新建的任务。

在命令行执行

1
gradle tasks

会列出所有主要的任务

如果想看到全部的任务和它们的依赖,可以运行:

1
gradle tasks --all

注意:Gradle会自动检查一个任务的输入和输出。比如连续两次运行build任务的,Gradle会报告所有的任务都已经是最新刚运行过的了,不需要再次运行。这样的话,任务之间就算是有相互依赖,也不会导致重复的执行。

Java项目常用的任务

Java plugin 主要创建了两个任务:

  1. jar
    assemble任务会依赖jar任务,看名字就知道这是负责打jar包的任务。jar任务本身又会依赖很多其他的任务,比如classes任务,classes任务会编译java代码
  2. test
    check任务会依赖test任务,这个任务会运行所有的测试。测试代码使用testClasses任务编译,但是我们基本不用手动运行testClasses任务因为test任务已经添加了对它的依赖。

通常情况下,我们只要运行assemble和check任务就够了。
想查看java插件提供的所有任务以及他们的依赖可以点这个链接

Android项目常用的任务

和其他gradle插件一样,Android插件也提供了一些默认的任务,比如assemble,check,build,clean,同时它也提供了一些自己特有的任务,比如:

  1. connectedCheck
    运行那些需要在真机或者模拟器上执行的检查任务,这些任务会并行地在所有连接的设备上运行
  2. deviceCheck
    使用APIs连接远程设备执行检查.主要用于CI(持续集成)服务上.
    上面两个任务都会执行 assemble 和 check任务。新加这两个任务是很有必要的,这样可以保证我们可以运行那些不需要连接设备的检查任务。

注意:build任务并不依赖于deviceCheck或者connectedCheck

一个Android项目通常至少会有两种输出:debug apk和release apk。对应的gradle中有两个任务可以分别输出不同的apk:
assembleDebug
assembleRelease
这两个任务又会依赖其他的任务来构建一个apk。assemble任务依赖这两个任务,调用assemble任务就会生成两种apk。

小提示: Gradle支持在命令行使用camel风格的缩写来代替任务的名字,比如:

1
gradle aR

等同于

1
gradle assembleRelease

只要没有其他任务的缩写也是’aR’

check相关的任务的依赖:
check依赖lint
connectedCheck依赖 connectedAndroidTest和connectedUiAutomatorTest (还没有实现)
deviceCheck依赖于那些实现了test扩展的插件所提供的任务

最后,Android gradle插件还提供了install和uninstall任务,用来安装和卸载apk

android-gradle-深入浅出(二:项目结构)

  • Android
  • Gradle
  • Android Studio

这篇主要介绍使用Gradle的Android项目的目录结构,以及在Gradle中的配置

Gradle采用约定优于配置的原则,最简单方式是使用一个默认的目录结构。当然目录结构是可以自己修改的。

默认情况下,android gradle项目的代码都在src目录下,src下会有两个目录main和androidTest,其中main目录下是项目代码,androidTest目录下是测试代码。
src/main/
src/androidTest/

在main和androidTest目录下面,针对不同类型的code,又不会不同的目录。
对于Java和Android的Gradle插件,Java代码和Java资源分别对应了java目录和resources目录。Android插件还需要一些其他的目录和文件,比如
AndroidManifest.xml
res/
assets/
aidl/
rs/
jni/

下面我们来看一下如何更改这些默认的配置。

Android工程,默认情况下,java文件和resource文件分别在src/main/java和src/main/res目录下,在build.gradle文件,andorid{}里面添加下面的代码,便可以将java文件和resource文件放到src/java和src/resources目录下。(注意,如果你在gradle文件中修改了,只需要重新sync一下,gradle会自动创建新的目录,并将对应的文件移动到新的目录下)

1
2
3
4
5
6
7
8
9
10
sourceSets {
main {
java {
srcDir 'src/java'
}
resources {
srcDir 'src/resources'
}
}
}

更简便的写法是

1
2
3
4
sourceSets {
main.java.srcDirs = ['src/java']
main.resources.srcDirs = ['src/resources']
}

数组里其实是可以指定多个目录,这样就可以将代码和资源放置到多个目录下。
在看一个Android项目的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
android {
sourceSets {
main {
manifest.srcFile 'AndroidManifest.xml'
java.srcDirs = ['src']
resources.srcDirs = ['src']
aidl.srcDirs = ['src']
renderscript.srcDirs = ['src']
res.srcDirs = ['res']
assets.srcDirs = ['assets']
}
androidTest.setRoot('tests')
}
}

这里的main code其实使用的就是Android Gradle的默认值,而androidTest不再使用默认的androidTest目录,而是使用tests目录。

android-gradle-深入浅出(一)

  • Android
  • Gradle
  • Android Studio

gradle是Android开发中引入的全新的构建系统,因为全新的构建系统主要是出于下面的目的:

  1. 方便复用代码和资源
  2. 构建多种版本的apk更见简单,不论是为多渠道构建不同的apk还是构建不同环境的apk(debug,release)
  3. 方便配置,扩展,自定义构建过程
  4. 良好的IDE集成

为什么选择Gradle?

Gradle主要有以下几个有点:

  1. 使用领域驱动语言(DSL)来描述构建逻辑
  2. 构建脚本使用Groovy,可以方便的定制构建逻辑
  3. 内建的依赖管理系统,使用Maven或者Ivy
  4. 非常灵活。可以使用最佳实践但并不强制
  5. 良好的IDE支持

Gradle项目使用项目根目录下的build.gradle文件来配置

一个最简单的Java项目的build.gradle如下

1
apply plugin: 'java'

这表示改项目会使用Gradle的java插件,该插件是Gradle默认提供的,java插件提供了构建java应用和运行测试的功能。

最简单的Android项目build.gralde如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
buildscript {
repositories {
mavenCentral()
}
dependencies {
classpath 'com.android.tools.build:gradle:0.11.1'
}
}
apply plugin: 'android'
android {
compileSdkVersion 19
buildToolsVersion "19.0.0"
}

该Android构建文件包含3个部分:
buildscript { … } 配置驱动构建过程的代码,意思就是buildscript中的配置仅仅在构建过程中起作用,也就是构建程序所使用的配置。
上面的代码中就声明了我们的构建程序会使用Maven中央仓库,并且有一个classpath依赖是指向一个Maven库的。这个Maven库就是Android Gradle插件,版本号是0.11.1。

随后就是使用这个Android插件,和之前使用Java插件一样。

最后,android { … }配置了Android项目构建时需要的所有参数。
默认情况下,只需要指定compileSdkVersion和buildtoolsVersion,compileSdkVersion指定使用哪个版本的sdk编译,buildToolsVersion指定使用哪个版本的构建工具。

小心:

  1. 只需要使用android插件,再使用Java插件会报错.
  2. 在local.properties中使用sdk.dir属性指定SDK路径,或者你可以使用ANDROID_HOME环境变量。

Otto源码分析

  • android
  • square
  • Otto
  • event bus
  • 源码分析

构造函数

使用Otto通常是通过一个Provider提供一个Bus单例。
首先我们来分析一下Bus的构造函数,Bus类的构造函数最终都会调用Bus(ThreadEnforcer enforcer, String identifier, HandlerFinder handlerFinder)这个构造函数。
其中enforcer用来限制执行register,unregister以及post event的线程,如果执行这些函数的线程不是enforcer指定的线程,就会抛出异常。
identifier相当于给Bus起的一个名字,在toString方法中使用。
handlerFinder是整个event bus的核心,用于在register,unregister的时候寻找所有的subscriber和producer。handlerFinder不需要用户指定,默认使用HandlerFinder接口中定义的常量ANNOTATED,ANNOTATED本身就是HandlerFinder的匿名实现。

注册

如果一个类对某些事件感兴趣,需要调用register方法来注册监听这些事件,监听通过在方法上使用@Subscribe来实现,Otto通过方法的参数来决定是否调用该方法。
register方法首先会调用handlerFinder的findAllProducers(object)方法去找到所有使用了@Produce注解的方法。findAllProducers其实是委托AnnotatedHandlerFinder.findAllProducers方法。在AnnotatedHandlerFinder中,定义了一个静态变量SUBSCRIBERS_CACHE

1
2
private static final Map<Class<?>, Map<Class<?>, Method>> PRODUCERS_CACHE =
new HashMap<Class<?>, Map<Class<?>, Method>>();

PRODUCERS_CACHE 的key是监听类,就是调用bus.register()的类,value本身又是一个map,这个map的key是事件的class,value是生产事件的方法。
比如下面这个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
public class MainActivity extends Activity {
@Inject Bus bus;
@Override
public void onResume() {
bus.register(this);
}
@Produce public ClickEvent produceClick() {
// TODO: React to the event somehow!
return new ClickEvent();
}
}

在PRODUCERS_CACHE中就会有一条记录,它的key是MainActivity.class,value对应的map中,key是ClickEvent.class,value是produceClick Method对象。
SUBSCRIBERS_CACHE:
MainActivity.class -》
ClickEvent.class -》produceClick

当调用AnnotatedHandlerFinder的findAllProducers方法时,会先根据传入的对象的类型,检查是否已经被缓存到PRODUCERS_CACHE,如果没有的话,就会调用loadAnnotatedMethods,利用反射去寻找所有使用了@Produce注解的方法,并且将结果缓存到PRODUCERS_CACHE中。最后,会从PRODUCERS_CACHE中取出监听类的所有Produce方法,遍历这些方法,为一个方法构建一个EventProducer对象,并将这个EventProducer对象放到一个以事件的class作为key的map中,然后返回这个map。EventProducer类包含了Produce方法和该方法所属的对象,并且提供了调用Produce方法的功能。

回到Bus的Register方法,调用完findAllProducers方法之后,会遍历传入的监听类的Produce方法,并且根据Produce方法的返回值类型,来检查是否已经有对应的Subscribe存在,如果有的话,就会调用Subscribe方法,并将Producer的返回值传入。

1
2
3
4
5
6
Set<EventHandler> handlers = handlersByType.get(type);
if (handlers != null && !handlers.isEmpty()) {
for (EventHandler handler : handlers) {
dispatchProducerResultToHandler(handler, producer);
}
}

这里需要注意的是,Bus对象两个map类型的常量,用来缓存所有事件的Producer和Subscriber。

1
2
3
4
5
6
7
/** All registered event handlers, indexed by event type. */
private final ConcurrentMap<Class<?>, Set<EventHandler>> handlersByType =
new ConcurrentHashMap<Class<?>, Set<EventHandler>>();
/** All registered event producers, index by event type. */
private final ConcurrentMap<Class<?>, EventProducer> producersByType =
new ConcurrentHashMap<Class<?>, EventProducer>();

从定义我们可以看出,一种事件只能有一个Producer,却可以有多个Subscriber。

找到了所有的producers之后,就是调用handlerFinder.findAllSubscribers(object)来寻找object中使用@Subscribe注解的方法,过程和findAllProducers类似,唯一的不同是一个事件可以有多个subscriber,因此findAllSubscribers的返回值类型是Map, Set>。其中EventHandler包含了subscribe方法和订阅事件的对象的信息。

找到监听类所有的subscribe方法之后,就需要查看bus中时候有和这些subscribe方法对应的producer方法,如果有的话,就会使用调用subscribe方法。这也就是文档上说的,一旦有新的subscriber订阅了某一事件,并且该事件有对应的producer,那么subscriber方法就会被立即调用,并且传入producer方法的返回值。

发送事件

post(Obejct event)方法用来发送事件给所有订阅者,它接收一个Object类型的参数,说明Otto的事件可以是任意类型的对象。post方法首先会获取所有event对象的父类

1
Set<Class<?>> dispatchTypes = flattenHierarchy(event.getClass());

然后遍历这些父类,找到他们的所有订阅者,发送事件。这表明任何订阅了event对象父类的订阅者也都会收到event事件。值得注意的是Otto使用ThreadLocal类型来存放事件队列ThreadLocal> eventsToDispatch,这样极大的简化了多线程模式下的开发。

取消订阅

unregister方法,做的事情和register刚好想法,从缓存中清除所有和当前监听对象相关的producers和subscribers。

Otto使用入门

  • android
  • square
  • Otto
  • event bus

介绍

Otto是square公司出的一个事件库(pub/sub模式),用来简化应用程序组件之间的通讯。
Otto 修改自Google的Guava库,专门为Android平台进行了优化。

使用

Otto本身是为Android平台专门开发的,使用的时候最好是使用单例模式。

1
Bus bus = new Bus();

bus对象只有作为单例共享的时候才足够高效,推荐使用依赖注入框架来注入单例对象或者采用类似的机制。

发布事件

发布一个事件很简单,调用post方法就可以,post方法可以接受任何类型对象

1
bus.post(new AnswerAvailableEvent(42));

订阅事件

订阅只需要在方法上加上@Subscribe注解,同时在适当的地方调用register方法:

1
2
3
@Subscribe public void answerAvailable(AnswerAvailableEvent event) {
// TODO: React to the event somehow!
}

注意subscribe方法接收的参数类型需要和post参数的类型一致或者是post参数类型的父类。

1
bus.register(this);

一旦调用了register方法,Otto就会通过反射去寻找所有带有@Subscribe或者@Produce注解的方法,并将这些方法缓存下来。只有在调用了register之后,该类里面标注了@Subscribe或者@Produce的方法才会在适当的时候被调用。另外,当不需要订阅事件的时候,可以调用unregister来取消订阅。

生产者

有时候当订阅某个事件的时候,希望能够获取当前的一个值,比如订阅位置变化事件的时候,希望能拿到当前的位置信息。Otto中@Produce正是扮演了这么一个生产者的角色。
@Produce也是用于方法,并且这个方法的参数必须为空,返回值是你要订阅的事件的类型。

1
2
3
4
@Produce public AnswerAvailableEvent produceAnswer() {
// Assuming 'lastAnswer' exists.
return new AnswerAvailableEvent(this.lastAnswer);
}

使用@Produce之后,也需要调用bus.register()。调用了register方法之后,所有之前订阅AnswerAvailableEvent事件的方法都会被执行一次,参数就是produceAnswer方法的返回值,之后任何新的订阅了AnswerAvailableEvent事件的方法,也都会立即调用produceAnswer方法。

线程限制

可以指定@Subscribe和@Produce标注的回调方法所运行的线程,默认是在MainThread中执行。

1
2
3
// 这两个方法是等价的
Bus bus1 = new Bus();
Bus bus2 = new Bus(ThreadEnforcer.MAIN);

如果不关心在哪个线程执行,可以使用ThreadEnforcer.ANY,甚至可以使用自己实现的ThreadEnforcer接口。

深入浅出RxJava(一:基础篇)

原文链接

RxJava正在Android开发者中变的越来越流行。唯一的问题就是上手不容易,尤其是大部分人之前都是使用命令式编程语言。但是一旦你弄明白了,你就会发现RxJava真是太棒了。

这里仅仅是帮助你了解RxJava,整个系列共有四篇文章,希望你看完这四篇文章之后能够了解RxJava背后的思想,并且喜欢上RxJava。

基础

RxJava最核心的两个东西是Observables(被观察者,事件源)和Subscribers(观察者)。Observables发出一系列事件,Subscribers处理这些事件。这里的事件可以是任何你感兴趣的东西(触摸事件,web接口调用返回的数据。。。)

一个Observable可以发出零个或者多个事件,知道结束或者出错。每发出一个事件,就会调用它的Subscriber的onNext方法,最后调用Subscriber.onNext()或者Subscriber.onError()结束。

Rxjava的看起来很想设计模式中的观察者模式,但是有一点明显不同,那就是如果一个Observerble没有任何的的Subscriber,那么这个Observable是不会发出任何事件的。

Hello World

创建一个Observable对象很简单,直接调用Observable.create即可

1
2
3
4
5
6
7
8
9
Observable<String> myObservable = Observable.create(
new Observable.OnSubscribe<String>() {
@Override
public void call(Subscriber<? super String> sub) {
sub.onNext("Hello, world!");
sub.onCompleted();
}
}
);

这里定义的Observable对象仅仅发出一个Hello World字符串,然后就结束了。接着我们创建一个Subscriber来处理Observable对象发出的字符串。

1
2
3
4
5
6
7
8
9
10
Subscriber<String> mySubscriber = new Subscriber<String>() {
@Override
public void onNext(String s) { System.out.println(s); }
@Override
public void onCompleted() { }
@Override
public void onError(Throwable e) { }
};

这里subscriber仅仅就是打印observable发出的字符串。通过subscribe函数就可以将我们定义的myObservable对象和mySubscriber对象关联起来,这样就完成了subscriber对observable的订阅。

1
myObservable.subscribe(mySubscriber);

一旦mySubscriber订阅了myObservable,myObservable就是调用mySubscriber对象的onNext和onComplete方法,mySubscriber就会打印出Hello World!

更简洁的代码

是不是觉得仅仅为了打印一个hello world要写这么多代码太啰嗦?我这里主要是为了展示RxJava背后的原理而采用了这种比较啰嗦的写法,RxJava其实提供了很多便捷的函数来帮助我们减少代码。

首先来看看如何简化Observable对象的创建过程。RxJava内置了很多简化创建Observable对象的函数,比如Observable.just就是用来创建只发出一个事件就结束的Observable对象,上面创建Observable对象的代码可以简化为一行

1
Observable<String> myObservable = Observable.just("Hello, world!");

java

接下来看看如何简化Subscriber,上面的例子中,我们其实并不关心OnComplete和OnError,我们只需要在onNext的时候做一些处理,这时候就可以使用Action1类。

1
2
3
4
5
6
Action1<String> onNextAction = new Action1<String>() {
@Override
public void call(String s) {
System.out.println(s);
}
};

subscribe方法有一个重载版本,接受三个Action1类型的参数,分别对应OnNext,OnComplete, OnError函数。

1
myObservable.subscribe(onNextAction, onErrorAction, onCompleteAction);

java

这里我们并不关心onError和onComplete,所以只需要第一个参数就可以

1
2
myObservable.subscribe(onNextAction);
// Outputs "Hello, world!"

java

上面的代码最终可以写成这样

1
2
3
4
5
6
7
Observable.just("Hello, world!")
.subscribe(new Action1<String>() {
@Override
public void call(String s) {
System.out.println(s);
}
});

使用java8的lambda可以使代码更简洁

1
2
Observable.just("Hello, world!")
.subscribe(s -> System.out.println(s));

Android开发中,强烈推荐使用retrolambda这个gradle插件,这样你就可以在你的代码中使用lambda了。

变换

让我们做一些更有趣的事情吧!

比如我想在hello world中加上我的签名,你可能会想到去修改Observable对象:

1
2
Observable.just("Hello, world! -Dan")
.subscribe(s -> System.out.println(s));

如果你能够改变Observable对象,这当然是可以的,但是如果你不能修改Observable对象呢?比如Observable对象是第三方库提供的?比如我的Observable对象被多个Subscriber订阅,但是我只想在对某个订阅者做修改呢?
那么在Subscriber中对事件进行修改怎么样呢?比如下面的代码:

1
2
Observable.just("Hello, world!")
.subscribe(s -> System.out.println(s + " -Dan"));

这种方式仍然不能让人满意,因为我希望我的Subscribers越轻量越好,因为我有可能会在mainThread中运行subscriber。另外,根据响应式函数编程的概念,Subscribers更应该做的事情是“响应”,响应Observable发出的事件,而不是去修改。

如果我能在某些中间步骤中对“Hello World!”进行变换是不是很酷?

操作符(Operators)

操作符就是为了解决对Observable对象的变换的问题,操作符用于在Observable和最终的Subscriber之间修改Observable发出的事件。RxJava提供了很多很有用的操作符。

比如map操作符,就是用来把把一个事件转换为另一个事件的。

1
2
3
4
5
6
7
8
Observable.just("Hello, world!")
.map(new Func1<String, String>() {
@Override
public String call(String s) {
return s + " -Dan";
}
})
.subscribe(s -> System.out.println(s));

使用lambda可以简化为

1
2
3
Observable.just("Hello, world!")
.map(s -> s + " -Dan")
.subscribe(s -> System.out.println(s));

是不是很酷?map()操作符就是用于变换Observable对象的,map操作符返回一个Observable对象,这样就可以实现链式调用,在一个Observable对象上多次使用map操作符,最终将最简洁的数据传递给Subscriber对象。

map操作符进阶

map操作符更有趣的一点是它不必返回Observable对象返回的类型,你可以使用map操作符返回一个发出新的数据类型的observable对象。

比如上面的例子中,subscriber并不关心返回的字符串,而是想要字符串的hash值

1
2
3
4
5
6
7
8
Observable.just("Hello, world!")
.map(new Func1<String, Integer>() {
@Override
public Integer call(String s) {
return s.hashCode();
}
})
.subscribe(i -> System.out.println(Integer.toString(i)));

很有趣吧?我们初始的Observable返回的是字符串,最终的Subscriber收到的却是Integer,当然使用lambda可以进一步简化代码:

1
2
3
Observable.just("Hello, world!")
.map(s -> s.hashCode())
.subscribe(i -> System.out.println(Integer.toString(i)));

前面说过,Subscriber做的事情越少越好,我们再增加一个map操作符

1
2
3
4
Observable.just("Hello, world!")
.map(s -> s.hashCode())
.map(i -> Integer.toString(i))
.subscribe(s -> System.out.println(s));

不服?

是不是觉得我们的例子太简单,不足以说服你?你需要明白下面的两点:

1.Observable和Subscriber可以做任何事情
Observable可以是一个数据库查询,Subscriber用来显示查询结果;Observable可以是屏幕上的点击事件,Subscriber用来响应点击事件;Observable可以是一个网络请求,Subscriber用来显示请求结果。

2.Observable和Subscriber是独立于中间的变换过程的。
在Observable和Subscriber中间可以增减任何数量的map。整个系统是高度可组合的,操作数据是一个很简单的过程。

使用Android依赖注入工具Dagger(二)

[翻译]原文地址

如果你读过该系列的第一篇文章,那么你一定看看在真实的代码中如何去实现。Dagger官网有一个咖啡机的例子,同时Jake Wharton在github上面也提供了一个更棒的例子给更有经验的用户参考。但是我们需要一个稍微简单一些的例子,而咖啡机并不符合我们的业务场景,这篇文章将提供一个使用依赖注入的例子来帮助我们理解一些依赖注入的基本知识。

代码已经放到了github上面

在项目中使用Dagger

使用Dagger必须要添加两个类库到项目中:

1
2
3
4
5
dependencies {
compile fileTree(dir: 'libs', include: ['*.jar'])
compile 'com.squareup.dagger:dagger:1.2.+'
provided 'com.squareup.dagger:dagger-compiler:1.2.+'
}

第一个是Dagger的类库,第二个是Dagger的编译库。编译库负责生成相应的类,在这些生成的类中,所有的依赖已经被注入。通过使用预编译技术,Dagger避免了对反射的使用。编译库仅仅是在编译的时候使用,因此我们把它标记为provided,这样就不会在最终生成的apk文件中包含它。

创建第一个模块

使用Dagger的时候,模块(module)将会成为你最常用的部分,你必须习惯这一点。模块就是一个类,这个类负责提供那些需要被注入的对象。模块通过@Module注解来定义。还有一些参数可以配置,我会在用到的时候再详细解释。

创建一个叫AppModule的类,它将提供Application的实例。应用程序中会有很多地方使用application对象,因此使得这个对象容易获取会给开发带来很大的便利。例子中的App类继承自Application类,并且已经被添加到Manifest中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@Module(
injects = {
App.class
}
)
public class AppModule {
private App app;
public AppModule(App app) {
this.app = app;
}
@Provides @Singleton public Context provideApplicationContext() {
return app;
}
}

让我们来看一下有哪些新东西?

@Module:标识一个类为Dagger的模块。

injects:那些需要这个模块注入他们的依赖的类。我们需要指定那些将被加入到Dagger的对象图中的类,后面会讲到这点。

@Provides:标识一个方法为注入的提供者。方法的名字可以随便写,但是返回值决定了它所能提供的类型。

@Singleton:一旦有了这个注解,那么这个方法将会永远返回相同的对象,这比常见的单例对象要好用的多。如果没有这个注解,每次注入这种类型的对象,都会得到一个全新的实例。在这个例子中,我们并没有在方法中创建一个新的对象,而是返回了一个已经存在的对象,即时我们不添加Singleton注解,该方法也会永远返回相同的实例,但是加上这个注解,可以增加代码的可读性。Application对象永远只会有一个。

我们将会的domain这个包下创建一个新的模块。在分层架构的没一层上都至少有一个模块会带来很多好处。这个模块会提供analytics manager对象。analytics manager对象会在应用启动的时候显示一个toast。在真实的项目中,这个manager对象可能会调用任何一个提供分析功能的服务比如Google Analytics

1
2
3
4
5
6
7
8
9
10
11
@Module(
complete = false,
library = true
)
public class DomainModule {
@Provides @Singleton public AnalyticsManager provideAnalyticsManager(Application app){
return new AnalyticsManager(app);
}
}

把这个模块标记为 not complete,是因为这个模块的一些依赖需要由其他的模块来提供。这里显然值得就是application对象,它需要由AppModule提供。当我们向Dagger请求AnalyticsManager的对象的时候,Dagger将会调用这个方法,同时会检测到这个方法依赖于另外一个模块,这时请求会被转发到对象图中。我们还把这个模块标记成library,因为Dagger编译器会检测到这个模块自己以及它所注入的类都没有使用AnalyticsManager,它仅仅是作为AppModule的一个库模块。

我们将会声明AppModule包含这个模块,之前的代码现在是这样:

1
2
3
4
5
6
7
8
9
10
11
@Module(
injects = {
App.class
},
includes = {
DomainModule.class
}
)
public class AppModule {
...
}

includes的作用就是这样。

创建对象图(object graph)

对象图是存放所有依赖的地方。对象图包含了所有创建的实例,并且可以把这些实例注入到添加到对象图中的类中。

之前的例子中(AnalyticsManager),我们使用了经典的注入方式即构造函数注入。但是Android中有很多(Application, Activity),我们不能控制他们的构造函数,因此我们需要其他的方式来注入依赖。

App类中展示了如何将对象图和这种直接的依赖注入结合起来使用。主要的对象图在Application中被创建,同时application对象自己也被注入到对象图中,这样就可以获得它所依赖的对象了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class App extends Application {
private ObjectGraph objectGraph;
@Inject AnalyticsManager analyticsManager;
@Override public void onCreate() {
super.onCreate();
objectGraph = ObjectGraph.create(getModules().toArray());
objectGraph.inject(this);
analyticsManager.registerAppEnter();
}
private List<Object> getModules() {
return Arrays.<Object>asList(new AppModule(this));
}
}

使用@Inject注解来标识依赖。依赖必须是public或者default scope的,这样Dagger才能对他们赋值。我们先创建了一个模块的数组(这里数组中只有一个模块,Do买呢Modu了被包含在AppModule中了),然后再使用这个数组去创建对象图,随后我们手动的注入了App的对象。所有这些调用完成之后,依赖就可以被注入了,我们可以放心的使用AnalyticsManager的方法了。

小结

现在你已经了解了Dagger。对象图(ObjectGraph)和模块(Module)是Dagger中最有意思的部分,想熟练使用Dagger的话必须要精通这两样。Dagger还提供了其他的一些工具比如延迟注入(lazy injection)和提供器注入(provider injection),请参考官网的介绍,但是在熟悉今天介绍的内容之前,我不推荐深入研究其他的。

代码

下一篇将主要讲解有作用域的对象图(scoped object graphs)。有作用域的对象图主要就是指那些只存在于创建者生命周期中的对象图。通常会在activity中使用。

使用Android依赖注入工具Dagger(一)

[翻译] 原文地址

在这个系列中,我将向你解释什么是依赖注入,为什么使用依赖注入以及在Android中如何使用依赖注入框架dagger, dagger是square出品的专门为Android优化设计的依赖注入框架。这篇文章是是我上一篇关于如何在Android项目中使用MVP模式的姊妹篇,读者中一定有人对在同一个项目中结合MVP模式和dagger感兴趣,我认为这两者可以完美的结合在一起。

开始的部分可能有些偏理论。知道什么是依赖注入以及为什么会有依赖注入很重要,不然的话很可能会觉得我们费那么大劲引入依赖注入很不值得。

什么是依赖

想要注入依赖,必须要先弄懂什么是依赖。简而言之,依赖就是两个模块之间的耦合,常见的情况是一个模块需要使用另外一个模块来完成某些任务。

为什么存在依赖是很危险的

模块的依赖是很危险的,因为一旦我们想要替换其中的一个模块,我们必须要更改和要替换的模块存在耦合的模块。尤其是当我们想创建一个可以测试的应用的时候,单元测试通常要求被测试的模块是和其他模块隔离开的。测试中通常是将非测试模块mock掉(相当于用假的实现相同接口的模块去替换)。看看下面的一段代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
public class Module1{
private Module2 module2;
public Module1(){
module2 = new Module2();
}
public void doSomething(){
...
module2.doSomethingElse();
...
}
}

我们如何去做到仅仅测试doSomething这个方法,而不用去关心doSomethingElse这个方法呢?如果测试失败了,那么又是哪个方法导致的呢?我们无法知道。如果doSomethingElse执行了保存到数据库相关的代码或者调用的其他的api,情况会变的更糟糕。

每一个new都是一个很难更改的依赖,我们应该尽可能的避免直接使用new。减少模块也不是一个避免过多依赖的好办法,别忘了单一职责原则(single responsibility principle)。

如何解决这个问题?依赖反转(dependency inversion)

既然不能使用new来直接创建依赖的对象,那就只好通过其他办法了。你猜是什么办法?yes,就是使用构造器来传入。这就是依赖反转的最基本的概念—不依赖具体的对象,依赖于抽象。
之前的那段代码可以改成这样:

1
2
3
4
5
6
7
8
9
10
11
12
13
public class Module1{
private Module2 module2;
public Module1(Module2 module2){
this.module2 = module2
}
public void doSomething(){
...
module2.doSomethingElse();
...
}
}

那么到底什么是依赖注入呢?

你猜对了,通过构造器传入依赖的对象就是依赖注入,这样就不需要在一个模块中去创建另外一个模块了。对象是在别的地方被创建然后通过构造器传入依赖它的对象中。

但是新的问题又出现了,如果我不能在一模块中创建另外一个模块,总要有一个地方来负责实例化这些模块吧。而且,如果我们创建了一个有很长构造器参数列表模块,代码将会变的令人恶心难读。这正是依赖注入器(independency injector)要帮助我们解决的。

什么是依赖注入器?

你可以把他想象成一个这样的模块,它负责构造所有其他模块的实例,并且传入这些模块的依赖。现在我们的应用程序中只有一个入口来负责创建所有模块的实例,而这个入口完全在我们的控制下。

什么是Dagger

Dagger是一个完全为低端设备设计的依赖注入器。绝大多数的依赖注入器都是基于反射来实现的。反射的确很酷,但是在低端设备上执行反射的效率太低。Dagger使用的是预编译技术来创建所有依赖的类。这样就避免了使用反射。Dagger虽然没有其他的依赖注入框架强大,但它是效率最高的。

Dagger仅仅是为了测试而使用么?

当然不是。Dagger将会让在其他app里面重用你的模块变的更加简单,甚至在同一个应用中修改摸个模块也会变的更加容易。比如一个应用,在开发版本中使用本地文件来获取数据,而在正式版本中,使用API 服务获取数据,使用依赖注入在不同的模式下注入不同的对象将会很完美的解决这个问题。

小结

我知道这篇文章很晦涩,但是解释清楚下面几篇将会使用的术语很重要。我们已经知道了什么是依赖,使用依赖反转的好处以及如何使用一个依赖注入框架去实现它。

接下来的几篇文章,让我们尽情的coding,以解手痒吧!