Keeping the Daggers Sharp
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…
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 aNullPointerException
.
// 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.