Skip to content

Dependency Injection

Boomaa23 edited this page Jun 4, 2020 · 1 revision

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.

@Component

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.

@Module

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

@Provides

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.

@Subcomponent

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

@Provided

Note: @Provided is not part of Dagger, and is instead part of apt-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.

@GenerateCreator

Note: @GenerateCreator is not part of Dagger, and is instead part of apt-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.

@Inject

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

@Singleton

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

@Qualifier

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.

Accessing the Injection Grid

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

Robot Class

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.

Joysticks and Buttons

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

Examples

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.