Dagger2 Guidelines

2019/06/27

       这篇文章指导如何优化和高效利用Dagger2。

       本文适合对Dagger2有一定经验,知道Dagger2的基本使用方法的读者。如果你对Dagger2还不熟悉,可以查看官方文档,或者参考我的上一篇文章Android Sunflower + Dagger2结合我的Demo项目sunflower-java一起学习。
       这篇文章主要参考Keeping the Daggers Sharp以及Dagger 2 on Android: The Official Guidelines

Constructor Injection

       优先选择构造器注入(Constructor Injection),而不是字段注入(Field Injection)。(Field不知道如何翻译,域?字段?/(ㄒoㄒ)/~~)

// BAD
class CardConverter {
    @Inject
    PublicKeyManager publicKeyManager;
    public CardConverter() {}
}

// GOOD
class CardConverter {
    private final PublicKeyManager publicKeyManager;
    @Inject
    public CardConverter(PublicKeyManager publicKeyManager) {
        this.publicKeyManager = publicKeyManager;
    }
}

Kotlin消除了构造器注入的构造器模板:

class CardConverter @Inject constructor(
    private val publicKeyManager: PublicKeyManager
)

       当然,对系统构造生成的对象,我们仍需使用字段注入。
例如Activity:

public class MainActivity extends Activity {
    @Inject
    PublicKeyManager publicKeyManager;
}

Scoping

       Scoping is expensive,使用@Scope是会消耗很多资源的。每个@Singleton或者自定义的@Scope都要通过Double check双重检查锁实现。所以除非必要,应该尽量减少使用@Scope。
       当需要集中访问一个具有可变状态的类时,单例很有用。

// GOOD
@Singleton
public class BadgeCounter {
    public final Observable<Integer> badgeCount;
    @Inject
    public BadgeCounter(...) {
        badgeCount = ...
    }  
}

       如果一个类,没有可变状态量,其实就没有必要使用@Singleton。而且Dagger中有个特殊的Scope注解@Reusable,参考官方文档——有时您希望限制实例化@Inject构造的类或调用@Provides方法的次数,但不需要保证在任何特定组件或子组件的生命周期内使用完全相同的实例,可以使用@Reusable。这在Android等环境中很有用,因为分配可能很昂贵。比如,Gson我认为很适合用@Reusable。

@Module
public class AppModule {
    @Reusable
    @Provides
    static Gson provideGson() {
        return new Gson();
    }
}

       但有一种情况,当你初始化一个第三方库,虽然你不需要传入任何状态变量,但如果它内部在维护一个或几个变量,那么也应该使用@Singleton而不是@Reusable。比如Stack Overflow response by Jeff Bowman中讨论的问题,OkHttpClient内部有线程池等变量,所以应该声明为@Singleton
       另一种情况,如果我们创建实例成本高昂,或者要避免重复创建和丢弃实例,则应该使用@Singleton

优先使用@Inject而不是@Provides

       在你可以控制的类中,优先使用Constructor Injection。当你使用@Inject注解Constructor时,当前类会自动加入Dagger的Graph中,而不用再次在Module中用@Provides声明。而且直接在当前类中声明,也更容易让人理解代码的意图。@Provides是给你创建那些控制不了的第三方库的实例而使用的。

// BAD
@Module
class ToastModule {
    @Provides RealToastFactory realToastFactory(Application context) {
        return new RealToastFactory(context);
    }
}

// GOOD
public class RealToastFactory {
    private Application context;
    @Inject
    public RealToastFactory(Application context) {
        this.context = context;
    }  
}

优先使用static @Provides 方法

       Dagger @Provides方法可以是静态的。

@Module
class ToastModule {
    @Provides
    static ToastFactory toastFactory(RealToastFactory factory) {
        return factory;
    }
}

       如果@Provides方法是静态的,生成的代码可以直接调用方法,而不必创建模块实例。该方法调用可以由编译器内联。

