-
Notifications
You must be signed in to change notification settings - Fork 0
Dependency Injection
This tutorial assumes you've already completed Basic Drive Systems.
GradleRIO-Redux includes a library called Dagger, made by Google. The purpose of this library is to be a dependency injector. Dependency injection is simply the process of one object receiving a dependency from another object, and it happens a lot. In fact, dependencies are injected every time a class is created with parameters. If class B is instantiated while class A is executing with a parameter of object C, then the dependency object C of class B is being injected, and dependency injection has taken place.
In robotics programming, dependency injection is most crucial to our subsystems, which often need to take in parameters such as motor IDs. In the examples provided up to this point, dependency injection was typically done from a RobotMap
or similar class that stores constants. However, doing this is not suggested, as many different objects will be trying to access the same class very quickly, and could lead to performance issues or other unwanted side effects.
This is where Dagger can help. Dagger uses something called the injection grid to act as an intermediate that can create, store, and return instances of any class it needs to, mostly through a series of annotations. Annotations are anything above a header with a @
in front of it. In IntelliJ, these are colored yellow.
Note: read this page in its entirety. Reading only part of it may result in an incomplete understanding of how Dagger works and lead to errors.
Dagger by default does nothing as it is not created and has no structure. As such, the programmer must tell Dagger to create a GlobalComponent
. The GlobalComponent
is what "holds" the injection grid and interfaces with the entire program, however what it holds is up to the programmer. To do this, make an abstract class called GlobalComponent
and annotate it with @Component
.
Create a series of abstract methods that will act as "getters" for each of your Subsystems.
// Example for a DriveTrain subsystem
public abstract DriveTrain getDriveTrain();
Now create a method that calls all of the created getters. You may call this whatever you like, but it makes sense to call it something like init()
or robotInit()
as that is what it will be doing. Basically, this is needed because the robot needs to initialize all the motors and subsystems before it starts working, and they need to be put into the injection grid before they can be extracted again.
Go to the Robot
class. Make a field called globalComponent of GlobalComponent
and put in these two lines under robotInit()
:
globalComponent = DaggerGlobalComponent.create();
globalComponent.robotInit();
These will tell Dagger to create a global component and initialize all the subsystems. DaggerGlobalComponent
will be created as soon as you build the project, so it may appear as an error initially. Note this will break each time there is an issue with the Dagger compile.
Although the "getters" for all the subsytems have been created, that's just a way to access the things in the injector grid. A class annotated with @Module
is where things are put into the injection grid. Constructors should be private and take no arguments. They must also be "registered" with the GlobalComponent
by specifying a modules=
in the @Component
annotation on GlobalComponent
. An example of how to do that is below:
// With a single module
@Component(modules = CCModule.class)
// With more than one module/multiple modules
@Component(modules = {SubsystemModule.class, ControlsModule.class, CCModule.class})
Dagger is a dependency injector, but cannot by itself create any objects. The user tells Dagger what to put into the injection grid via the @Provides
annotation on methods in @Module
annotated classes. These should be public
visibility static
methods that return the things you want injected. For example, a SubsystemModule
might include a Turret
subsystem. The body of this method should be the only time that references the constructor of the subsystem. Otherwise, Dagger may be injecting a separate version of the subsystem than is being used elsewhere. (More on this in the @Singleton
section).
The @Module
annotated classes are the perfect alternatives for the RobotMap
discussed earlier, because they will be called only by the instantiated classes. As such, putting static
and final
fields in these classes is recommended, especially for things such as motor IDs. Pass those to the constructors in the methods, and they will be used when Dagger injects them as dependencies.
A subcomponent is exactly what it sounds like, just a child component of a parent component. The format of a subcomponent is the same as the component, except there will likely be no init()
method. Typically we will have a subcomponent called CommandComponent
to access commands from the injection grid. Note: the given classes are not actual commands, instead lists of commands by type. More detail is provided in the Riviera Robotics specific details page.
Additionally, subcomponents must have an enclosing module and a builder interface. For the enclosing module, specify subcomponents = <subcomponentClass>.class
in the parameter for @Module
on a blank interface. This can be an inner class/interface of the subcomponent class. The builder simply needs to be an interface annotated with @Subcomponent.Builder
that has a method build()
which returns an instance of the subcomponent. An example of this using CommandComponent
is below:
@Subcomponent
public abstract class CommandComponent {
public abstract DriveCommands drive();
@Module(subcomponents = CommandComponent.class)
public interface CCModule {
}
@Subcomponent.Builder
public interface Builder {
CommandComponent build();
}
}
The reason we do this is to be able to access things through the GlobalComponent
, which is also the reason we stored it in Robot
. Given this, add a line to GlobalComponent
that allows this access, as well as a line in Robot
that builds these "access routes". An example using CommandComponent
is provided below:
// in GlobalComponent
public abstract CommandComponent.Builder getCommandComponentBuilder();
// in Robot
// define a CommandComponent field first (here called commandComponent)
commandComponent = globalComponent.getCommandComponentBuilder().build();
Note:
@Provided
is not part of Dagger, and is instead part ofapt-creator
by octylFractal aka Octavia. However, as it is an annotations processor and crucial to robot code, it is included here.
This annotation is applied per constructor parameter, and tells Dagger to pull a parameter of that type from the injection grid. Note that this is mutually exclusive to @Inject
and the enclosing class must be annotated with @GenerateCreator
to be accessible.
Note:
@GenerateCreator
is not part of Dagger, and is instead part ofapt-creator
by octylFractal aka Octavia. This follows the same logic as@Provided
.
The @GenerateCreator
annotation does exactly what the name implies, it generates a creator. More specifically, it produces a creator class for the class that it's annotated on. This creator can be used to "create" an instance of that class without directly calling one of the class' constructors. Creators should be used for any command with some @Provided
parameters and some remaining parameters. The creator will automatically pull the Dagger dependencies when "created" and leave a create(...)
method that has the non-injected parameters.
To use this constructor, an intermediary class should be used, as it allows for the creator (named automatically in the form <commandClassName>
+ "Creator") to be automatically injected and simple access through the GlobalComponent
and associated subcomponents.
For an example, let's say we have class TurretCommands
which has an @Inject
annotated constructor and takes in a creator from @GenerateCreator
annotated command class TurretSetAngle
, with constructor as below. Note that the Turret
subsystem parameter is automatically removed from the constructor of TurretSetAngle
when used through the constructor.
public class TurretCommands {
private final TurretSetAngleCreator turretSetAngleCreator;
@Inject
public TurretCommands(TurretSetAngleCreator turretSetAngleCreator) {
this.turretSetAngleCreator = turretSetAngleCreator;
}
public TurretSetAngle setAngle(double angle) {
return turretSetAngleCreator.create(angle);
}
}
@GenerateCreator
public class TurretSetAngle extends CommandBase {
public TurretSetAngle(@Provided Turret turret, double angle) {
// Do constructor stuff
}
// Other command stuff
}
As the method is called setAngle(double angle)
, we can call the method by that name through the GlobalComponent
, given that the TurretCommands
class is represented by an abstract method. Note that the naming doesn't particularly matter, as it's mostly preference and will just change how you call the command constructor.
Note:
@Inject
is part of Java's builtin inject package (javax.inject.Inject
), but is included as Dagger provides an implementation for it and as such relies on its use.
This annotation is for constructors, and signifies that Dagger should pull from the injection grid anything included in the constructor. Use this if the entire parameter list is injected, or if there are no parameters. Typically commands use @Inject
in this capacity.
If @Inject
is being used for this, such as with a command that takes no parameters, a TurretCommands
type intermediate class can access it via a Provider<T>
instead of a XXXXCreator
as @GenerateCreator
would produce. The type T
is the class you'd like to return a new instance of. Instead of create(...)
you'd call get()
. An example of this is below:
private final Provider<TurretToggleMode> toggleModeProvider;
// constructor TurretCommands() {...
public TurretToggleMode toggleMode() {
return toggleModeProvider.get();
}
Note that @Inject
can also be used to put objects into the injection grid, provided they have either no parameters. For things with parameters, use an @Module
with @Provides
annotated method instead. To make this annotated class accessible, add an abstract method to the GlobalComponent
or applicable subcomponent (note that the constructor is still annotated, not the class).
Note:
@Singleton
is part of Java's builtin inject package (javax.inject.Singleton
), but is included as Dagger provides an implementation for it and as such relies on its use. This follows the same logic as@Inject
.
The @Singleton
annotation is extremely useful in robotics programming, mainly because it forces only one instance of a class to be created as an object. In this way, it's almost forcing a class to be static, but that isn't exactly what goes on, because there's still one object floating around that has been instantiated.
If we were to omit @Singleton
, the injection grid would create a new object of our subsystems each and every time one was requested (such as each time a command is called), which very likely will cause issues with WPILib. For this reason, every @Provides
annotated method should have @Singleton
on it. The reason for this is that it will create only one instance of the class, similar in effect to annotating the class itself with @Singleton
. We use it on the method because the method has @Provides
on it, and that's what Dagger interacts with first.
Other things should also include @Singleton, as listed below:
- Classes annotated with
@Inject
on a constructor - Any class that takes in a subsystem (ex
DriveTrain
) GlobalComponent
Note:
@Qualifier
is part of Java's builtin inject package (javax.inject.Qualifier
), but is included as Dagger provides an implementation for it and as such relies on its use. This follows the same logic as@Inject
and@Singleton
.
Explaining what a qualifier is and how it works is a bit beyond the scope of this tutorial/guide. In short, it's something you put on an annotation-creating interface that "provides an implementation of a bean type". For more information on this, look at the Java EE Docs and then this StackOverflow post.
For our purposes, the @Qualifier
annotation helps us differentiate between two objects of the same type but that have very different purposes. Take, for example, a DriveSide
. There are two of these, right and left, that are objects of the same DriveSide
class, but can't be mixed up, otherwise driving wouldn't work correctly. In a "typical" FRC setup, we'd just have two fields that store a right and left DriveSide
in DriveTrain
, but for our purposes that isn't a good idea and not ideal anyways. Instead, we can annotate each with a @Sided
annotation that takes a Side
enum as a parameter to distinguish between the two sides. That way, we can differentiate the two sides but also retain the Dagger injection schema.
There are many different possible implementations of @Qualifier
, but we typically have a @Sided
annotation with enum Side
and an @Input
annotation with enum Selector
. A sample version of @Sided
is below.
@Qualifier
public @interface Sided {
Side value();
public enum Side {
LEFT, RIGHT;
}
}
Note that this has an enum and specifies a value method. Also note the @interface
in the class header. This is not an annotation, and instead speficies that Sided
should be an annotation for use elsewhere. The enum could be another enum, and does not necessarily have to be inside the annotation interface.
Once an injection grid has been populated and set up as described above, anything put into it can be accessed. The @Inject
parameter automatically does this, as does the @GenerateCreator
/@Provides
combination. However, if you want to call/create a command or something of that nature, you'll need to go through GlobalComponent
Because the DaggerGlobalComponent
is created inside the robot class and stored, it can be accessed by calling methods on its stored field (for our example globalComponent
). Subcomponents, if set up like above, can be called (after being built) on their stored fields as well (here commandComponent
). Note the available methods are the ones in the respective Component
or Subcomponent
.
The button to command mapping should not be in the Robot class, mainly to reduce length and allow for easy changes to button maps. Instead, make a separate ButtonConfiguration
class or similar which is @Inject
annotated. Crucially this should pass in the joysticks that the buttons will be set onto, and the Builder for the CommandComponent
. We don't want to pass in the CommandComponent
itself because that hasn't been injected into the injection grid and physically cannot be injected. As such, make sure to call build()
on it before storing as an instance variable.
Buttons are simply a new instance of JoystickButton
, passing the Joystick joystick
and int button
for each one. Behavior is defined by the chained methods on that button, such as whenPressed(Command cmd)
. Put these in a method. Note: If this method is not called during some type of init()
, the buttons will not register.
Note that joysticks are also a good time to use a @Input
annotation or similar, with an enum for which joystick it is. Riviera Robotics has typically upheld a system of joystick mapping as below:
- 0: Driver Left
- 1: Driver Right
- 2: Codriver Left
- 3: Codriver Right
- 4: Driver Buttons
- 5: Codriver Buttons
Need some examples? Riviera Robotics started using this in 2019, so any robot code after that should include a working example of using Dagger, annotation processing, and dependency injection.
- Tutorials
- Getting the JDK
- Getting IntelliJ
- Learn Java
- Coding Your First Robot
- Basic Drive Systems
- Command-Based Programming
- 5818-lib
- Dependency Injection
- Formatting Guide
- Command Line Guide
- Quick References
- WPILib Documentation
- Git Book (External Resource)
- Robot Documentation
- Rogue Cephalopod (2017)