-
Notifications
You must be signed in to change notification settings - Fork 114
Scopes
A scope is one of the most important concept in Toothpick. It is actually important in Dependency Injection (DI) at large but Toothpick clearly exposes it to developers.
In Toothpick, injections and instances creation always take place within a given scope.
Let's consider the following example of injection :
//a class using field and method injections
class Foo {
@Inject Bar bar;
@Inject void setQurtz(Qurtz qurtz) {...}
}
//injecting an object in a scope
Toothpick.inject(new Foo(), scope);
The statement above is an entry point of a dependency graph in Toothpick. It will inject new Foo()
, which means that :
- all
@Inject
annotated fields of the instance ofFoo
will be assigned; - all
@Inject
annotated methods of the instance ofFoo
will be called;
In both cases, the values of the injected field and the parameters of the injected methods will be created inside the scope scope
and its parents. Note that all transitive dependencies of Bar
and Qurtz
will also be created if needed, also in the scope scope
and its parents.
Let's consider the following example of instance creation :
//a class using field and method injections
class Foo {
@Inject Foo() {...}
@Inject Bar bar;
@Inject void setQurtz(Qurtz qurtz) {...}
}
//creating an instance in a scope
scope.getInstance(Foo.class);
The statement above is also an entry point of a dependency graph in Toothpick. It will create an instance of the class Foo
and inject all its members as in the previous injection example. The creation of the instance of Foo
will use the @Inject
annotated constructor and all required parameters, if any, will also be created within the scope scope
.
If Toothpick creates an instance, it will always inject its dependencies.
A scope contains bindings & scoped instances :
-
a 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. Binding IFoo.class
to Foo.class
will not create scoped instances. Instead, every time we inject @Inject IFoo iFoo
, a new instance of Foo.class
will be created, there will be no recycled instance.
In the same way, if we bind IFoo.class
to a Provider<IFoo>
instance, it means that a new instance of IFoo
be created every time we inject @Inject IFoo iFoo
from this scope and children scopes. Note that in the case of the provider, the provider itself is scoped = recycled, not the instances it produces.
Finally, some bindings are implicitly scoped. For instance, binding Foo.class
to an instance of Foo
implicitly tells Toothpick to recycle this instance every time we inject @Inject Foo foo
, in this scope and its children scopes.
In Toothpick, scopes create a tree (actually a disjoint forest, a.k.a a directed acyclic graph - DAG-). Each scope can have children scopes.
//Example of scopes during the life of an Android application
Application Scope
/ \ \
/ \ \
Activity Scope \ Service 2 Scope
/ \
/ Service1 Scope
/
Fragment Scope
With the scope tree, Toothpick is a DI framework that exposes 2 DAGs (which makes it a square dag :P) :
- the DAG of injections (class
A
uses an injected dependencyB
) - the DAG of scopes (
Fragment Scope
is a child ofActivity Scope
)
Being a DAG has a direct impact on the Toothpick scope tree :
Toothpick will always bubble up the scope tree. It will never go down the scope tree.
Operations on the scope tree (adding / removing children, etc.) should be performed via the Toothpick
class that wraps these operations.
Scopes have a name, which can be any object. Here is the code to create the scope tree above.
Toothpick.openScopes(application, activity, fragment);
Toothpick.openScopes(application, service1);
ToothPick.openScopes(application, service2);
It's intuitive to use the objects having a life cycle as names. When the object starts its life cycle, create a scope Toothpick.openScope(scopeName)
and when the object dies, close its associated scope Toothpick.closeScope(scopeName)
. These object are generally not created by a DI framework, but can be injected and be an entry point of a dependency injection graph.
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.
Note: when using openScope
, a developer can never know if such a scope already existed or not. If a scope by this name already existed, it is returned, otherwise, it is created. This feature enables advanced programming techniques when used together with scope annotations.
The methods to open and close scopes take a parameter : the scope name, not the scope object itself. A scope name can be any object. We highly recommend, when an object has a life cycle and an associated scope, to use this object directly as the name of its associated scope.
Toothpick.openScope(activity);
Toothpick.closeScope(activity);