-
Notifications
You must be signed in to change notification settings - Fork 115
Scope Annotations
Scope annotations are an alternative to defining scoped bindings, more static but powerful and convenient.
Scope annotations apply to classes in Toothpick.
The most famous scope annotation is probably the @javax.inject.Singleton
annotation.
//a class using the scope annotation @Singleton
@Singleton
class Foo {...}
This annotation is simply understood as "I want a single instance of the Foo
class", and this remains true in Toothpick. Foo
will have a single instance and will be available in the root scope and all its children.
But the JSR 330 defines a mechanism to go beyond this simple example and allows the creation of custom scope annotations.
Note : In the JSR 330 standard classes, @Singleton
is defined as
@javax.inject.Scope
@Documented
@Retention(RUNTIME)
interface @Singleton {}
being annotated with @Scope
is the reason why @Singleton
is understood as a scope annotation in all JSR 330 based DI frameworks.
//a custom scope annotation
@javax.inject.Scope
@Documented
@Retention(RUNTIME)
interface @ActivitySingleton {}
//a class using the custom scope annotation @ActivitySingleton
@ActivitySingleton
class Bar {...}
A scope annotation is an annotation class that is qualified by the javax.inject.Scope
annotation, such as the @ActivitySingleton
scope above.
Annotating Bar
with @ActivitySingleton
means "I want a single instance of Bar in the scope that supports the scope annotation @ActivitySingleton
".
Scope annotations must have RUNTIME retention. This is needed to perform runtime checks to ensure the right usage of Toothpick.
Scopes can be associated or bound to a scope annotation. (Note: we use the same word 'bind' but it's not related to binding Foo
--> Bar
).
Let's define a simple scope annotation :
//A scope annotation
@javax.inject.Scope
@Documented
@Retention(RUNTIME)
public @interface ContextSingleton {}
to bind a scope to this annotation :
//Binding a scope to a scope annotation
scope.bindScopeAnnotationClass(ContextSingleton.class);
Then, to use the annotation :
@ContextSingleton
public class Foo {}
All classes annotated with a scope annotation will automatically be scoped by Toothpick in the scope that is associated to the scope annotation. Such a scope must be available at runtime as a parent scope of the current scope.
When the statement @Inject Foo foo;
is executed in the scope scope
or its children scopes, it will :
- create an instance of
Foo
; - recycle this instance for every
@Inject Foo
statement. - check that
Foo
only has either unscoped dependencies or dependencies scoped in the scopescope
or its parents.
Note that, by design, using a scope annotation to annotate a class as in :
@ActivitySingleton class Foo {...}
makes it mandatory for Foo
to be created in a scope that is bound to the annotation @ActivitySingleton
. It is not possible to create a Foo
instance via Toothpick outside of such a scope, nor is it possible to bind the Foo
class in a binding, in any module, except if the module is bound to the scope annotation @ActivitySingleton
.
Scope annotations allow to :
- create a binding
- AND scope the binding in a given scope
- AND recycle the instance between injections (creating a scoped singleton).
Using scope annotations is exactly the same as defining a singleton scoped binding.
The annotation way is more expressive, it is generally useful for more static scopes, that have a constant name.
//binding a Foo instance in a scope
scope.installModule( new Module() {{
bind(Foo.class).to(Foo.class).scope();
}});
//is equivalent to
scope.bindToAnnotationScope(ContextSingleton.class);
@ContextSingleton
public class Foo {}
From now on, we will use the following convention in diagrams to say that a scope annotation is bound to a given scope :
Scope S0 = @Singleton //bound to the scope annotation @Singleton
\
\
Scope S1 // not bound to any scope annotation
\
\
@S2Singleton //a scope bound to the scope annotation @S2Singleton
Note that it is possible to bind multiple annotations to a scope and to bind the same scope annotation to multiple scopes. Though, neither of these practices are advisable and they should be avoided.
Nevertheless, this is the way we can migrate apps that were using the infamous ContextSingleton
of RoboGuice. But remember, this is a bad practice, as it doesn't take advantage of scope resolution, or blurs it and make it leak-prone.
A scope has a name in Toothpick. It is required to open it :
Scope s = Toothpick.open(<any object>);
The name can be of any type.
If the name of a scope is an scope annotation class, then this scope is automatically bound to this scope annotation.
//custom scope annotation
@Scope
interface @ActivitySingleton {}
//bind a scope to a scope annotation, by name
Scope s0 = Toothpick.open(ActivitySingleton.class);
The scope s0
will automatically be associated to the scope annotation @ActivitySingleton
. It is strictly equivalent to
//bind a scope to a scope annotation, by name
Scope s0 = Toothpick.open(<any object>);
s0.bindToAnnotationScope(ActivitySingleton.class);
Both mean that any class annotated with this scope annotation will be created in s0
.
Example :
Scope s0 = @ActivitySingleton
\
\
Scope s1
@ActivitySingleton class Foo {...}
class Bar {...}
class Qurtz {
@Inject Foo foo;
@Inject Bar bar;
}
Toothpick.inject(new Qurtz(), s1);
In the example above, the Qurtz
instance will receive a new Bar
instance created in the scope s1
, and the Foo
instance will be the singleton that lives in the scope s0
.
Provider classes can be annotated:
@ActivitySingleton class FooProvider implements Provider<Foo> {...}
bind(Foo.class).toProvider(FooProvider.class);
scope.getInstance(Foo.class);
In this case, the scope verification mechanism is enforced for creating a singleton instance of the FooProvider
class. The same provider instance will be reused to create all injected instances of Foo
.
It is equivalent to using the following binding:
bind(Foo.class).toProvider(FooProvider.class).singletonInScope();
Using the binding above, FooProvider
doesn't need to be annotated.
If we want to indicate that not only the provider will be a singleton but also the produced Foo
instance itself, we can use the extra annotation:
@ActivitySingleton @ProvidesSingletonInScope
class FooProvider implements Provider<Foo> {...}
It is equivalent to using the following binding:
bind(Foo.class).toProvider(FooProvider.class).providesSingletonInScope();
Using the binding above, FooProvider
doesn't need to be annotated with any of the annotation.