Skip to content

Latest commit

 

History

History
594 lines (430 loc) · 18.7 KB

File metadata and controls

594 lines (430 loc) · 18.7 KB

DI

DI Concept

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.

Without dependency injection

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);
    }
}

Dependency injection without frameworks

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) {
    }
}

Content

Dagger Maven Source dagger-starsbadge

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"
}

Dagger. Dependency injection process

  • 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.

Dagger. Dependency injection

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);
    }
}

Dagger. Scopes

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.

Dagger. Component dependencies

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();

Dagger. Subcomponents

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());

Content

Toothpick Maven Source toothpick-starsbadge

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'
}

Toothpick. Basic Usage

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();
    }
}

Toothpick. Scopes

A scope contains bindings & scoped instances:

  • binding: is way to express that a class (or interface) IFoo is associated to an implementation Foo, which we denote IFoo --> Foo. It means that writing @Inject IFoo a; will return a Foo. 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.

Toothpick. Modules and Bindings

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 of Foo.

  • bind(IFoo.class).toInstance(new Foo()) Every @Inject IFoo will be assigned the same instance of Foo. The instance defined in the module.

  • bind(IFoo.class).toProvider(FooProvider.class) Every @Inject IFoo will be assigned a new instance of Foo produced by a new instance of FooProvider.

  • bind(IFoo.class).toProviderInstance(new FooProvider()) Every @Inject IFoo will be assigned a new instance of Foo produced by the same instance of FooProvider. The instance defined in the module.

  • bind(Foo.class) Every @Inject Foo will be assigned a new instance of Foo.

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 of Foo will be created inside the scope that defines this binding. All the dependencies of Foo 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 of Foo is recycled/reused for each injection of IFoo.

  • providesSingletonInScope(): same as singletonInScope(), but applied to the object provided by the providers. The provider will provide only one instance of IFoo, and it will be created inside the scope where the binding is defined.

Content

Koin Maven Source koin-starsbadge

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"
}

Koin. Basic Usage

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
    }
}

Koin. Scopes

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()

    ...
}

Koin. Injecting into Java

public class JavaActivity extends AppCompatActivity {

    private Lazy<MyJavaPresenter> javaPresenter = inject(MyJavaPresenter.class);

    ...
}

Content

Sources

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/