Skip to content
Stéphane Nicolas edited this page Sep 13, 2019 · 30 revisions

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.

What is a scope ?

When reading the JSR 330 specifications, it's not very clear what a scope is.

In toothpick, and especially in TP 3, scopes are more stricly defined. Scopes are a tool to organize memory when an application is running. A scope is a space in memory that is associated to a duration, it has a lifecycle. Scopes make sure that you can fully garbage collect all things created in a given part of then memory when they are not needed anymore. Scopes also enforce design between different parts of an application, and prevent memory leaks.

Let's take an example on Android. On Android, an instance of the application class exists as soon and as long as your app is running. While the app will run = while the instance of the application lives, multiple activities will live and die.

   -----------------------------------------
   |         Application lives             |
   -----------------------------------------
   -------------------- --------------------
   | Activity 0 lives | | Activity 1 lives |
   -------------------- --------------------

In the app above, it would be very convenient to create a space in memory for what belongs to the application, and a space in memory for what belongs to each activity. It would:

  • make sure that things that belong to the application do not know about activities. If they would, it would leak an activity, preventing it from being garbage collected after it is not displayed anymore on screen.
  • let the activity use things associated to the application, there is no issue there. Such entities do always exist when the activity is alive.
  • allow to garbage collect all things related to each activity when they are closed.
  • enforce the isolation of activities with respect to each other. The only dependencies they can share in this case, are related to the application.

Scopes do exactly that. A typical Android app using TP will create a scope for the application instance, and one for each activity. And the concept can be pushed much further, one can create as many scopes as you want in TP.

Scopes, instances creation and injections

In Toothpick, injections and instances creation always take place within a given scope.

Injecting within a 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 of Foo will be assigned;
  • all @Inject annotated methods of the instance of Foo 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.

Creating an instance within a scope

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 in the same way 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.

Toothpick scopes

A scope contains bindings & scoped instances :

  • a 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.
  • scope singletons : a scope singleton is an instance that is reused for all injections of a given class in a given scope. Scope singletons are "singletons" in their scope, and are visible to children scopes. (Note that this is really emphasized in TP3.)

Not all bindings create scope singletons. Binding IFoo.class to Foo.class will not create a scope singleton. Instead, every time we inject @Inject IFoo iFoo, a new instance of Foo.class will be created, there will be no recycled instance, no singleton of Foo.

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 a provider, the provider itself could be a singleton or not, same goes for the instanes it produces. This can be fine customized when defining a binding.

Finally, some bindings are implicitly singletons. 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.

The scope Tree and the Toothpick class

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 dependency B)
  • the DAG of scopes (Fragment Scope is a child of Activity 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);

As an alternative, the fluent API of TP 3, would define the same tree by:

Toothpick.openScope(application);
    .openSubScope(activity)
    .openSubScope(fragment);
   
Toothpick.openScope(application)
         .openSubScope(service1);

Toothpick.openScope(application)
         .openSubScope(service2);

It is also possible to pass a configuration lambda when opening a scope, please read this section about it.

TP 3.1+ root scope

Starting with version 3.1, TP offers a convenient API to access the root scope without giving it a name:

//java
Toothpick.openRootScope()
//kotlin
KTP.openRootScope()

Note that there is no closing method for closing the root scope as it is not needed. These methods are intended for simple applications that want to use a single scope. Though, we had also in mind that such applications might want to later add other scopes as they grow in complexity. Hence, the root scope that is returned acts both as a default scope for simple apps and will also return the root of the scope tree as your application adds other scopes.

If you use a default scope, you don't need an application scope, these are conceptually equivalent (the root scope is static, so it is unique inside a VM, exactly as the application instance is unique inside a Android JVM).

Scopes associated to 'living objects'

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 objects are generally not created by a DI framework, but can be injected and be an entry point of a dependency injection (sub)graph.

Opening multiple scopes is possible, the opened scopes will then be children from each other, in left-to-right order. This method will return the last open scope.

Note: when using openScope, it will always work whether or not such a scope already existed. 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.

A small note on opening & closing

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

Links