Keeping the Daggers Sharp ⚔️ – Square Corner Blog – Medium


Dagger 2 is a great dependency injection library, but its sharp edges can be tricky to handle. Let’s go over a few best practices that Square follows to keep mobile engineers from hurting themselves!

Favor constructor injection over field injection

  • Field injection requires the fields to be non final and non private.
// BAD
class CardConverter {
  @Inject PublicKeyManager publicKeyManager;
  @Inject public CardConverter() {}
}
  • Forgetting an @Inject on a field introduces a NullPointerException.
// BAD
class CardConverter {
  @Inject PublicKeyManager publicKeyManager;
Analytics analytics; // Oops, forgot to @Inject
  @Inject public CardConverter() {}
}
  • Constructor injection is better because it allows for immutable and therefore thread safe objects that don’t have a partially constructed state.
// GOOD
class CardConverter {
  private final PublicKeyManager publicKeyManager;
  @Inject public CardConverter(PublicKeyManager publicKeyManager) {
this.publicKeyManager = publicKeyManager;
}
}
  • Kotlin eliminates the constructor injection boilerplate:
class CardConverter
@Inject constructor(
private val publicKeyManager: PublicKeyManager
)
  • We still use field injection for objects constructed by the system, such as Android activities:
public class MainActivity extends Activity {
  public interface Component {
void inject(MainActivity activity);
}
  @Inject ToastFactory toastFactory;
  @Override protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
Component component = SquareApplication.component(this);
component.inject(this);
}
}

Singletons should be extremely rare

  • Singletons are useful when we need a centralized access to a mutable state.
// GOOD
@Singleton

public class BadgeCounter {
  public final Observable<Integer> badgeCount;
  @Inject public BadgeCounter(...) {
badgeCount = ...
}
}
  • If an object has no mutable state, it doesn’t need to be a singleton.
// BAD, should not be a singleton!  
@Singleton

class RealToastFactory implements ToastFactory {
private final Application context;
  @Inject public RealToastFactory(Application context) {
this.context = context;
}
  @Override public Toast makeText(int resId, int duration) {
return Toast.makeText(context, resId, duration);
}
}
  • On rare occasions, we use scoping to cache instances that are expensive to create, or that are repeatedly created and thrown away.

Favor @Inject over @Provides

  • @Provides methods should not duplicate the constructor boilerplate.
  • Code is easier to understand when coupled concerns are in one place.
@Module
class ToastModule {
// BAD, remove this binding and add @Inject to RealToastFactory
@Provides RealToastFactory realToastFactory(Application context) {
return new RealToastFactory(context);
}
}
  • This is especially important for singletons; it’s a key implementation detail that you need to know when reading that class.
// GOOD, I have all the details I need in one place.
@Singleton

public class BadgeCounter {
  @Inject public BadgeCounter(...) {}  
}

Favor static @Provides methods

  • Dagger @Provides methods can be static.
@Module
class ToastModule {
@Provides
static ToastFactory toastFactory(RealToastFactory factory) {
return factory;
}
}
  • The generated code can directly invoke the method instead of having to create a module instance. That method call can be inlined by the compiler.
@Generated
public final class DaggerAppComponent extends AppComponent {
// ...
  @Override public ToastFactory toastFactory() {
return ToastModule.toastFactory(realToastFactoryProvider.get())
}
}
  • One static method won’t change much, but all bindings being static will result in a sizable performance increase.
  • Make your modules abstract and Dagger will fail at compile time if one of the @Provides methods isn’t static.
@Module
abstract class ToastModule {
@Provides
static ToastFactory toastFactory(RealToastFactory factory) {
return factory;
}
}

Favor @Binds over @Provides

  • @Binds replaces @Provides for when you’re mapping one type to another.
@Module
abstract class ToastModule {
@Binds
abstract ToastFactory toastFactory(RealToastFactory factory);
}
  • The method must be abstract. It will never be invoked; the generated code will know to directly use the implementation.
@Generated
public final class DaggerAppComponent extends AppComponent {
// ...
  private DaggerAppComponent() {
// ...
this.toastFactoryProvider = (Provider) realToastFactoryProvider;
}
  @Override public ToastFactory toastFactory() {
return toastFactoryProvider.get();
}
}

Avoid @Singleton on interface bindings

Statefulness is an implementation detail

  • Only implementations know if they need to ensure centralized access to mutable state.
  • When binding an implementation to an interface, there shouldn’t be any scoping annotation.
@Module
abstract class ToastModule {
// BAD, remove @Singleton
@Binds @Singleton
abstract ToastFactory toastFactory(RealToastFactory factory);
}

Enable error-prone

Several Square teams are using it to detect common Dagger mistakes, check it out.

Conclusion

These guiding principles work well for our context: small heterogeneous teams working on a large shared Android codebase. Since your context is likely different, you should apply what makes the most sense for your team.

It’s your turn! What good practices do you follow to keep your dagger code sharp?



Source link