Skip to content

Scope Annotations

Daniel Molinero edited this page Jan 19, 2017 · 18 revisions

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.

Custom scope annotations

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

Binding a scope to a scope annotation

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 scope scope or its parents.

Scope annotated classes ...are singleton scoped !

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 and scoped bindings

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.

Scope annotations and scope names

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.

Scope annotations and providers

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.

Links