The AVS Device SDK introduced a new framework to simplify integration tasks named Manufactory. Manufactory makes it easier to add, remove, and customize SDK components without digging into the core SDK code. Version 1.21 of the SDK now includes a preview application that demonstrates building an Alexa application using Manufactory. This initial release of Manufactory gives you an idea of what's to come in the next few SDK releases. It includes a preview sample application that will evolve over time, and should stabilize by the summer of 2021. This blog outlines how Manufactory works, the reasoning behind the changes, and provides a glimpse of future AVS Device SDK changes.
Over the last three years, the SDK has grown significantly. Version 1.0, released in August 2017, supported five capability agents and was less the 150k lines of code. Version 1.20, released in June 2020, supports 14 Capability Agents and is over 480k lines of code. With this growth, we realized that we needed to make customization and integration of the SDK easier.
The SDK is adaptable. You are free to add, remove, or customize components to suit your needs. Currently, the SDK uses dependency injection to pass objects created by the application into the SDK core. The default client consumes these objects and orchestrates the distribution throughout the SDK. In version 1.20, the DefaultClient::create() function takes 58 input parameters. The application must create and initialize all of these objects, which leads to increased complexity and integration challenges.
The SDK consists of the following categories of components.
Manufactory allows you to select which components you want to add to your device and simplifies the task of adding and removing features or replacing the sample implementations. Componentization is a cleaner way to customize device configuration and improves maintainability by making it easier to upgrade core SDK components. For example, you can update the device SDK core components without having to modify your application.
We started adding Manufactory to the SDK in version 1.20. With Manufactory, you don't need to know the details of the component you want to use – you simply request one from Manufactory and use it. Manufactory creates and initializes objects, handles dependencies and manages the lifecycle of these objects. It automates and abstracts this logic from you, eliminating any complex logic required to initialize components in the application. The diagram below illustrates the basic structure of Manufactory.
Figure 1 Structure of manufactory
When you create a Manufactory, you pass it a component. A component is a collection of factories used to create the objects supporting the application. Manufactory creates and manages these objects.
Manufactory exposes a get<>()method. The application can request an object by invoking get<>() with the desired object type. To do this, Manufactory first checks its object cache to see if it has the proper type to return. If it doesn't already have one in its cache, Manufactory invokes the appropriate factory to create the object.
To manage an object with Manufactory, the defining class must:
class AudioPlayer {
public:
static std::shared_ptr<AudioPlayer> createAudioPlayer();
};
Manufactory relies on C++ templates for type checking and resolving dependencies. This means Components must specify the C++ Types they export. A Component is a templated Type unique to the Types in its declaration. When declaring a Component, specify all the Types the Component will export to Manufactory—for example, the following SampleAlexaComponent is a unique Type of a Component that exports an AudioPlayer, SpeakerInterface, UIManager, ConfigurationNode and an AuthDelegateInterface.
SampleAlexaComponent = acsdkManufactory::Component<
std::shared_ptr<AudioPlayer>,
std::shared_ptr<SpeakerInterface>,
std::shared_ptr<UIManage>,
std::shared_ptr<ConfigurationNode>,
std::shared_ptr<AuthDelegateInterface>>
A factory must be available to the Component to create each of the declared types, or compilation will fail. The Component can, however, hide some types. It can have factories for types used internally that are not exported.
You can build Components from other components, so a parent component can create objects and inject them into the child components that need them. For this process, Components can specify Imports. These imports can be used as input parameters to other factory functions, but they must be available from a parent Component.
The following example shows an AuthorizationDelegateComponent that exports an AuthDelegateInterface, but requires a CBLAuthRequesterInterface, ConfigurationNode, and HttpPostInterface. The AuthorizationDelegateComponent can't be created independently, but it can be included as a part of a parent component that can fulfill these import dependencies.
AuthorizationDelegateComponent = acsdkManufactory::Component<
std::shared_ptr<AuthDelegateInterface>,
Import<std::shared_ptr<CBLAuthRequesterInterface>>,
Import<std::shared_ptr<ConfigurationNode>>,
Import<std::unique_ptr< HttpPostInterface>>,
The ComponentAccumulator class builds Components. It has functions to add various types of objects. Each of these functions returns a ComponentAccumulator so they can be chained together. In the end, the ComponentAccumulator is converted into a Component that is passed to Manufactory. The example below shows a component that satisfies the exports declared by the SampleAlexaComponent declared earlier.
SampleAlexaComponent getComponent(initParams) {
return ComponentAccumulator<>()
.addPrimaryFactory(getAlexaClientSDKInit(initParams))
.addComponent(acsdkAudioPlayer::getComponent())
.addComponent(speakerManager::getComponent())
.addComponent(acsdkShared::getComponent())
.addComponent(acsdkAuthorizationDelegate::getComponent())
.addUnloadableFactory(createCBLAuthRequesterInterface)
.addRetainedFactory(UIManager::create );}
In this example, we create the SampleAlexaComponent Component. To do this, several different types of factories and components are added together using the ComponentAccumulator. Descriptions of these items are listed below.
Note that other 'add' functions are available to add non-shareable objects, object instances, and more. For more details, see the AVS Device SDK on GitHub and the API references.
To create a Manufactory, call the Manufactory create<>() method and pass in the component you wish to use. The only catch is the Manufactory must declare all the Types it exports, and all those Types must be available for export by the Component used to create that Manufactory. The declaration below is almost identical to the Component declaration, except we chose not to export the authentication delegate.
using SampleAlexaManufactory = acsdkManufactory::Manufactory<
std::shared_ptr<AudioPlayer>,
std::shared_ptr<SpeakerInterface>,
std::shared_ptr<UIManage>,
std::shared_ptr<ConfigurationNode>>
manufactory = SampleAlexaManufactory::create(SampleAlexaComponent);
Manufactory creates and manages the objects defined in the components it houses. When the application needs an object, it calls the appropriate templated get<>() function on the manufactory instance to acquire a pointer to the object. Manufactory either returns a pointer to an existing object or creates a new one—whatever is appropriate for that object type. In the example below, we acquired a pointer to an instance of the UImanager and use that instance to configure notifications settings.
uiManager = manufactory->get<std::shared_ptr<UIManager>>();
uiManager->configureSettingsNotifications(params);
Manufactory allows you to pick and choose which components you want to use in your application. You can start with the AlexaClientComponent in the Preview Alexa Client of the AVS Device SDK and add or remove optional components. Some optional components are wrapped in an #ifdef tag in the Preview Alexa Client, allowing addition of the component code with compiler flags demonstrating this flexibility.
It's also straightforward to replace an implementation. For example, you might replace the authorization delegate to use an application-based authentication instead of the Code Based Authentication. To do this, simply replace the factory method passed to addFactory(). As long as it supports the same AuthDelegate interface, no other code in the application needs to change.
The only change required to support MyNewUIManager, other than writing MyNewUIManager, is changing the factory passed to the component through the addRetainedFactory() method. Nothing else in the application needs to change to adapt to the new implementation.
Manufactory is a significant change to the AVS Device SDK. We added some of these changes in version 1.20 of the SDK. To minimize churn over the next several releases, we added a preview client. The Preview Alexa Client, found under avs-device-sdk/applications, gives you a sneak peek into the Manufactory implementation. Note that the Preview Alexa Client will change dramatically as the implementation evolves. Consider the preview client as an 'alpha' release for you to follow along. As of this writing, it's not recommended to use the previewAlexaClient for production. If you want to build the preview application in version 1.21, Run CMake as you usually would then run 'make PreviewAlexaClient'.
The current Sample Application relies on the default client. The 58 input parameters to the DefaultClient::create()function are initialized in the sample application making the sampleApplication.cpp file 1769 lines long. From version 1.20 going forward, this Sample Application will not change much. We will only update the Sample Application to support new Alexa features. We will not make any more changes to the Sample Application to support Manufactory. The changes to support Manufactory are going to be made in the default client.
Figure 2 Legacy application
In the transition phase, there will be both a Sample application, and its replacement, Preview Alexa Client. The sample application will behave the same way as the legacy application. However, the new Preview Application will use Manufactory to initialize objects. It will pass the Manufactory to a new overloaded create() function in the Default client, which will get the objects from the manufactory and pass them to the rest of the application.
Figure 3 Transition phase
Ultimately, the sample app will be eliminated and default client will be replaced by a simpler ‘Alexa Client’ with a single run() function taking one parameter, Manufactory. Manufactory will also be available to the Application to allow access to objects for customization purposes and to interface with any external device components.
Figure 4 Future Vision
Use the Preview Application to explore and follow along as the SDK evolves. While the actual implementation of Manufactory is quite complex, using it is relatively straightforward. You simply pass your objects to Manufactory through components and then use them. As we streamline the interfaces and evolve the implementation, customizing the SDK will become even more manageable. Over the next several releases, you will see additional changes to add modularization and customization to the SDK until we stabilize the implementation in mid-2021. So, don't worry, you don't have to consume any of it yet. The Preview Application is there for you to explore, and we will maintain the current Sample Application as-is for all of 2021. The Preview Application will become the primary Sample Application in 2022.
The AVS Device SDK team is dedicated to helping you easily build Alexa devices. We do this by continuing to iterate on a robust solution that is reliable, customizable and easy to maintain. We look forward to the creative and innovative ways you integrate Alexa into your products. Feel free to provide feedback through your HAQM Point of contact, Github Issues or me @sschonstal.