解答了我很多问题,感谢大家的分享精神,以前查东西不愿意在国内论坛什么的,因为经常有这样的场景楼主提出问题,然后大家讨论,由于参与的人未必会全力去调研,一些比较绕的问题,最终还是楼主本人找到了答案,然后楼主基本上会说“问题解决”基本不会告诉你是怎么解决的,他未必是为了保密,很多时候都是懒得写出来。虽然如果发生战争我绝对会帮解放军打老外,但老外的论坛确实不会出现这情况也是事实。。。 第一次发文就牢骚,差评。
想全面写一个安卓架构方面的东西,但由于我是从windows到安卓的,或者说2个都做,发现两者之间架构原理一样,但用到的东西还是差距挺多的,我学安卓就是从架构开始的。因为当年学windows没有这样做,导致有很多坏习惯。所以现在第一件事要保证就是,代码是高质量的,而高质量的代码,架构是重中之重。 至于一些需要积累的东西,那没法学,只能慢慢来了。
下面先来翻译下dagger2的官方文档,毕竟他也是我们架构的材料之一。(我现在学东西是以官方文档为主,各位大神博客为辅)
Dagger2官方文档
在任何APP中最好的类都是那些做事情的类:BarcodeDecoder(注:条形码解码器, 这只是个类名,开场白吗,弄几个高级的类名充下门面而已,不必深究,下同), KoopaPhysicsEngine, AudioStreamer. 业务逻辑使这些主干类具有依赖关系,可能是通过一些类把他们交织在一起,比如:BarcodeCameraFinder,DefaultPhysicsEngine,当然可能还有HttpStreamer。那么我们可以说这些做事情的类,是我们的主干类。(蜀汉之五虎)
和主干类比起来,任何应用程序中最糟糕的类是那些根本不做什么就占用空间的类:BarcodeDecoderFactory、CameraServiceLoader和MutableContextWrapper。这些类就像是笨拙的胶带一样,把主干类粘合起来。(注:dagger能负责创造和保持这些主干类,所以它先贬低通常的factory类什么的,套路啊) (三国:我诸葛亮(dagger2)管理五虎将就够了,你们其它人都靠边,你们谁需要关羽出战找我,我保证关羽全力出战)
Dagger是这些Factory类的替代品,它实现了依赖注入设计模式,没有编写样板的负担。它允许您关注感兴趣的类。声明依赖关系,指定如何满足它们,并交付应用程序。(我诸葛亮能者多劳了)
通过在标准javax.inject
上注入注解(JSR 330),每个类都很容易测试。仅仅为了将RpcCreditCardService交换为FakeCreditCardService,您不需要一堆样板文件。
依赖注入不仅仅用于测试。它还使创建可重用的、可互换的模块变得容易。你可以在你所有的应用中共享相同的AuthenticationModule。您可以在开发过程中运行DevLoggingModule,在生产环境中运行ProdLoggingModule,以获得每种情况下的正确行为。
为什么Dagger2与众不同
依赖注入框架已经存在多年,具有各种各样的用于配置和注入的api。那么,为什么要重新发明轮子呢?Dagger 2是第一个用生成的代码实现完整堆栈的。指导原则是生成代码,模仿用户可能已经手写的代码,以确保依赖注入尽可能简单、可跟踪和高性能。更多关于设计的背景资料,请观看Gregory Kick的演讲(幻灯片)。(链接被墙了,也没必要看)
使用Dagger
我们将通过构建一个咖啡机来演示依赖注入和Dagger。
有关可以编译和运行的完整示例代码,请参见Dagger的咖啡机实例。
声明依赖
Dagger构造应用程序类的实例并满足它们的依赖关系。它使用javax.inject.Inject注解以确定它感兴趣的构造函数和字段
使用@Inject注释Dagger应该用来创建类实例的构造函数。当请求一个新实例时,Dagger将获得所需的参数值并调用这个构造函数。(如下代码所示)
class Thermosiphon implements Pump {
private final Heater heater;
@Inject
Thermosiphon(Heater heater) {
this.heater = heater;
}
...
}
Dagger可以直接注入字段。在本例中,它为heater字段获得一个Heater(加热器)实例,为pump字段获得一个Pump(泵)实例。(如下代码所示)
class CoffeeMaker {
@Inject Heater heater;
@Inject Pump pump;
...
}
如果您的类有@Inject注解的字段,但没有@Inject注解的构造函数,那么Dagger会在被请求时注入这些字段,但不会创建新的实例。添加一个带@Inject注释的无参数构造函数,以表明Dagger也会创建实例。
Dagger也支持方法注入,不过构造函数或字段注入通常是首选。
缺少@Inject注解的类不能由Dagger构造。
满足依赖关系
默认情况下,Dagger通过如上所述构造请求类型的实例来满足每个依赖项。当您请求一个咖啡机时,它将通过调用new CoffeeMaker()并设置其可注入字段来获得一个。
但是@Inject并不是在任何地方都有效:
- 接口不能被构造。
- 第三方类不能被注解(annotated)。
- 可配置对象必须被配置!(Configurable objects must be configured!)
对于@Inject不足或不合适的情况,可以使用@Provides
注释的方法来满足依赖关系。方法的返回类型定义了它满足的依赖项。
例如,当需要一个加热器时,provideHeater()就会被调用:
@Provides static Heater provideHeater() {
return new ElectricHeater();
}
@Provides方法也可以有自己的依赖关系。例如,由于ElectricHeater有一个@Inject构造函数,上面的方法可以写成:
@Provides static Heater provideHeater(ElectricHeater heater) {
return heater;
}
通过这种方式,Dagger负责实例化ElectricHeater,而@Provide方法仅用于将其别名为Heater类型。
在本例中,我们可以使用@Binds方法来定义别名,从而进一步简化工作。与@Provides不同,@Binds方法是抽象的,并且没有实现:
@Binds abstract Heater bindHeater(ElectricHeater impl);
最后,所有的@Provides方法必须属于一个模块。这些模块指的是具有@Module注释的类。(如下代码所示)
@Module
interface HeaterModule {
@Binds abstract Heater bindHeater(ElectricHeater impl);
}
按照约定,@Provides方法用provide前缀命名,@Binds方法用bind前缀命名,模块类用Module后缀命名。
构建流程
带@Inject和@Provides注释的类形成了一个对象图,由它们的依赖关系链接。像应用程序的主方法或Android应用程序那样调用代码,通过定义良好的根集访问该图。在Dagger 2中,这个集合是由一个接口定义的,该接口的方法没有参数,并且返回所需的类型。通过对这样的接口应用@Component注释并将module类型传递给modules参数,Dagger 2然后完全生成该契约的实现。
@Component(modules = DripCoffeeModule.class)
interface CoffeeShop {
CoffeeMaker maker();
}
该实现与前缀为Dagger的接口具有相同的名称。通过builder()方法获取实例,并使用返回的builder设置依赖关系并build()一个新实例。
CoffeeShop coffeeShop = DaggerCoffeeShop.builder()
.dripCoffeeModule(new DripCoffeeModule())
.build();
注意:如果@Component不是顶级类型,那么生成的组件的名称将包括包含它的外部类型的名称,并用下划线连接。例如,以下代码:
class Foo {
static class Bar {
@Component
interface BazComponent {}
}
}
将生成一个名为DaggerFoo_Bar_BazComponent的Component
任何具有可访问的默认构造函数的模块都可以省略,因为如果没有设置,构建器将自动构造一个实例。任何模块的@Provides方法都是静态的,实现根本不需要实例。如果所有依赖项都可以在用户不创建依赖项实例的情况下构建,那么生成的实现也将有一个create()方法,可以使用该方法来获得新实例,而无需处理构建器
CoffeeShop coffeeShop = DaggerCoffeeShop.create();
现在,我们的CoffeeApp可以简单地使用CoffeeShop的Dagger-generated 来获得一个 fully-injected CoffeeMaker。
public class CoffeeApp {
public static void main(String[] args) {
CoffeeShop coffeeShop = DaggerCoffeeShop.create();
coffeeShop.maker().brew();
}
}
现在已经构造好了架构,并且注入了入口点,可以运行我们的咖啡机应用程序了。
$ java -cp ... coffee.CoffeeApp
~ ~ ~ heating ~ ~ ~
=> => pumping => =>
[_]P coffee! [_]P
关于绑定
上面的示例展示了如何使用一些更典型的绑定构造组件,但是有多种机制可以用于绑定。以下可作为依赖项使用,并可用于生成格式良好的组件:
- 在@Module中通过@Provides声明的方法,且此@Module来自@Component.modules的直接引用或者是通过@Module.includes传递
- 任何具有未限定作用域的@Inject构造函数的类型,或者具有匹配某个组件作用域的@Scope注释的类型
- 组件依赖项的组件提供的方法
- 组件本身
- 不受限制的builders 包含的subcomponent
- 上述任何绑定的提供程序或惰性包装器
- 上述任何绑定的延迟提供程序(e.g., Provider
>) - 任何类型的MembersInjector
单例和范围绑定
用@Singleton
注释@Provides方法或可注入类。这将为其所有客户端使用该值的单个实例。
@Provides @Singleton static Heater provideHeater() {
return new ElectricHeater();
}
可注入类上的@Singleton注释也可以用作文档。它提醒潜在的维护者,这个类可以由多个线程共享。
@Singleton
class CoffeeMaker {
...
}
由于Dagger 2将体系中的作用域实例与组件实现的实例相关联,因此组件本身需要声明它们打算表示的作用域。例如,在同一个组件中使用@Singleton绑定和@RequestScoped绑定没有任何意义,因为这些作用域具有不同的生命周期,因此他们必须存在于具有不同生命周期的组件中。要声明一个组件与给定的作用域关联,只需将作用域注释应用到组件接口。
@Component(modules = DripCoffeeModule.class)
@Singleton
interface CoffeeShop {
CoffeeMaker maker();
}
组件可以应用多个范围注释。这声明了它们都是相同作用域的别名,因此该组件可以包含具有它声明的任何作用域的作用域绑定。
可重用的范围
有时您希望限制实例化@Inject构造的类或调用@Provides方法的次数,但是您不需要保证在任何特定组件或子组件的生命周期内使用完全相同的实例。这在诸如Android这样的环境中非常有用,因为在这些环境中,分配是很昂贵的。
对于这些绑定,您可以应用@Reusable
作用域。@Reusable作用域绑定与其他作用域不同,它不与任何单个组件关联。相反,实际使用绑定的每个组件将缓存返回的或实例化的对象。
这意味着,如果在组件中安装带有@Reusable绑定的模块,但是只有子组件实际使用该绑定,那么只有该子组件将缓存绑定的对象。如果不共享一个祖先的两个子组件都使用这个绑定,那么它们都将缓存自己的对象。如果组件的祖先已经缓存了对象,子组件将重用它。
不能保证组件只调用绑定一次,因此将@Reusable应用到返回可变对象的绑定,或引用相同实例很重要的对象,是危险的。对于不可变对象使用@Reusable是安全的,如果您不关心它们被分配了多少次,那么您将不考虑它们的作用域。
(注:上边这一大段文字有点不好理解,结合下面代码来理解吧)
@Reusable // 用多少勺子没关系,但不要浪费。
class CoffeeScooper {
@Inject CoffeeScooper() {}
}
@Module
class CashRegisterModule {
@Provides
@Reusable // 不要这样做!你在乎你把钱放在哪个收银机里。
// 应该使用特定的范围代替.
static CashRegister badIdeaCashRegister() {
return new CashRegister();
}
}
@Reusable // 不要这样做!您确实需要每次都使用一个新的过滤器,因此应该取消其作用域。
class CoffeeFilter {
@Inject CoffeeFilter() {}
}
惰性的注入
有时需要延迟实例化对象。对于任何绑定T,都可以创建一个Lazy
,它将实例化延迟到第一次调用Lazy
class GrindingCoffeeMaker {
@Inject Lazy lazyGrinder;
public void brew() {
while (needsGrinding()) {
//Grinder在第一次调用.get()时创建了一次并缓存了它。
lazyGrinder.get().grind();
}
}
}
提供者注入
有时您需要返回多个实例,而不是只注入一个值。当您有几个选项(工厂、构建器等)时,一个选项是注入Provider
而不仅仅是T。Provider
class BigCoffeeMaker {
@Inject Provider filterProvider;
public void brew(int numberOfPots) {
...
for (int p = 0; p < numberOfPots; p++) {
maker.addFilter(filterProvider.get()); //每次换一个新filter。
maker.addCoffee(...);
maker.percolate();
...
}
}
}
注意:注入ProviderT可能会创建令人困惑的代码,并且可能有架构中作用域错误或结构错误的设计味道。通常,您会希望使用工厂或LazyT,或重新组织代码的生存期和结构,以便能够注入T.注入ProviderT在某些情况下是救命稻草。一种常见的用法是,当你必须使用与你的对象的自然生存周期不一致的遗留架构时例如,servlet在设计上是单例的,但只在特定请求数据的上下文中有效。
限定符
有时,单独的类型不足以识别依赖项。例如,一个复杂的咖啡机应用程序可能需要水和热板分开的加热器。
在本例中,我们添加了一个限定符注释。这是任何本身具有@Qualifier
注释的注释。下面是@Named
的声明,它是javax.inject中包含的一个限定符注释:
@Qualifier
@Documented
@Retention(RUNTIME)
public @interface Named {
String value() default "";
}
您可以创建自己的限定符注释,或者只使用@Named。通过注释感兴趣的字段或参数来应用限定符。类型和限定符注释都将用于标识依赖项。
class ExpensiveCoffeeMaker {
@Inject @Named("water") Heater waterHeater;
@Inject @Named("hot plate") Heater hotPlateHeater;
...
}
您可以创建自己的限定符注释,或者只使用@Named。通过注释对应的@Provide方法来提供限定值,从而应用限定符。
@Provides @Named("hot plate") static Heater provideHotPlateHeater() {
return new ElectricHeater(70);
}
@Provides @Named("water") static Heater provideWaterHeater() {
return new ElectricHeater(93);
}
依赖项可能没有多个限定符注释。
可选的绑定
如果你想要绑定可以工作,即使某些依赖没有绑定到组件中,你可以添加一个@BindsOptionalOf
方法到一个模块:
@BindsOptionalOf abstract CoffeeCozy optionalCozy();
这意味着@Inject构造函数和成员以及@Provide方法可以依赖于一个Optional
具体来说,您可以注入以下任何一种:
- Optional
(除非有@Nullable绑定的CoffeeCozy;见下文) - Optional
> - Optional
> - Optional
> >
(您还可以注入上述条目的Provider或Lazy或Lazy of Provider,但这不是很有用。)
如果CoffeeCozy有一个绑定,并且该绑定是@Nullable,那么注入Optional
如果子组件包含基础类型的绑定,则可以在子组件中提供一个组件中没有的可选绑定。
你可以使用Guava’s Optional
或 Java 8’s Optional
.
绑定实例
通常在构建组件时,您有可用的数据。例如,假设您有一个使用命令行参数的应用程序;您可能想要将这些参数绑定到组件中。
也许您的应用程序接受一个表示用户名的参数,您希望以@UserName字符串的形式注入该用户名。您可以向组件构建器添加一个带注释的方法@BindsInstance
,以允许将该实例注入到组件中。
@Component(modules = AppModule.class)
interface AppComponent {
App app();
@Component.Builder
interface Builder {
@BindsInstance Builder userName(@UserName String userName);
AppComponent build();
}
}
接下来你的应用程序看起来可能是这样的:
public static void main(String[] args) {
if (args.length > 1) { exit(1); }
App app = DaggerAppComponent
.builder()
.userName(args[0])
.build()
.app();
app.run();
}
在上面的示例中,在组件中注入@UserName字符串将使用调用该方法时提供给构建器的实例。在构建组件之前,必须调用所有@BindsInstance方法,传递一个非空值(下面的@Nullable绑定除外)。
如果@BindsInstance方法的参数标记为@Nullable,那么绑定将被认为是“可空”的,就像@Provide方法可空一样:注入位置也必须将其标记为@Nullable,并且null是绑定的可接受值。此外,构建器的用户可能会省略对方法的调用,而组件将把实例视为null。
应该优先使用@BindsInstance方法编写带有构造函数参数的@Module并立即提供这些值。
编译时验证
Dagger注释处理程序是严格的,如果任何绑定无效或不完整,则会导致编译器错误。例如,此模块安装在一个组件中,该组件缺少Executor的绑定:
@Module
class DripCoffeeModule {
@Provides static Heater provideHeater(Executor executor) {
return new CpuHeater(executor);
}
}
编译时,javac会拒绝丢失的绑定:
[ERROR] COMPILATION ERROR :
[ERROR] error: java.util.concurrent.Executor cannot be provided without an @Provides-annotated method.
通过向组件中的任何模块中添加带有@Provides注释的Executor方法来解决这个问题。虽然@Inject、@Module和@Provide注释是分别验证的,但绑定之间的所有关系验证都是在@Component级别进行的。Dagger 1严格依赖于@Module级别的验证(它可能反映了也可能没有反映运行时行为),但是Dagger 2省略了这样的验证(以及@Module上伴随的配置参数),支持全图验证。
编译时代码生成
Dagger的注释处理器还可以生成名为CoffeeMaker_Factory.java或CoffeeMaker_MembersInjector.java的源文件。这些文件是Dagger实现细节。您不需要直接使用它们,尽管它们在通过注入逐步调试时非常方便。您应该在代码中引用的唯一生成的类型是用于组件的带有Dagger前缀的类型。
在构建中使用Dagger
您需要在应用程序的运行时中包含dagger-2.X.jar。为了激活代码生成,您需要在编译时在构建中包含dagger-compiler-2.X.jar。有关更多信息,请参阅README。
License
Copyright 2012 The Dagger Authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
以上即为官方文档的翻译,确实有点难于理解。后面我会弄个以kotlin为蓝本的安卓demo程序来进行阐述。