Dependency injection is a technique whereby one object supplies the dependencies of another object. A dependency is an object that can be used. An injection is the passing of a dependency to a dependent object that would use it. The intent is to decouple objects so that no client has to be changed simply because an object it depends on needs to be changed to a different one. This permits following the Open / Closed principle and Inversion of Control principle. The most important advantage is that it increases the possibility of reusing the class and to be able to test them independent of other classes.
public class MyPresenter {
// Internal reference to the service used by this client
private MyDatabase database;
MyPresenter() {
// Specify a specific implementation in the constructor instead of using dependency injection
database = new MyDatabase(context);
}
}
Constructor injection
// Constructor
MyPresenter(MyDatabase database) {
// Save the reference to the passed-in service inside this client
this.database = database;
}
Setter injection
// Setter method
public void setDatabase(MyDatabase database) {
// Save the reference to the passed-in service inside this client.
this.database = database;
}
Interface injection
This is simply the client publishing a role interface to the setter methods of the client's dependencies. It can be used to establish how the injector should talk to the client when injecting dependencies.
public interface DatabaseSetter {
public void setDatabase(MyDatabase database);
}
public class MyPresenter implements DatabaseSetter {
private MyDatabase database;
// Set the service that this client is to use.
@Override
public void setDatabase(MyDatabase database) {
this.database = database;
}
}
Constructor injection assembly
class MyActivity {
@Override
public void onCreate() {
Context context = this.getApplicationContext();
MyDatabase db = new MyDatabase(context);
MyPresenter presenter = new MyPresenter(db);
}
}
class MyDatabase {
MyDatabase(Context context) {
}
}
class MyPresenter {
MyPresenter(MyDatabase db) {
}
}
A fast dependency injector for Android and Java. Compile-time evaluation. Uses code generation and is based on annotations.
dependencies {
implementation 'com.google.dagger:dagger:2.23.1'
annotationProcessor 'com.google.dagger:dagger-compiler:2.23.1'
}
For kotlin.
apply plugin: 'kotlin-kapt'
dependencies {
implementation 'com.google.dagger:dagger:2.23.1'
kapt "com.google.dagger:dagger-compiler:2.23.1"
}
-
Dependency provider: Classes annotated with @Module are responsible for providing objects which can be injected. Such classes define methods annotated with @Provides. The returned objects from these methods are available for dependency injection.
-
Dependency consumer: The @Inject annotation is used to define a dependency. Can be used on a constructor, a field, or a method. @Inject annotated constructor provides itself as a dependency. @Inject annotated field asks for dependency to be injected in that field.
-
Connecting consumer and producer: A @Component annotated interface defines the connection between the provider of objects (modules) and the objects which express a dependency. The class for this connection is generated by the Dagger.
Module provides dependencies.
@Module
class ContextModule {
Context context;
ContextModule(Context context) {
this.context = context;
}
@Provides
@Named("ApplicationContext")
Context anyName() {
return context;
}
}
Database asks for context dependency and provides itself as a dependency.
@Singleton
class MyDatabase {
@Inject
MyDatabase(@Named("ApplicationContext") Context context) {
}
}
Component connects dependants that ask for dependencies with modules that provides them.
@Component (modules={ContextModule.class, OtherModule.class})
interface MyComponent {
void inject(MyPresenter presenter);
}
Instantiation of the component.
class MyApplication extends Application {
private static MyComponent component;
@Override
void onCreate() {
component = DaggerMyComponent.builder()
.contextModule(new ContextModule(getApplicationContext()))
.otherModule(new OtherModule())
.build();
}
public static MyComponent getMyComponent() {
return component;
}
}
Invocation of dependency injection. Presenter asks for database, component injects database by calling it's constructor, which in turn asks for context dependency, which is provided by the module.
class MyPresenter {
@Inject
MyDatabase db;
MyPresenter() {
MyApplication.getMyComponent().inject(this);
}
}
Scope determines the lifetime of a variable. If we do not specify any Scope annotation, the Component will create a new instance every time the dependency is injected, whereas if we do specify a Scope, Component can retain the instance for future use.
The scope is defined with a given name, where the name could be any name e.g. @Singleton, @ActivityScope, @FragmentScope, @WhateverScope.
@Scope
@Retention(RetentionPolicy.RUNTIME)
public @interface ScopeName {
}
Dagger provides @Singleton scope annotation. It is just a usual named scope defined the same way but with a name Singleton. @Singleton annotation doesn't provide singleton functionality by itself. It's developer's responsibility to make sure that the Component with @Singleton annotation created only once. Otherwise every Component will have it's own version of @Singleton object.
Lifecycle of scoped objects tied to the lifecycle of the Component. Components live as long as you want it to or as long as class that created component wasn't destroyed (like android activity or fragment). If Component built in application its instances of scoped objects will live as long as application or until manually cleared. This is useful for application level dependencies like ApplicationContext. If Component is built in activity, scoped instances will be cleared on destroy of activity.
If at least one provide method in a module has a scope annotation the Component should have the same scope annotation. Which means that if you want named scope, you need a separate Component for it. Components can depend on each other.
- Two dependent components cannot have the same Scope.
- Parent component must explicitly declare objects which can be used in child components.
- Component can depend on other components.
@Component (modules={ContextModule.class, OtherModule.class})
@Singleton
interface AppComponent {
// Providing dependencies to children.
@Named("ApplicationContext") Context anyName();
MyDatabase anyOtherName();
// injects
}
@Component (dependencies={AppComponent.class}, modules={ActivityModule.class})
@ActivityScope
interface ActivityComponent {
// Context and MyDatabase available from parent and can be passed futher.
@Named("ApplicationContext") Context anyName();
// injects
}
Instantiation.
appComponent = DaggerAppComponent.builder()
.contextModule(new ContextModule(getApplicationContext()))
.otherModule(new OtherModule())
.build();
activityComponent = DaggerActivityComponent.builder()
.appComponent(appComponent)
.build();
Same goal as component dependencies, different approach.
- Parent component is obliged to declare Subcomponents getters inside its interface.
- Subcomponent has access to all parents objects.
- Subcomponent can only have one parent.
@Component (modules={ContextModule.class, OtherModule.class})
@Singleton
interface AppComponent {
ActivityComponent plusActivityComponent(ActivityModule activityModule);
// injects
}
@Subcomponent (modules={ActivityModule.class})
@ActivityScope
interface ActivityComponent {
// injects
}
Instantiation.
appComponent = DaggerAppComponent.builder()
.contextModule(new ContextModule(getApplicationContext()))
.otherModule(new OtherModule())
.build();
activityComponent = appComponent.plusActivityComponent(new ActivityModule());
A scope tree based Dependency Injection library for Java. It is a full-featured, runtime based, but reflection free, implementation of JSR 330. Android helper is called Smoothie.
dependencies {
implementation 'com.github.stephanenicolas.toothpick:toothpick-runtime:2.1.0'
implementation 'com.github.stephanenicolas.toothpick:smoothie-androidx:2.1.0'
annotationProcessor 'com.github.stephanenicolas.toothpick:toothpick-compiler:2.1.0'
}
Repository provides itself as a singleton dependency.
@Singleton
public class Repository {
private String data = "data";
@Inject
public Repository() {}
public String getData() {
return data;
}
}
Presenter provides itself as a dependency. Its own constructor dependencies will be resolved at instantiation.
public class Presenter {
private Repository repository;
@Inject
public Presenter(Repository repository) {
this.repository = repository;
}
public String getText() {
return repository.getData();
}
}
Activity requieres presenter. It is injected with Toothpick.inject() withing created scope. Scope is closed after activity is destroyed.
public class MainActivity extends AppCompatActivity {
@Inject public Presenter presenter;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
Scope scope = Toothpick.openScope(this);
Toothpick.inject(this, scope);
String text = presenter.getText();
}
@Override
protected void onDestroy() {
Toothpick.closeScope(this);
super.onDestroy();
}
}
A scope contains bindings & scoped instances:
-
binding: is way to express that a class (or interface)
IFoo
is associated to an implementationFoo
, which we denoteIFoo --> Foo
. It means that writing@Inject IFoo a
; will return aFoo
. Bindings are valid for the scope where there are defined, and are inherited by children scopes. Children scopes can also override any binding inherited from of a parent. -
scoped instances: a scoped instance is an instance that is reused for all injections of a given class. Scoped instances are "singletons" in their scope, and are visible to children scopes.
Not all bindings create scoped instances. Scopes create a tree. Each scope can have children scopes. Scopes have a name, which can be any object. Opening multiple scopes is possible, the opened scopes will then be the children from each other, in left-to-right order. This method will return the last open scope.
Setting up the bindings in a scope is performed via installation of modules in a scope. A module defines a set of bindings.
public class ActivityModule extends Module {
public ActivityModule() {
bind(BaseRepository.class).to(Repository.class);
bind(BasePresenter.class).to(Presenter.class);
}
}
Scope scope = Toothpick.openScope(this);
scope.installModules(new ActivityModule());
Toothpick.inject(this, scope);
Named bindings.
bind(BaseRepository.class).withName("repo").to(Repository.class);
bind(BasePresenter.class).withName("presenter").to(Presenter.class);
@Inject
public Presenter(@Named("repo") BaseRepository repository) {
this.repository = repository;
}
@Inject @Named("presenter") public BasePresenter presenter;
Binding modes.
-
bind(IFoo.class).to(Foo.class) Every
@Inject IFoo
will be assigned a new instance ofFoo
. -
bind(IFoo.class).toInstance(new Foo()) Every
@Inject IFoo
will be assigned the same instance ofFoo
. The instance defined in the module. -
bind(IFoo.class).toProvider(FooProvider.class) Every
@Inject IFoo
will be assigned a new instance ofFoo
produced by a new instance ofFooProvider
. -
bind(IFoo.class).toProviderInstance(new FooProvider()) Every
@Inject IFoo
will be assigned a new instance ofFoo
produced by the same instance ofFooProvider
. The instance defined in the module. -
bind(Foo.class) Every
@Inject Foo
will be assigned a new instance ofFoo
.
Scoped, Unscoped.
In toothpick there are 2 kinds of bindings: unscoped bindings, scoped bindings. An unscoped binding expresses no constraints on the creation of the Foo instances, as opposed to a scoped binding. An unscoped binding is said to belong to a given scope (in which it will be installed via a Module).
A binding is scoped when we call one of its method xxxInScope():
-
instancesInScope(): there is an association
IFoo --> Foo
, in the same way as an unscoped binding does AND the instance ofFoo
will be created inside the scope that defines this binding. All the dependencies ofFoo
will have to be found at runtime in the scope where the binding is scoped or in its parent scopes. -
singletonInScope(): same as
instancesInScope()
AND the same instance ofFoo
is recycled/reused for each injection ofIFoo
. -
providesSingletonInScope(): same as
singletonInScope()
, but applied to the object provided by the providers. The provider will provide only one instance ofIFoo
, and it will be created inside the scope where the binding is defined.
A pragmatic lightweight dependency injection framework for Kotlin developers. Written in pure Kotlin, using functional resolution only: no proxy, no code generation, no reflection.
dependencies {
implementation "org.koin:koin-android:2.0.1"
// Koin Android Scope features
implementation "org.koin:koin-android-scope:2.0.1"
// Koin Android ViewModel features
implementation "org.koin:koin-android-viewmodel:2.0.1"
// Koin Android Experimental features
implementation "org.koin:koin-android-ext:2.0.1"
}
Declare modules.
// Given some classes
class Controller(val service : BusinessService)
class BusinessService()
val myModule = module {
single { BusinessService() } // Create singleton.
single(named("mock")) { MockBusinessService() } // Named.
single { HttpClient(getProperty("server_url")) } // getProperty() resolves from Koin properties.
factory { Controller(get()) } // Create a new instance each time. get() resolves dependencies.
viewModel { MyViewModel(get()) } // Create ViewModel.
}
Start Koin.
class MyApplication : Application() {
override fun onCreate() {
super.onCreate()
startKoin {
androidLogger() // Optional, for logging.
androidContext(this@MyApplication)
androidFileProperties() // Optional to load properties from assets.
modules(myModule, myOtherModule)
}
}
}
Inject.
class MyActivity() : AppCompatActivity() {
val controller : Controller by inject() // lazy
val myViewModel: MyViewModel by viewModel()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val service : BusinessService = get() // directly
}
}
MyScopePresenter class declared as a scoped definition for MyScopeActivity. This will allows to bind a MyScopePresenter with a scope, and drop this instance with the scope closing.
val appModule = module {
scope(named<MyScopeActivity>()) {
scoped { MyScopedPresenter(get()) }
}
scope(named("MY_CUSTOM_SCOPE)) {
scoped { SomeScopedService(get()) }
}
}
The currentScope allows to retrieve/create a Koin scope for given activity.
class MyScopeActivity : AppCompatActivity() {
// inject MyScopePresenter from current scope
val scopePresenter: MyScopePresenter by currentScope.inject()
...
}
public class JavaActivity extends AppCompatActivity {
private Lazy<MyJavaPresenter> javaPresenter = inject(MyJavaPresenter.class);
...
}
Toothpick
https://github.com/stephanenicolas/toothpick
https://github.com/stephanenicolas/toothpick/wiki
Koin
https://github.com/InsertKoinIO/koin
https://insert-koin.io/
https://insert-koin.io/docs/2.0/getting-started/introduction/