美文网首页
Android Dagger 2 轻松学

Android Dagger 2 轻松学

作者: as_pixar | 来源:发表于2020-01-01 17:40 被阅读0次

    对于 Java 注解不熟悉

    这一部分的开发者基础知识确实薄弱,那怎么办呢?当然是学习了。就算不为 Dagger2,注解的知识内容也应该好好值得学习,虽然在平常开发中,我们自己编写注解的机会很少,但是我们运用第三方开源库的时候,应该会经常看见注解的身影,所以熟悉注解不是为了自己编写注解代码,而是为了开发过程中更加高效从容而已。

    如果,对 Java 注解一无所知,我可以给大家一个感性的认知。

    一般,我们评价某人会说,这是一个好人、坏人、男神、女神、大神、贼、好色、咸猪手、单身狗等等,这是我们人为贴得标签,这些标签有助于我们自己或者其他人去获取被评价的人的基本信息。

    而在 Java 软件开发中,我们也可以给某些类,某些字段贴上作用类似的标签,这种标签的名字就叫做注解,只不过这种标签是给代码看的。

    标签只对特定的人起作用,比如小张被人贴了一个小气鬼的标签,所以小红认为小张是一个小气鬼,但是小张本人不会因为这个标签而改变自己变得不是小张,也许本质上小张是个大方的人。

    所以,注解本身也不会影响代码本身的运行,它只会针对特定的代码起到一定的用处,用来处理注解的代码被称作 APT(Annotation Processing Tool)。

    更详细的内容请阅读这篇文章 秒懂 Java 注解 (Annotation)

    对依赖注入手段不熟悉

    这一块而言,如果让很多人慌张的原因,我觉得可能是依赖注入这个词过于学术化了。而从小到大,10 多年的应试教育让绝大部分的同学对于这些枯燥无味的概念产生了畏惧。其实,没有那么夸张的,不要被这些东西吓倒。

    因为,Java 学习的时候,我们一直写这样的代码。

    class B{}
    
    
    class A {
        B b;
    
        public A() {
            b = new B();
        }
    }
    

    这样的代码,一点问题都没有,类 A 中有一个成员变量 b,b 的类型是类 B。所以,在软件开发中,可以称A 依赖 B,B 是 A 的依赖,显然,A 可以依赖很多东西,B 也可以依赖很多东西。

    通俗地讲,依赖这个概念也没有什么神奇的,只是描述了一种需求关系。

    我们再来看一种情况,现在,业务需要,代码越来越复杂。

    class B{}
    
    class C{
        int d;
        public C (int value) {
            this.d = value;
        }
    }
    class A {
        B b;
        C c;
    
        public A() {
            b = new B();
            c = new C(3);
        }
    }
    

    现在,A 有了一个新的依赖 C。不过,由于业务的演进,C 这个类经常发生变化,最明显的变化就是它的构造方法经常变动。

    class C{
        int d;
        String e;
        public C (String value) {
            this.e = value;
        }
    }
    
    
    class A {
        B b;
        C c;
    
        public A() {
            b = new B();
            //c = new C(3);
            c = new C("hello");
        }
    }
    

    C 变动的时候,由于 A 依赖于它,A 不得不修改自己的代码。但是,事情还没有完。C 还会变动,C 把 B 也带坏了节奏。

    class B{
        int value;
    
        public B(int value) {
            this.value = value;
        }
    
    }
    
    class C{
        int d;
        String e;
        public C (int index,String value) {
            this.d = index;
            this.e = value;
        }
    }
    
    
    class A {
        B b;
        C c;
    
        public A() {
            b = new B(110);
    //      b = new B();
            //c = new C(3);
    //      c = new C("hello");
            c = new C(12,"hello");
        }
    }
    

    可以想像的是,只要 B 或者 C 变动一次,A 就可能需要修改自己的代码,用专业术语描绘就是A 与依赖模块太过于耦合,这个可是犯了软件设计的大罪,
    我们再可以想像一下,A 是领导,B 和 C 是小兵,如果因为 B 和 C 自身的原因,导致领导 A 一次次地改变自己,那么以现在流行的话来说就是,“你良心不会痛吗?”。所以我们需要的就是进行一些变化来进行解耦,也就是解除这种耦合的关系。让 A 不再关心 B 和 C 的变化,而只要关心自身就好了。

    class A {
        B b;
        C c;
    
        public A(B b, C c) {
            this.b = b;
            this.c = c;
        }
    
    }  
    

    在上面代码中,A 不再直接创建 B 与 C,它把依赖的实例的权力移交到了外部,所以无论 B 和 C 怎么变化,都不再影响 A 了。这种实例化依赖的权力移交模式被称为控制反转(IoC),而这种通过将依赖从构造方法中传入的手段就是被传的神乎其乎的依赖注入(DI)。其实,本质上也没有什么神奇的地方,只是起了一个高大上的名字而已,好比东北的马丽,在国际化上的大舞台,宣称自己是来自神秘东方的 Marry 一样。

    依赖注入有 3 种表现形式。
    构造方法注入

    class A {
        B b;
        C c;
    
        public A(B b, C c) {
            this.b = b;
            this.c = c;
        }
    
    } 
    

    Setter 注入

    class A {
        B b;
        C c;
    
    
        public void setB(B b) {
            this.b = b;
        }
    
    
        public void setC(C c) {
            this.c = c;
        }
    
    }
    

    接口注入

    interface Setter {
        void setB(B b);
        void setC(C c);
    }
    
    
    class A implements Setter{
        B b;
        C c;
    
        public void setB(B b) {
            this.b = b;
        }
    
    
        public void setC(C c) {
            this.c = c;
        }
    
    }
    

    大家肯定会想,依赖注入的引进,使得需求方不需要实例化依赖,但总得有地方去实例化这些依赖啊。确实,依赖注入引进了第三方,你可以称它为 IoC 容器,也可以称它为注射器(injector),为了便于理解,我们之后都有注射器来指代吧,通过注射器可以将依赖以上面 3 种注入方式之一注入到需求方。

    病人需要的是药水,所以病人是需求者,药水是病人的依赖,注射器把药水注射给病人。

    而 Dagger2 就是一个依赖注入框架,你也可以想像它是一位非常智能化的服务员,用来处理大量的顾客的各种订餐需求,然后针对不同的菜单提供给不同的顾客不同类型的餐具。

    对于 Java 反射不熟悉

    对于这一块不熟悉的同学同样是基础知识太薄弱,需要补强。

    相对于正常流程开发,Java 反射是非常规化手段。如果正常流程开发是司机驾驶一辆汽车,那么反射的运用就是采用无人驾驶的手段。

    Dagger2 中也应用了反射,不过开发者本身不需要运用反射,Dagger2 是自身框架通过反射处理注解。

    学习反射内容可以阅读这篇文章

    Dagger2 与其它开源库略有不同

    开源软件的出现,大大造福了程序员,所以,大家都说不要重复创造轮子。

    但是,我个人一直认为,不重复创造轮子,不代表可以不去深入了解这些轮子。

    我把 Android 开发中所应用到的开源库当作武装。

    我把 Android 开发中所应用到的开源库当作武装。

    武装两部分构成,武器和装备。

    那么,在 Android 中什么样的库可以当作是武器呢?什么样的库可以当作是装备呢?

    那么,在 Android 中什么样的库可以当作是武器呢?什么样的库可以当作是装备呢?

    装备呢?战斗开始时,就要穿上或者安装好的物件。

    刀、枪、棍、棒是武器,盔甲是装备。
    武器拿来就用,盔甲等却要在开始战斗前就装备上。

    Java 软件代码是在虚拟机中运行的,所以在这里可以把 jvm 当作战场。

    Piccso、Glide、Logger、sweet-alert-dialog 等等,这些开源库都是在程序运行过程中拿来就用的。

    而 GreenDao、Butterknife、Dagger2 这些因为涉及到了反射处理,而反射处理相对于正常开发速度很慢,所以它们通常在编译时产生一些新的代码,然后才能在程序运行过程中使用,也就是说它们都把反射处理移动到编译器编译代码时的阶段,而程序运行时并不涉及到反射,这就是这些框架运用了反射技术,但是仍然高效的秘诀所在。

    所以,Dagger2 会产生中间代码,不少同学应该会有迷惑,为什么引进了 Dagger2 时,要先编译一次代码,不然就会报错。现在,可以解释了,编译代码是为了生成中间代码,然后在中间代码的基础上按照正常的流程开发。

    Dagger2 并非横空出世

    都说要站在巨人的肩膀上,Dagger2 其实也算站在巨人的肩膀上。

    Dagger2 是一款依赖注入的框架,但依赖注入的框架有 ,所以 Dagger2 也并不算是一款新鲜事物,大家觉得新奇不过是因为对于依赖注入框架本身了解过少罢了。

    Dagger2 是在 Dagger 的基础上来的,Dagger 是由 Square 公司开发的,Dagger2 基于 Dagger 由 Google 公司开发并维护。

    Square 是一家伟大的公司,Android 大神 JakeWoton 之前就在它任职,不久前才离职。而我们熟悉的 RxJava、Butterknife、Retrofit、OKHttp 等等都是 Square 提供的,外号 Square 全家桶。

    有个重要的地方就是 Dagger2 是基于注解开发的,而 Dagger2 中所涉及到的注解其实是基于 javax.inject 上开发的,它出自 JSR330

    JSR330 是规范,建议大家怎么做,而 Dagger2 则实现了这个规范。

    因此,对于普通开发者而言,学习 Dagger2 其实就是学习相关的注解的意义与用途。

    Dagger2 的引进

    Dagger2 是适应于 Java 和 Android 开发的依赖注入框架,记住得是它不仅仅对 Android 开发有效。

    Dagger2 官网地址是 https://google.github.io/dagger//

    对于 Eclipse 开发而言,需要下载相应的 jar 包。

    对于 AndroidStudio 开发而言,只需要在相应的 build.gradle 引入对应的依赖就好了。

    如果你 AndroidStudio 的 gradle build tool 版本在 2.2 以上,直接在引进就好了

    dependencies {
      implementation 'com.google.dagger:dagger:2.25.4'
     annotationProcessor 'com.google.dagger:dagger-compiler:2.25.4'
    }
    

    Dagger2 的基本概念

    前面讲到过 Dagger2 基于 JSR330 注解,在普通开发者视角中,就是这些注解构成了 Dagger2 的全部。

    前面文章我提到过,注解如同标签,给一个人贴标签有助于自己去理解这个人,而给代码贴标签,有助于 APT 程序去处理相应的代码,Dagger2 有自己的注解,而这些注解也有特定的意义,它们大体上都是为了实现依赖注入。

    我们说依赖注入有 3 种手段:

    • 构造方法注入
    • Setter 注入
    • 接口注入

    但是,如果不借助于框架的话,我们就必须自己编写相应的代码,这些代码充当了注射器的角色。

    B b = new B(5);
    C c = new C(110,"110");
    
    A a = new A();
    a.setB(b);
    a.setC(c);
    

    A 将内部的依赖 B 和 C 的实例化的权力移交到了外部,通过外部的注入。

    Dagger2 这类依赖注入框架的出现进一步解放了我们的双手,Dagger2 有一套自己的依赖注入机制,我们不再手动编写注射器,而只要按照规则配置好相应的代码就好了,Dagger2 会自动帮我们生成注射器,然后在适当的时候进行依赖注入。

    什么意思呢?意思就是我们不需要调用 a.setB() 和 a.setC() 方法,只需对代码添加一些注解就好了。

    class B{
        int value;
        @Inject
        public B(int value) {
            this.value = value;
        }
    
    }
    
    class C{
        int d;
        String e;
        @Inject
        public C (int index,String value) {
            this.d = index;
            this.e = value;
        }
    }
    
    class A {
        @Inject
        B b;
        @Inject
        C c;
    }
    

    看起来,不可思议,不是吗?@Inject 是一个注解,只要按照 Dagger2 的配置,就能颠覆我们之前的编码习惯。

    但不管看起来怎么神奇,任何事都有一个本质。

    因此这个本质就是,Dagger2 是一个依赖注入框架,依赖注入的目的就是为了给需求方在合适的时候注入依赖。

    Dagger2 的使命就是为了给需求者注射依赖。

    @Inject 注解就如同一个标签,或者说它是一个记号,它是给 Dagger2 看的。它运用的地方有两处。

    • @Inject 给一个类的相应的属性做标记时,说明了它是一个依赖需求方,需要一些依赖。
    • @Inject 给一个类的构造方法进行注解时,表明了它能提供依赖的能力。

    就这样,通过 @Inject 注解符号,就很容易标记依赖和它的需求方。但是,单单一个 @Inject 是不能让 Dagger2 正常运行的。还需要另外一个注解配合。这个注解就是 @Component。

    而 @Component 相当于联系纽带,将 @inject 标记的需求方和依赖绑定起来,并建立了联系,而 Dagger2 在编译代码时会依靠这种关系来进行对应的依赖注入。

    @Inject 和 @Component

    我们来编写代码,验证一下。
    假设有这么一个场景:

    一个宅男,他喜欢在家玩游戏,所以饿了的时候,他不想自己煮饭吃,也不愿意下楼去餐厅,他选择了外卖。

    public class ZhaiNan {
    
        @Inject
        Baozi baozi;
    
        @Inject
        Noodle noodle;
    
        @Inject
        public ZhaiNan() {
    
        }
    
        public String eat() {
            StringBuilder sb = new StringBuilder();
            sb.append("我吃的是 ");
            if ( baozi != null ) {
                sb.append(baozi.toString());
            }
    
            if (noodle != null) {
                sb.append("  ");
                sb.append(noodle.toString());
            }
            return sb.toString();
        }
    }
    
    public class Baozi {
    
        @Inject
        public Baozi() {
        }
    
        @Override
        public String toString() {
            return "小笼包";
        }
    }
    
    public class Noodle {
    
        @Inject
        public Noodle() {
        }
    
        @Override
        public String toString() {
            return "面条";
        }
    }
    

    上面代码可以看到,@Inject 注解的身影,需求方是 ZhaiNan 这个类,而 Baozi 和 Noodle 是它的依赖。前面说过,光有 @Inject 的话还不行,需要 @Component 配合。

    @Component 怎么使用呢?
    很简单,它只需要注解在一个接口上就好了。

    @Component()
    public interface Platform {
        ZhaiNan waimai();
    }
    

    Platform 是一个接口,它代表着外卖平台,它内部有一个 waimai() 的方法,返回 ZhaiNan 的类型。

    这个接口特别的地方就是它的方法中的返回类型。如果一个方法返回了一个类型,那么其实也算是一种依赖的提供,我们可以在后续的代码中感受。

    既然是接口,那么它就需要实现类,但是 Dagger2 会自动帮我们生成一个实现类,前提是使用这个类的时候,要先对工程进行编译。前面用装备解释过 Dagger2 这种类型的库,它会在编译阶段产生中间代码,这些中间代码就包括自动实现了被 @Component 注解过的接口实现类。

    所以,我们如果要使用 Dagger2 为了我们自动生成的类时,我们就应该先 Build->Make Project 编译一次代码。生成的代码位置在 app 模块 build 文件夹中,在 AndroidStudio 切换 Project 视角就可以看到。


    这个目录下都是 Dagger2 产生的中间产物,DaggerPlatform 就是 Dagger2 为我们自动实现的 Platform 这个接口的实现类,注意它的名字都是 Dagger+接口名称。

    有了 DaggerPlatform,我们就能够使用 Dagger2 进行代码的依赖注入了。

    public class MainActivity extends AppCompatActivity {
    
        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.activity_main);
        }
    
        public void testInject(View view) {
            ZhaiNan zhaiNan = DaggerPlatform.create().waimai();
            Toast.makeText(this, zhaiNan.eat(), Toast.LENGTH_LONG).show();
        }
    
    }
    

    然后,测试效果是:


    需要注意的地方是,Component 的实现类是由 Dagger2 自动生成的,它的名字前面说了是 Dagger+接口名称。但这是通常情况,因为 @Component 注解的都是顶级类。但还有一种情况是。

    class Foo {
      static class Bar {
        @Component
        interface BazComponent {}
      }
    }
    

    它只是一个内部类的接口,Dagger2 针对这种情况需要把外部的类的名字加下划线的形式拼接起来,所以上例中 Dagger2 生成的 Component 实现类类名是 DaggerFoo_Bar_BazComponent。

    我们并没有在任何地方用 new 关键字亲自创建 ZhaiNan 这个类的实例,但是它确实有效,而且它的内部依赖 Baozi 和 Noodle 都被实例化了,也就是说依赖被正确地注入到了 ZhaiNan 的实例对象当中。

    所以,@Component 和 @Inject 的配合就能够使用 Dagger2 了,但这里面存在一个局限,@Inject 只能标记在我们自己编写的类的构造方法中,如果我们使用第三方的库或者标准库的话,是不是代表我们对于这些就无能为力了呢?

    答案显然是否定的,Dagger2 作为一款优秀的框架必须考虑到开发过程中的方方面面,不然谈何优秀呢?

    Dagger2 为了能够对第三方库中的类进行依赖注入,提供了 @Provides 和 @Module 两个注解。

    @Provides 和 @Module

    Provide 本身的字面意思就是提供,显然在 Dagger2 中它的作用就是提供依赖。

    Module 是模块的意思,Dagger2 中规定,用 @Provides 注解的依赖必须存在一个用 @Module 注解的类中。

    @Module
    public class ShangjiaAModule {
        @Provides
         public Baozi provideBaozi() {
            return new Baozi("豆沙包");
        }
        @Provides
         public Noodle provideNoodle() {
            return new Noodle();
        }
    }
    
    public class Baozi {
    
        String name;
    
        @Inject
        public Baozi() {
            name = "小笼包";
        }
    
        public Baozi(String name) {
            this.name = name;
        }
    
        @Override
        public String toString() {
            return name;
        }
    }
    

    值得注意的地方有

    • @Provides 修饰的方法一般用 provide 作方法名前缀。
    • @Module 修饰的类一般用 Module 作为后缀。

    前面有讲过,@Component 是依赖双方的联系纽带,现在多了一个 @Module 注解,怎么配合使用呢?方法,很简单。只要在 @component 注解后面的括号中取值就是。

    @Component(modules = ShangjiaAModule.class)
    public interface WaimaiPingTai {
        ZhaiNan waimai();
    }
    

    然后编写测试代码

     public void testModule(View view) {
            ZhaiNan waimai = DaggerWaimaiPingTai.create().waimai();
            Toast.makeText(this, waimai.eat(), Toast.LENGTH_LONG).show();
        }
    

    演示效果如下:


    我们再看看 @Provides 用法。

    @Module
    public class ShangjiaAModule {
        @Provides
         public Baozi provideBaozi() {
            return new Baozi("豆沙包");
        }
        @Provides
         public Noodle provideNoodle() {
            return new Noodle();
        }
    }
    

    @Provides 注解的方法中直接用 new 创建了依赖,其实还有另外一种方式。我们先对 Noodle 进行重构,让它作为面条的基类,然后继承Noodle。

    public  class Noodle {
        @Inject
        public Noodle() {
        }
    }
    
    public class Tongyi extends Noodle{
    
        @Inject
        public Tongyi() {
        }
    
        @Override
        public String toString() {
            return "统一方便面";
        }
    }
    

    ZhanNan 这个类不用改变,然后,用另外一种方式编写 @Provides 注解的方法。

    @Module
    public class ShangjiaAModule {
        @Provides
         public Baozi provideBaozi() {
            return new Baozi("豆沙包");
        }
        @Provides
         public Noodle provideNoodle(Tongyi noodle) {
            return noodle;
        }
    }
    

    测试代码也不需要更改,演示效果如下:


    那么,两种方式有什么区别呢?

    @Provides
        public Noodle provideNoodle(Tongyi noodle) {
            return noodle;
        }
    
        @Provides
        public Noodle provideNoodle() {
            return new Noodle();
        }
    

    什么时候用 new 关键字?什么时候直接返回传入进来的参数?
    我们不妨再创建一个类 Kangshifu,同样继承自 Noodle 这个基类。

    public class Kangshifu extends Noodle{
    
        public Kangshifu() {
        }
    
        @Override
        public String toString() {
            return "康师傅方便面";
        }
    }
    

    与 Tongyi 这个类不同的地方是,它并没有用 @Inject 注解构造方法。
    我们再尝试更改 @Provides 注解的相应方法。

    @Module
    public class ShangjiaAModule {
        @Provides
         public Baozi provideBaozi() {
            return new Baozi("豆沙包");
        }
        @Provides
         public Noodle provideNoodle(Kangshifu noodle) {
            return noodle;
        }
    //    @Provides
    //     public Noodle provideNoodle(Tongyi noodle) {
    //        return noodle;
    //    }
    }
    

    再进行编译的时候,会发现 IDE 报错了。

    错误: 
    com.example.dagger2.bean.Kangshifu cannot be provided without an @Inject constructor or an @Provides-annotated method.
    com.example.dagger2.bean.Kangshifu is injected at
    com.example.dagger2.bean.ShangjiaAModule.provideNoodle(noodle)
    com.example.dagger2.bean.Noodle is injected at
    com.example.dagger2.bean.ZhaiNan.noodle
    com.example.dagger2.bean.ZhaiNan is provided at
    com.example.dagger2.bean.WaimaiPingTai.waimai()
    

    Log 提示的错误信息是 Kangshifu 这个类代码中没有被 @Inject 注解过的构造方法,也没有办法从一个被 @Provides 注解过的方法中获取。

    所以,什么时候用 new 创建对象,什么时候可以直接返回传入的参数就很明显了。对于被 @Inject 注解过构造方法或者在一个 Module 中的被 @Provides 注解的方法提供了依赖时,就可以直接返回传入的参数,而第三方的库或者 SDK 自带的类就必须手动创建了。

    @Module
    public class ShangjiaAModule {
        @Provides
         public Baozi provideBaozi() {
            return new Baozi("豆沙包");
        }
    
        @Provides
         public Kangshifu provideKangshifu() {
            return new Kangshifu();
        }
    //    @Provides
    //     public Noodle provideNoodle(Tongyi noodle) {
    //        return noodle;
    //    }
    }
    

    这段代码,是可以正常运行的。

    现在,我有一个新的需求更改。我在 ZhaiNan 类中 eat() 方法中要把餐厅名字打印出来。

    public class ZhaiNan {
    
        @Inject
        Baozi baozi;
    
        @Inject
        Noodle noodle;
    
        @Inject
        public ZhaiNan() {
    
        }
    
        @Inject
        String resturant;
    
        public String eat() {
            StringBuilder sb = new StringBuilder();
            sb.append("我从 ");
            sb.append(resturant);
            sb.append("订的外卖,");
            sb.append("我吃的是 ");
            if ( baozi != null ) {
                sb.append(baozi.toString());
            }
    
            if (noodle != null) {
                sb.append("  ");
                sb.append(noodle.toString());
            }
            return sb.toString();
        }
    }
    

    注意的是,我要新增了一个 String 类型的字段 resturant 来代表餐厅,并且用 @Inject 注解它。
    于是,相应的 Module 也要更改。

    @Module
    public class ShangjiaAModule {
        @Provides
         public Baozi provideBaozi() {
            return new Baozi("豆沙包");
        }
        @Provides
         public Noodle provideNoodle(Kangshifu noodle) {
            return noodle;
        }
    
        @Provides
         public Kangshifu provideKangshifu() {
            return new Kangshifu();
        }
    
        @Provides
         public String provideResturant() {
            return "王小二包子店";
        }
    //    @Provides
    //     public Noodle provideNoodle(Tongyi noodle) {
    //        return noodle;
    //    }
    }
    

    然后,再编译测试。这次的编译报错了。

    错误: [Dagger/MissingBinding] java.lang.String cannot be provided without an @Inject constructor or an @Provides-annotated method.
    java.lang.String is injected at
    com.example.dagger2.bean.ZhaiNan.resturant
    com.example.dagger2.bean.ZhaiNan is provided at
    com.example.dagger2.bean.Platform.waimai()
    

    报错的原因是之前编写的接口 Platform 没有办法获取 ZhanNan 中 resturant 的依赖。因为之前并没有为 Platform 指定 Module。

    那么现在我们就为它指定 ShangjiaAModule 吧。

    编译后,进行测试,程序正常运行。


    现在,我把代码再重构。在 ShangjiaAModule 中提供餐厅名字的时候,直接返回了“王小二包子店”,这个过于直接,缺少变动,现在针对这个进行变化。

    @Module
    public class ShangjiaAModule {
    
        String restaurant;
    
        public ShangjiaAModule(String restaurant) {
            this.restaurant = restaurant;
        }
    
        ......
    
        @Provides
         public String provideResturant() {
            return restaurant;
        }
    
    }
    
    
    ZhaiNan zhaiNan = DaggerWaimaiPingTai.builder().build().waimai();
            Toast.makeText(this, zhaiNan.eat(), Toast.LENGTH_LONG).show();
    

    restaurant 在 ShangjiaAModule 创建的时候被赋值,但我们之前的代码好像并没有处理 ShangjiaAModule 的创建,那么它如何创建呢?
    我们先尝试重新编译代码并运行。

    编译没有出错,但运行的时候出错了。

    Caused by: java.lang.IllegalStateException: com.example.dagger2.bean.ShangjiaAModule must be set
            at dagger.internal.Preconditions.checkBuilderRequirement(Preconditions.java:95)
            at com.example.dagger2.bean.DaggerWaimaiPingTai$Builder.build(DaggerWaimaiPingTai.java:47)
            at com.example.dagger2.MainActivity.testModule(MainActivity.java:28)
    

    Log 提示的是调用 DaggerPlatform中的Builder.build() 方法时出错了,因为 ShangjiaAModule 并没有出错。我们定位代码。

    final ZhaiNan zainan = DaggerPlatform.builder()
                    .build()
                    .waimai();
    

    我之前没有讲的是,如果一个 Module 没有实现任何构造方法,那么在 Component 中 Dagger2 会自动创建,如果这个 Module 实现了有参的构造方法,那么它需要在 Component 构建的时候手动传递进去。怎么传呢?Component 中生成的 Builder 构造器有与 Module 名字相同的方法,并且参数类型就是 Module 类型。大家细细体会下面代码就明白了。

        public void testInject(View view) {
            ZhaiNan zainan = DaggerPlatform.builder().
                    shangjiaAModule(new ShangjiaAModule("小王店铺"))
                    .build()
                    .waimai();
            Toast.makeText(this, zainan.eat(), Toast.LENGTH_LONG).show();
        }
    
        public void testModule(View view) {
            ZhaiNan zhaiNan = DaggerWaimaiPingTai.builder()
                    .shangjiaAModule(new ShangjiaAModule("小王店铺"))
                    .build()
                    .waimai();
            Toast.makeText(this, zhaiNan.eat(), Toast.LENGTH_LONG).show();
        }
    

    再编译运行。


    另外,还有一种特殊情况就是,像在 Android 中,MainActivity 这样的代码是我们自己编写的,所以我们可以给相应的属性添加 @Inject 注解,但是 MainActivity 对象的创建却是由 Android Framework 框架决定的,那么,Dagger2 有没有针对这种内部拥有 @Inject 标注的属性,答案是肯定的。

    我们知道,Component 是一个接口,方法的返回值里面可以定义很多方法。提供一种类型的对象,前提是这个类的对象被 @Inject 注解过构造方法或者在 Module 中被 @Provides 注解过的方法提供。

    @Component(modules = ShangjiaAModule.class)
    public interface WaimaiPingTai {
        ZhaiNan waimai();
    }
    

    ZhaiNan 能够在一个 Component 中的方法中作为类型返回是因为它符合我上面说的条件,它的构造方法被 @Inject 注解过。

    需要注意的是,Component 中的方法除了可以返回类型,还可以在方法中传入类型参数。目的是针对这个参数对象进行依赖注入。

    @Component(modules = ShangjiaAModule.class)
    public interface WaimaiPingTai {
        ZhaiNan waimai();
    
        void zhuru(ZhaiNan zhaiNan);
    }
    

    我新增了一个方法,zhuru() 中的参数就是 ZhaiNan 类型,代表 DaggerWaimaiPingTai 调用这个方法时能够对一个 ZhaiNan 对象进行依赖注入。

    可以编写代码验证。

    public void testZhuRu(View view){
            final ZhaiNan zhaiNan = new ZhaiNan();
            WaimaiPingTai waimaiPingTai = DaggerWaimaiPingTai.builder()
                    .shangjiaAModule(new ShangjiaAModule("开封市胡辣汤"))
                    .build();
            // 通过调用接口中的方法给 zhaiNan 进行依赖注入
            waimaiPingTai.zhuru(zhaiNan);
            Toast.makeText(this, zhaiNan.eat(), Toast.LENGTH_LONG).show();
        }
    

    运行结果如下:


    所以,我们可以给接口方法参数传值的形式来给 Activity 进行依赖注入。

    @Module
    public class ActivityModule {
    
        @Provides
        public int provideActivityTest(){
            return 1234567890;
        }
    }
    
    
    @Component(modules = {ShangjiaAModule.class,ActivityModule.class})
    public interface WaimaiPingTai {
        ZhaiNan waimai();
    
        void zhuru(ZhaiNan zhaiNan);
    
        void inject(MainActivity mainActivity);
    }  
    

    我们编写了新的 Module,然后把它放时 WaimaiPingTai 这个 Component 中去,再添加了 inject() 方法,为的是能够给 MainActivity 实例进行依赖注入。

    现在我们添加测试代码,首先在 MainActivity 中添加一个 int 类型的成员变量。

    @Inject
    int testvalue;
    

    然后要调用相关注入方法

    public void testInjectActivity(View view){
            WaimaiPingTai waimaiPingTai = DaggerWaimaiPingTai.builder()
                    .shangjiaAModule(new ShangjiaAModule("中原一大碗"))
                    .build();
            waimaiPingTai.inject(this);
            Toast.makeText(this, "testValue is " + testValue, Toast.LENGTH_LONG).show();
        }
    

    运行结果:


    gifhome_640x1392_4s.gif

    Component 的创建方式

    我们可以看到,创建 Component 都是通过它的 Builder 这个类来进行构建的。其实还有另外一种方式。那就是直接调用 Component 实现类的 create() 方法。

    public class Test {}
    
    @Component(modules = TestCreate.class)
    public interface TestCreateComponent {
        Test ceshi();
    }
    
    TestCreateComponent testCreateComponent = DaggerTestCreateComponent.create();
    Test test = testCreateComponent.ceshi();
    

    上面代码中创建 TestCreateComponent 并没有借助于 Builder,而是直接调用了 DaggerTestCreateComponent 的 create() 方法,但是它有一个前提,这个前提就是 Component 中的 module 中被 @Provides 注解的方法都必须是静态方法,也就是它们必须都被 static 修饰。

    @Module
    public class TestCreate {
    
        @Provides
        public static int provideTest1() {
            return 1;
        }
    
        @Provides
        public static String provideTest2() {
            return "test component create()";
        }
    
        @Provides
        public static Test provideTest(){
            return new Test();
        }
    }
    

    因为不需要创建 Module 对象实例,所以 Builder 自然就可以省去了。

    @Inject 和 @Provides 的优先级

    可能有心思细腻的同学会问,同样是提供依赖,如果一个类被 @Inject 注解了构造方法,又在某个 Module 中的 @Provides 注解的方法中提供了依赖,那么最终 Dagger2 采用的是哪一个?

    public class Baozi {
    
        String name;
    
        @Inject
        public Baozi() {
            name = "小笼包";
        }
    
        public Baozi(String name) {
            this.name = name;
        }
    
        @Override
        public String toString() {
            return name;
        }
    }
    
    @Module
    public class ShangjiaAModule {
    
        String restaurant;
    
        public ShangjiaAModule(String restaurant) {
            this.restaurant = restaurant;
        }
    
        @Provides
         public Baozi provideBaozi() {
            return new Baozi("豆沙包");
        }
    
    }
    

    Baozi 这个类就符合我上面给的情景,一方面它确实拥有被 @Inject 注解过的构造方法,另一方面在 Module 中它又通过 @Provides 提供了依赖。那么,最终,Dagger2 采取了哪一种呢?

    答案是 Module,其实现象我们在之前的测试时已经可以观察到了,最终屏幕显示的是豆沙包选项。

    Dagger2 依赖查找的顺序是先查找 Module 内所有的 @Provides 提供的依赖,如果查找不到再去查找 @Inject 提供的依赖。

    到这里,我们讲解了 Dagger2 中最常见的 4 个注解:@Inject、@Component、@Module、@Provides。

    正常情况下,这 4 个注解能够很好的完成一般的代码开发了。但是,这都是基础功能,Dagger2 提供了更多的一些特性。

    Dagger2 中的单例 @Singleton

    我们在平常开发中经常要涉及到各种单例。比如在 Android 中开发,数据库访问最好要设计一个单例,网络访问控制最好设计一个单例。我们经常编写这样的代码。

    public class DBManager {
    
        private static DBManager instance;
    
        private DBManager() {
        }
    
        public static DBManager getInstance() {
            if ( instance == null ) {
                synchronized ( DBManager.class ) {
                    if ( instance == null ) {
                        instance = new DBManager();
                    }
                }
            }
    
            return instance;
        }
    }
    

    这种代码手段千遍一律,而 Dagger2 提供了另外一种可能。那就是利用 @Singleton 注解解决它。@Singleton 怎么使用呢?我们用代码来说明。

    @Singleton
    public class TestSingleton {
    
        @Inject
        public TestSingleton() {
        }
    }
    
    @Singleton
    @Component
    public interface ActivityComponent {
        void inject(SecondActivity activity);
    }
    

    用 @Singleton 标注在目标单例上,然后用 @Singleton 标注在 Component 对象上。

    编写测试代码

    public class SecondActivity extends AppCompatActivity {
    
        @Inject
        TestSingleton testSingleton1;
    
        @Inject
        TestSingleton testSingleton2;
    
        @Override
        public void onCreate(@Nullable Bundle savedInstanceState, @Nullable PersistableBundle persistentState) {
            super.onCreate(savedInstanceState, persistentState);
            setContentView(R.layout.activity_main);
    
            DaggerActivityComponent.builder()
                    .build()
                    .inject(this);
        }
    
        public void testSingle(View view){
            Toast.makeText(SecondActivity.this,"test1 hashcode:"+testSingleton1.toString()
                    +" test2 hashcode:"+testSingleton2.toString(),Toast.LENGTH_LONG).show();
        }
    }
    

    编译后,测试结果往下:


    可以看到,两个对象的 hashcode 是一样的,说明 TestSingleton 这个类实现了单例。
    另外,如果要以 @Provides 方式提供单例的话,需要用 @Singleton 注解依赖提供的方法。如

    @Module
    public class SecondActivityModule {
    
        @Provides
        @Singleton
        public TestSingleton provideTestSingleton(){
            return new TestSingleton();
        }
    }
    

    @Singleton 引出 @Scope

    我们在上一节的内容可以看到,通过 @Singleton 注解就可以实现一个单例了。本节的目标就是深入分析一下 @Singleton。

    @Scope
    @Documented
    @Retention(RUNTIME)
    public @interface Singleton {}
    

    @Singleton 是一个注解,但是它被一个元注解 @Scope 注解了,所以,可以猜测到的是 @Scope 是真正厉害的角色。而实际上 @Singleton 只是 @Scope 一个默认的实现而已,但是因为它更具可读性,能够让开发者一眼就明白它的作用是为了单例。但是,单例也是有范围限制的。

    分析 @Singleton 其实就等同于分析 @Scope 。Scope 的字面意思是作用域,也就是表达一种能力的范围。那么在 Dagger2 中它表达了一种什么样的能力范围呢?

    大家有没有想过,为什么要用 @Singleton 同时标注 @Provides 和 @Component ?

    文章一开始就讲过,Component 是联系需求与依赖的纽带,所以用 @Singleton 确定的单例作用域应该也是在 Component 的范围内。也就是说 @Scope 的作用范围其实就是单例能力范围,这个范围在单个的 Component 中。

    在上面的代码中,MainActivity 和 SecondActivity 运用了不同的 Component 现在我们可以测试一下它们所获取的 TestSingleton 会不会是同一个对象。

    public class MainActivity extends AppCompatActivity {
    
        @Inject
        public  TestSingleton testSingleton;
    
        public void goSecondActivity(View view){
            WaimaiPingTai waimaiPingTai = DaggerWaimaiPingTai.builder()
                    .shangjiaAModule(new ShangjiaAModule("中原一大碗"))
                    .build();
            waimaiPingTai.inject(this);
    
            Intent intent=new Intent(this,SecondActivity.class);
            startActivity(intent);
            Toast.makeText(MainActivity.this,"testsingleton is "+ testSingleton,Toast.LENGTH_LONG).show();
        }
    
    @Module
    public class ActivityModule {
    
        @Provides
        public int provideActivityTest(){
            return 1234567890;
        }
    
        @Provides
        @Singleton
        public TestSingleton provideSingleton(){
            return new TestSingleton();
        }
    }
    
    @Singleton
    @Component(modules = {ShangjiaAModule.class,ActivityModule.class})
    public interface WaimaiPingTai {
        ZhaiNan waimai();
    
        void zhuru(ZhaiNan zhaiNan);
    
        void inject(MainActivity mainActivity);
    }
    

    我们改写了 MainActivity 和它涉及到的 Module、Component。
    现在,我们检测 MainActivity 和 SecondActivity 中的 TestSingleton 是不是同一个。

    @Singleton 起作用是因为它被 @Scope 注解,所以,如果可能,我们也可以自己定义 Scope。

    我自己定义一个 @PageScope 注解,我的想法是一个 Activity 有不同的 Fragment,所以以 @PageScope 标注的依赖对象在这些 Fragment 之间是同一个对象,也就是说在这个 Activity 中实现了单例。而在另外一个 Activity 中因为采取了不同的 Component 对象,所以它们的 Fragment 也共用了同一个依赖对象,但是两个 Activity 中各自的依赖确不是同一个对象。

    大家细细体会。

    @Qualifiers 和 @Name

    Qualifiers 是修饰符的意思,那么它修饰的是什么呢?不知道大家有没有察觉到,前面的演示代码其实很简单,经不起太多推敲。

    在一个 Module 中 @Provides 提供的依赖是由返回值决定的。这样就会出现问题,同一种类型不同实例,怎么去区别?比如

    public class SecondActivity extends AppCompatActivity {
    
    
        @Inject
        String phone;
    
        @Inject
        String computer;
    
    }
    

    phone 和 computer 应该要对应不同的字符串。但是,我们该如何在 Module 中进行编码呢?

    @Module
    public class SecondActivityModule {
    
        @Provides
        @Singleton
        public TestSingleton provideTestSingleton(){
            return new TestSingleton();
        }
    
        @Provides
        public String providePhone() {
            return "手机";
        }
    
        @Provides
        public String providePhone() {
            return "电脑";
        }
    
    }
    

    大家可能会想到这样编码,但是这样的代码根本编译不过。因为 Dagger2 是根据返回的类型来进行依赖关系确定的。如果存在两个方法返回一样的类型,那么正常情况下 Dagger2 显然就没有办法处理了。

    不过,Dagger2 给出了解决方案。用 @Name 注解就好了,配合 @Inject 和 @Provides 一起使用。例如

     @Inject
    @Named("phone")
    String phone;
    
    @Inject
    @Named("computer")
    String computer;
    
    @Module
    public class SecondActivityModule {
    
        @Provides
        @Singleton
        public TestSingleton provideTestSingleton(){
            return new TestSingleton();
        }
    
        @Provides
        @Named("phone")
        public String providePhone() {
            return "手机";
        }
    
        @Provides
        @Named("computer")
        public String provideComputer() {
            return "电脑";
        }
    
    }
    

    当然,如果你嫌每次给 @Name 麻烦,你可以自定义注解。

    @Qualifier
    @Documented
    @Retention(RUNTIME)
    public @interface Named {
    
        /** The name. */
        String value() default "";
    }
    

    @Name 只是被 @Qualifier 注解的一个注解。所以,它能够有效完全是因为 @Qualifier。

    @Qualifier
    @Documented
    @Retention(RUNTIME)
    public @interface Phone {
    }
    
    
    @Qualifier
    @Documented
    @Retention(RUNTIME)
    public @interface Computer {
    }
    

    通过 @Qualifier 建立了 @Phone 和 @Computer 注解。

    @Qualifier
    @Documented
    @Retention(RUNTIME)
    public @interface Phone {
    }
    
    
    @Qualifier
    @Documented
    @Retention(RUNTIME)
    public @interface Computer {
    }
    

    通过 @Qualifier 建立了 @Phone 和 @Computer 注解。

    @Inject
    @Phone
    String phone;
    
    @Inject
    @Computer
    String computer;
    
    @Module
    public class SecondActivityModule {
    
        @Provides
        @Singleton
        public TestSingleton provideTestSingleton(){
            return new TestSingleton();
        }
    
        @Provides
        @Phone
        public String providePhone() {
            return "手机";
        }
    
        @Provides
        @Computer
        public String provideComputer() {
            return "电脑";
        }
    
    }
    

    这样的效果是一样的。但是好处在于 @Name() 中要传入字符串,一不小心就容易将单词拼错,容易出错。

    Dagger2 中的延迟加载

    有些时候,我们希望依赖只有在我们使用的时候再去实例化,这样的机制叫做延迟加载。

    public class TestLazy {
    
        String name;
    
        public String getName() {
            if ( name == null ) {
                name = "TestLazy";
            }
    
            return name;
        }
    }
    

    只有调用 TestLazy 实例的 getName() 方法时,name 才会被初始化。

    Dagger2 提供了延迟加载能力。只需要通过 Lazy 就好了,Lazy 是泛型类,接受任何类型的参数。

    public class TestLazy {
    
        @Inject
        @Named("TestLazy")
        Lazy<String> name;
    
        public String getName() {
            return name.get();
        }
    
    }
    
    @Module
    public class SecondActivityModule {
    
        @Provides
        @Singleton
        public TestSingleton provideTestSingleton(){
            return new TestSingleton();
        }
    
        @Provides
        @Phone
        public String providePhone() {
            return "手机";
        }
    
        @Provides
        @Computer
        public String provideComputer() {
            return "电脑";
        }
    
        @Provides
        @Named("TestLazy")
        public String provideTestLazy() {
            return "TestLazy";
        }
    
    }
    

    这样,只有第一次调用 TestLazy 的 getName() 方法时,name 都会被注入。

    Provider 强制重新加载

    应用 @Singleton 的时候,我们希望每次都是获取同一个对象,但有的时候,我们希望每次都创建一个新的实例,这种情况显然与 @Singleton 完全相反。Dagger2 通过 Provider 就可以实现。它的使用方法和 Lazy 很类似。

    
    public class TestProvider {
        @Inject
        Provider<Integer> randomValue;
    
        public int getRandomValue () {
            return randomValue.get().intValue();
        }
    }
    
    
    @Module
    public class SecondActivityModule {
    
        ......
    
        @Provides
        public int provideRandomValue(){
            return (int) Math.random();
        }
    
    }
    

    但是,需要注意的是 Provider 所表达的重新加载是说每次重新执行 Module 相应的 @Provides 方法,如果这个方法本身每次返回同一个对象,那么每次调用 get() 的时候,对象也会是同一个。

    对于 Dagger2 的用途与意义心生迷惑

    我想,还是会有一大部分的同学看到这里的时候仍然不明白,Dagger2 的妙处。

    其实在文章开始的地方我就讲了 Dagger2 的本质,它本质就是一款依赖注入框架,用来解耦的。

    掌握上面的 Dagger2 基础知识已经足够让你进行此类代码编写了,另外也足够让你去看懂一些运用了 Dagger2 的优秀开源项目,比如 Google 提供的示例

    todo-mvp-dagger

    这个项目示例就是为了演示 Dagger2 与 MVP 架构的配合使用。由于文章篇幅所限,我不作过多的讲解,大家自行研究。

    如果你对 Dagger2 兴趣更浓烈了

    Dagger2 的知识内容稍多,所以如果你耐着性子学习完后,兴致依赖不减,我可以给你一些建议。

    • 自己去阅读官网文档。因为那才是第一手资料,虽然它写的不是很好,但毕竟权威。
    • 多去观察不同的博文,因为每个人思考方式不一样,所以观察问题的角度可能不一样。
    • 自己多练,任何没有经过自己实践的行为在软件编程中都不可取。
      阅读优秀的开源代码,并思考。
    • 在思考的同时,纠正自己的理解,然后再实践,再思考,再总结。我总说主动学习要好过被动学习,任何没有经过自己思考和求索的学习都是被动学习,看文档、看博文、看代码那都是别人的知识,你需要的就是用自己把这些纳入自己的知识体系中,当你也能讲述给其他人听的时候,那时你就可以确定你掌握它了。

    如果你仍然意识不到 Dagger2 的美好

    这个其实也没有多大关系。不要迷恋武器。

    也许你写的代码中类文件不是很多,模块之间的耦合并不是很强,或者是整个系统并不复杂,强行引进 Dagger2 只会让你感受复杂,多了很多类,多了很多编译的步骤,还增加了学习成本,你会觉得不划算。

    我们总说优化代码,设计架构,其实对于很多开发人员而言,大量的产品需求就能让自己加好几个晚上的班,并且需求还经常变动,如果你这个时候跟他讲代码规范什么的,肯定不现实。现实的事情是,完成需求永远第一要务。

    饱暖才能思淫欲。

    我与其劝你去感受 Dagger2 的美好,还不如劝你细细去体会一下依赖注入的美好。

    如果,你仍然觉得 Dagger2 麻烦,中肯地讲一句:那么索性放弃它算了,Dagger2 不重要,依赖注入才重要。

    等到哪一天,你真的有强烈的对于代码解耦需求,也许你会想起 Dagger2 这么一款框架,那时候回过来学习,我保证你的效果会非常明显,你的理解力也会较现在更加的深刻。

    所以,我最终的目的仍然是希望你能够好好学习 Dagger2,希望大家不要误解我这一节的真实用意。

    相关文章

      网友评论

          本文标题:Android Dagger 2 轻松学

          本文链接:https://www.haomeiwen.com/subject/drmboctx.html