public final class DaggerAppComponent extends AppComponent {
  // ...
    @Override
    public ToastFactory toastFactory() {
        return ToastModule.toastFactory(realToastFactoryProvider.get())
    }
}

       一个静态方法不会有太大变化,但如果所有绑定都是静态的,那么性能会有相当大的提升。
       如果你把module设为abstract抽象类,那所有的方法必须是staticabstract。如果不是,编译时会报错。
       如果像上一篇文章提到的,因为你的模块有状态而做不到abstract或者static,你应该重新审视修改它。就行官网提到的:

Warning: it’s discouraged for modules to have state; this can lead to unpredictable behavior. Moreover, module instances in general are rarely useful. We have considered removing them. –Dagger Core Semantics

       在Kotlin中它变得棘手,因为你不能像Java一样在类中使用静态方法。一种解决方案是:把module类设置成object;使用@JvmStatic注解声明provide方法:

@Module
object YourModule {
    @JvmStatic
    @Provides
    fun provideThatDependency() = ThatDependency()
}

优先使用@Binds而不是@Provides

       当你将一种类型映射到另一种类型时,使用@Binds而不是@Provides

@Module
abstract class BookPresenterModule {
    @Binds
    abstract BookPresenter bindBookPresenter(BookPresenterImpl bookPresenter);
}

       该方法必须是抽象的。它不会被调用;生成的代码将知道直接使用该实现。

避免在interface绑定中使用@Singleton

Statefulness is an implementation detail

@Module
abstract class ToastModule {
    // BAD, remove @Singleton
    @Singleton
    @Binds
    abstract ToastFactory toastFactory(RealToastFactory factory);
}

       只有实现才知道他们是否需要确保集中访问可变状态。
       将实现绑定到接口时,不应该有任何作用域注释。

Application context

       设置Dagger的第一步是都是将应用程序上下文设为公开依赖项。对于Android应用来说,就是其Application context。

  1. 最早的实现方式:
     @Singleton
     @Component(modules = {ApplicationModule.class, YourModule.class, ThatOtherModule.class})
     interface ApplicationComponent
    
    
     @Module
     class ApplicationModule {
         private Context applicationContext;
         public ApplicationModule(private Context applicationContext) {
             this.applicationContext = applicationContext;
         }
         @Provides
         Context provideApplicationContext(){
             return applicationContext;
         }
     }
    
     class YourApplication extends Application() {
         DaggerApplicationComponent.builder()
             .applicationModule(ApplicationModule(applicationContext))
             .build();
     }
    

           我们创建一个接收应用程序上下文作为构造函数参数的模块,然后创建一个公开它的提供方法。这很好用,但是就像上文说的,module有了状态,我们不能再使用静态@Provides方法了。

  2. 为了解决这个问题,在Dagger 2.9中,加入了@BindsInstance注解。我们能够在组件(或子组件)构建器中注释方法,以便我们可以轻松地绑定在图形外部构造的实例。
     @Singleton
     @Component(modules = {ApplicationModule.class, YourModule.class, ThatOtherModule.class})
     interface ApplicationComponent{
         @Component.Builder
         interface Builder {
             @BindsInstance
             Builder applicationContext(Context applicationContext);
             ApplicationComponent build();
         }
     }
    
    
     class YourApplication extends Application() {
         DaggerApplicationComponent
             .builder()
             .applicationContext(applicationContext)
             .build();
     }
    

           虽说还是比较繁琐,但它不再需要一个有状态模块。

  3. 之后,在Dagger 2.22引入了@Component.Factory注解,即component factory来代替component builder。工厂只有一个方法,我们可以注释它的参数。
     @Component(modules = ...)
     interface ApplicationComponent {
    
         @Component.Factory
         interface Factory {
             ApplicationComponent create(@BindsInstance Context applicationContext);
         }
         //...
     }
    
     class YourApplication extends Application() {
         DaggerApplicationComponent
             .factory()
             .create(applicationContext);
     }
    

           它不仅简化了编码。而且这种改变带来了编译时的安全性:之前,如果我们有多个builder方法,就有可能忘记调用其中一个并且它仍然可以编译。现在,我们只有一个方法,每当我们调用它时,我们必须提供每个参数,因此不可能再忘记为组件提供强制依赖。

Post Directory