Service Customization

In many cases, customizing the behaviour of the Rover SDK will involve either creating custom implementations of Rover interfaces, or perhaps extending and overriding existing Rover objects. Thus, you will need to register your own factories into the SDK’s built-in dependency injection system to provide them to the rest of Rover.

See the Deep Dive section below for greater details.


Creating your Custom Implementation of a Rover Service

Start by creating your own implementation of a service from the Rover platform. We’ll use a hypothetical service here, RoverReplaceableService, for illustration.

class MyOverriddenRoverReplaceableService(
    private val neededRoverDependency: NeededRoverDependencyInterface
): RoverReplaceableService {
    /* overridden methods here */
}

Creating an Assembler

The way to expose your own implementation of an object to Rover is by creating your own custom Assembler akin to those found in the Rover modules. Assemblers register all of the components provided by a Rover module.

A subsequent Assembler can override registered factories from a former one, which is key for overriding Rover services.

Within your Assembler you’ll want to register a Factory closure to create an instance of your implementation within your custom Assembler.

Rover.initialize(
    /* ..., */
    // now we'll create a little anonymous Assembler (that is, a dependency injection
    // module) here to override a previous one.
    // Ensure that you add it in the list after the official Rover assembler that
    // originally adds the service you wish to override!
    object : Assembler {
        override fun assemble(container: Container) {
            container.register(
                // This is either Scope.Singleton or Scope.Transient.  Singletons
                // are remembered and the same instance re-returned on subsequent
                // resolutions of the same type.  Scope.Transient factories are
                // re-evaluated and the new instance returned every time.
                Scope.Singleton,
                RoverReplaceableService::class.java
            ) { resolver ->
                // This lambda body is the Factory.  Now we'll construct the
                // instance we want to provide for this type, and let it be returned:
                MyOverriddenRoverReplaceableService(
                    // the resolver allows you to look up any dependencies from
                    // the Rover object graph.
                    resolver.resolveSingletonOrFail(
                        NeededRoverDependencyInterface::class.java
                    )
                )
            }
        }
    }
)

If a given implementation of that object included with the Rover SDK allows itself to be extended, you may override that instead. Many Rover services are resolved through their interfaces, though, so in that case you’ll want to be sure to register your overridden version by the interface type.


Deep Dive: The Dependency Injection Framework

The Rover SDK’s internal dependency injection system serves to keep track of and construct all of the various components of the SDK, henceforth referred to as Services. Note that Rover refers to all of its internal objects as Services, be they stateful singletons or stateless transient view models.

The Rover.shared singleton is thusly a Dependency Injection Container, which globally holds the entire Rover object graph.

Each Rover module (e.g. Core, Experiences, Location, and so on) comes with an Assembler that will register() into the Container a Factory for each of the object types they are responsible for, possibly assigning them names to disambiguate differently configured variants of the same type.

The Factories are simply lambdas that return newly minted object instances of their types. The Factory lambdas are given a Resolver from which they can request any needed dependencies. They are lazily executed when something tries to resolve a dependency, and thus the entire object graph is instantiated lazily as needed.

The Assemblers themselves also can contain some startup logic to execute at registration time (that is, at app startup) to run any side effects, like setting themselves up to receive events from the Android framework or a library (e.g., location updates).