Skip to content

Scope Resolution

Stéphane Nicolas edited this page Jul 17, 2019 · 17 revisions

Scope resolution

Scope resolution is one of the most important part of Toothpick. It is an advanced feature that helps to design better layered applications, manage memory in a more fine grained way and prevents memory leaks.

The core idea is that scopes are always bubbled up when resolving an injection, and looking up for a binding. Toothpick never goes down the scope tree.

At runtime, when Toothpick will create the injection graph to create an instance of a class and its dependencies, Toothpick will crash if a dependency of this class can't be found in the scope itself or its parent scopes.

Example

//Example of scopes during the life of an Android application

+----------------------------------------------------------------------------------+  Resolution
| +---------------------------------------------------------------+                |  space
| |  application scope = @RootScope :                             | Resolution     |  for Activity
| |        /                    - Scope --> (application)         | space          |  scope
| |       /                     - IDisplay --> DisplayImpl1       | for @RootScope |
| |      /                      - FooSingleton --> (FooSingleton) | scope          |  
| +-----/---------------------------------------------------------+                |
|   activity scope = @ActivityScope :                                              |
|                               - Scope --> (activity)                             |
|                               - IDisplay --> DisplayImpl2                        |
|                               - FooActivitySingleton --> (FooActivitySingleton)  |
+----------------------------------------------------------------------------------+

Let's define :

class DisplayImpl1 {@Inject Scope scope}
class DisplayImpl2 {@Inject Scope scope}
@RootScope @Singleton class FooSingleton {@Inject IDisplay display; @Inject Scope scope}
@ActivityScope @Singleton class FooActivitySingleton {@Inject Scope s; @Inject IDisplay display;}
@RootScope class FooError {@Inject FooActivitySingleton foo;}

Below, we detail the result of the statement

 scope.getInstance(obj);  

where both obj and scope vary.

scope = root Scope

All dependencies will be resolved in the @RootScope resolution space.

  • obj = scope.getInstance(Scope.class)

  • obj = application scope.

  • obj = scope.getInstance(IDisplay.class)

  • obj is an instance of Display1Impl

  • obj.scope = application scope

  • obj is not a singleton, an extra call to getInstance would produce a new instance of DisplayImpl1

  • obj = scope.getInstance(FooSingleton.class)

  • obj is an instance of FooSingleton

  • obj is a singleton, an extra call to getInstance would return the same instance of FooSingleton in this scope and any child scope

  • obj.scope = application scope

  • obj.display is an instance of Display1Impl

  • obj.display.scope = application scope

  • obj = scope.getInstance(FooActivitySingleton.class)

  • would crash as FooActivitySingleton is annotated with @ActivityScope and the application scope (or its parents) do not support this annotation.

  • obj = scope.getInstance(FooSingletonError.class)

  • would crash as FooActivitySingleton is annotated with @ActivityScope and the application scope (or its parents) do not support this annotation.

scope = activity Scope

All dependencies will be resolved in the activity scope resolution space.

  • obj = scope.getInstance(Scope.class)

  • obj = activity scope.

  • obj = scope.getInstance(IDisplay.class)

  • obj is an instance of Display2Impl

  • obj.scope = activity scope

  • obj is not a singleton, an extra call to getInstance would produce a new instance of DisplayImpl2

  • obj = scope.getInstance(FooSingleton.class)

  • obj is the same singleton instance produced by applicationScope.getInstance(FooSingleton.class)

  • obj.scope = application scope

  • obj.display is an instance of Display1Impl

  • obj.display.scope = application scope

  • obj = scope.getInstance(FooActivitySingleton.class)

  • obj is an instance of FooActivitySingleton

  • obj is a singleton, the same instance of FooActivitySingleton would be returned with an extra call of getInstance, in this scope and any child scope

  • obj.scope = activity scope

  • obj.display is an instance of Display2Impl

  • obj.display.scope = activity scope

  • obj = scope.getInstance(FooSingletonError.class)

  • would crash as FooActivitySingleton is annotated with @ActivityScope and the application scope (or its parents) do not support this annotation. Though we are in the activity scope, FooSingletonError is annotated with @RootScope and will be created in the application scope.

When, in a scope s we scope a binding IFoo --> (Foo), we are setting up the possibility of creating instances of Foo in the scope s : all dependencies of Foo, included their transitive dependencies, must exist in s and the scope above it OR be unscoped. Foo can never ever use any dependency that would belong to a children scope.

Scope Resolution and memory leaks

The scope resolution mechanism in Toothpick is actually a constraint checking system. It obeys to a simple rule:

In Toothpick, the scope tree is always bubbled up, never down.

This simple rule makes sure that a scope never has a given child scope fulfilling the dependencies of one of its bindings. A scope only knows about its parents and should only depend on them (or on unscoped bindings). And this scope checking mechanism helps to prevent memory leaks.

Let's take a simple example on Android: the scoped bindings of the application scope will only be able to use entities define in this scope itself. They will not be allowed to use dependencies scoped in the activity scope, indeed, the activities scopes, the children of the application scope are not even visible during scope resolution. This ensures that it is not possible to hold a reference on an activity (or an entity in its scope) at the application level. Because as soon as this would become true, it would mean that the activity would not be garbage collected, even after its scope is closed: because the application scope would still hold a reference on it.

Note that, of course, you can still create a leak "manually", for instance an application singleton could define a method setCurrentActivity and any activity could pass itself as a parameter. But, using TP and DI only, we eliminate this issue.

The same principle will also apply to the activity scope itself: all its scoped bindings and singletons will have to use dependencies scoped in the activity scope itself or the application scope. But none of them will have access to scoped bindings and instances of a potential fragment child scope. Again, this prevent a memory leak where a fragment would not be garbage collected as its reference would be held by an activity, even though the fragment and its associated scope have been closed.

A last example: with toothpick it is not possible to make the very common mistake of using a fragment (let's say a dialog) during networking activity. Why ? Because the network service class should be scoped at the application scope level. By doing so, we make sure that it will never be able to use, even transitively, an activity or a fragment. This is actually a great feature, it simply allows, for instance, services or the application itself to use the network layer without crashing, as those don't provide a fragment manager. It also prevents activities from leaking as a reference to a fragment would be held in the network stack somewhere in your app.

We strongly encourage developers to take advantage of scope resolution to make more robust apps.