How to Build a Multimodal Alexa Skill

Create More Visuals With Less

Table of Contents

Since we now have a functioning document for our no context launch request, let’s make our other screens. In this section you will complete the visuals for both the countdown and birthday screens while learning how to create a reusable layout and APL package. In addition, we will use the video component to deliver a special video on your birthday.

1. Layouts

So far we have made this screen:

And we want to make these screens:

See the similarities? We can already make the countdown screen with our current document! And, we can almost make the Birthday screen. There are only two differences between the birthday screen and our current launch screen. The birthdayt screen has a video instead of an image, and the background asset is different. Let’s start by optimizing our current document using layouts.

Layouts are composite components; they are components made out of other components and layouts. We have already been using layouts (responsive components are layouts defined by Amazon in the APL Package, "alexa-Layouts"), but now it is time to define our own layout based on the patterns we see in our screens for our experience.

Layouts are also defined in JSON. They have a description, parameters accepted, and an items section which is composed of other components. The parameters can have default values in case there is no parameter value provided, as well as a specific type (or "any"). We can extract the text parts of our document and create a layout out of it. Let’s do so now.

A. Open the APL authoring tool.

B. Add the layout of the repeated patterns to the layouts section of the document. The reusable text layout looks like:

"cakeWalkText": {
    "description": "A basic text layout with start, middle and end.",
    "parameters":[
        {
            "name": "startText",
            "type": "string"
        },
        {
            "name": "middleText",
            "type": "string"
        },
        {
            "name": "endText",
            "type": "string"
        }
    ],
    "items": [
        {
            "type": "Container",
            "items": [
                {
                    "type": "Text",
                    "style": "bigText",
                    "text": "${startText}"
                },
                {
                    "type": "Text",
                    "style": "bigText",
                    "text": "${middleText}"
                },
                {
                    "type": "Text",
                    "style": "bigText",
                    "text": "${endText}"
                }
            ]
        }
    ]
}

This can simplify our already existing document and will allow us to get rid of duplicate JSON. Note, there is one difference here between the text components for Echo spot and the rest of the devices. We will resolve this with a conditional statement inside of the paddingTop property instead of on the container. This greatly simplifies our whole JSON document!

C. Remove the redundant code (all of the duplicate code moved to the layout on each section) and replace it with the new "cakeWalkText" layout. We would use this layout like so:

{
    "type": "cakeWalkText",
    "startText":"${payload.text.start}",
    "middleText":"${payload.text.middle}",
    "endText":"${payload.text.end}"
}

When we remove the redundant code and replace it with references to our layout, we get:

{
    "type": "APL",
    "version": "1.1",
    "settings": {},
    "theme": "dark",
    "import": [
        {
            "name": "alexa-layouts",
            "version": "1.1.0"
        }
    ],
    "resources": [],
    "styles": {
        "bigText": {
            "values": [
                {
                    "fontSize": "72dp",
                    "color": "black",
                    "textAlign": "center"
                }
            ]
        }
    },
    "onMount": [],
    "graphics": {},
    "commands": {},
    "layouts": {
        "cakeWalkText": {
            "description": "A basic text layout with start, middle and end.",
            "parameters":[
                {
                    "name": "startText",
                    "type": "string"
                },
                {
                    "name": "middleText",
                    "type": "string"
                },
                {
                    "name": "endText",
                    "type": "string"
                }
            ],
            "items": [
                {
                    "type": "Container",
                    "items": [
                        {
                            "type": "Text",
                            "style": "bigText",
                            "paddingTop":"${@viewportProfile == @hubRoundSmall ? 75dp : 0dp}",
                            "text": "${startText}"
                        },
                        {
                            "type": "Text",
                            "style": "bigText",
                            "text": "${middleText}"
                        },
                        {
                            "type": "Text",
                            "style": "bigText",
                            "text": "${endText}"
                        }
                    ]
                }
            ]
        }
    },
    "mainTemplate": {
        "parameters": [
            "payload"
        ],
        "items": [
            {
                "type": "Container",
                "items": [
                    {
                        "type": "AlexaBackground",
                        "backgroundImageSource": "${payload.assets.backgroundURL}"
                    },
                    {
                        "type": "cakeWalkText",
                        "startText":"${payload.text.start}",
                        "middleText":"${payload.text.middle}",
                        "endText":"${payload.text.end}"
                    },
                    {
                        "type": "AlexaImage",
                        "alignSelf": "center",
                        "imageSource": "${payload.assets.cake}",
                        "imageRoundedCorner": false,
                        "imageScale": "best-fill",
                        "imageHeight":"40vh",
                        "imageAspectRatio": "square",
                        "imageBlurredBackground": false
                    }
                ],
                "height": "100%",
                "width": "100%",
                "when": "${@viewportProfile != @hubRoundSmall}"
            },
            {
                "type": "Container",
                "items": [
                    {
                        "type": "AlexaBackground",
                        "backgroundImageSource": "${payload.assets.backgroundURL}"
                    },
                    {
                        "type": "cakeWalkText",
                        "startText":"${payload.text.start}",
                        "middleText":"${payload.text.middle}",
                        "endText":"${payload.text.end}"
                    }
                ],
                "height": "100%",
                "width": "100%",
                "when": "${@viewportProfile == @hubRoundSmall}"
            }
        ]
    }
}

But wait…​ This isn’t that much simpler- it looks longer now! Let’s simplify our document further by breaking out the layout and styles into it’s own APL package.

2. Hosting Your Own APL package

Once you have your components rendering and the launch screen looks the same, it is time to host our layout so it can be used in more than one document. Layouts, styles, and resources can all be hosted in an APL package. In fact, an APL package has the same format of an APL document except, there is no mainTemplate. This is a great way to share resources, styles, or your own custom responsive components or UI patterns across multiple APL documents. You can even create your own documents to share with the rest of the Alexa developer community!

We want to host both our style and our layout. To do so we will use our S3 bucket on our backend. Unfortunately, since we are using the Alexa hosted environment, we cannot modify permissions on the S3 provision we are given. Alexa devices and simulators need the header, Access-Control-Allow-Origin to be set and allow *.amazon.com. To learn more about Cross-Origin Resource Sharing, check out the tech docs. Also, the link must be public which we cannot do with Alexa hosted. But for this exercise, we will use this GitHub link to host our JSON APL package. Note: Github supports CORS on all domains.

If you are using another service to host your assets that service must also send the appropriate headers.

Our package will be just the reusable set of properties from our APL document. This includes the layouts and the styles. We also will need the import section because we rely on alexa-layouts in order to render our layout. Imports are transitive in APL. This is basically everything except for the mainTemplate. Our package will be:

{
    "type": "APL",
    "version": "1.1",
    "settings": {},
    "theme": "dark",
    "import": [
        {
            "name": "alexa-layouts",
            "version": "1.1.0"
        }
    ],
    "resources": [],
    "styles": {
        "bigText": {
            "values": [
                {
                    "fontSize": "72dp",
                    "color": "black",
                    "textAlign": "center"
                }
            ]
        }
    },
    "onMount": [],
    "graphics": {},
    "commands": {},
    "layouts": {
        "cakeWalkText": {
            "description": "A basic text layout with start, middle and end.",
            "parameters":[
                {
                    "name": "startText",
                    "type": "string"
                },
                {
                    "name": "middleText",
                    "type": "string"
                },
                {
                    "name": "endText",
                    "type": "string"
                }
            ],
            "items": [
                {
                    "type": "Container",
                    "items": [
                        {
                            "type": "Text",
                            "style": "bigText",
                            "text": "${startText}"
                        },
                        {
                            "type": "Text",
                            "style": "bigText",
                            "text": "${middleText}"
                        },
                        {
                            "type": "Text",
                            "style": "bigText",
                            "text": "${endText}"
                        }
                    ]
                }
            ]
        }
    }
}

Now in our main document, we can remove everything except for the mainTemplate blob, and add in a new import for our package. In the authoring tool, we can use this public link to test.

A. Add this import to your document:

{
    "name": "my-cakewalk-apl-package",
    "version": "1.0",
    "source": "https://raw.githubusercontent.com/alexa/skill-sample-nodejs-first-apl-skill/master/modules/code/module4/documents/my-cakewalk-apl-package.json"
}

With this import, we can now reference the values from our custom style (bigText) and layout (cakeWalkText). Our document is now significantly smaller and easier to read since we can remove our layout and style:

{
    "type": "APL",
    "version": "1.1",
    "settings": {},
    "theme": "dark",
    "import": [
        {
            "name": "my-cakewalk-apl-package",
            "version": "1.0",
            "source": "https://raw.githubusercontent.com/alexa/skill-sample-nodejs-first-apl-skill/master/modules/code/module4/documents/my-cakewalk-apl-package.json"
        },
        {
            "name": "alexa-layouts",
            "version": "1.1.0"
        }
    ],
    "resources": [],
    "styles": {},
    "onMount": [],
    "graphics": {},
    "commands": {},
    "layouts": {},
    "mainTemplate": {
        "parameters": [
            "payload"
        ],
        "items": [
            {
                "type": "Container",
                "items": [
                    {
                        "type": "AlexaBackground",
                        "backgroundImageSource": "${payload.assets.backgroundURL}"
                    },
                    {
                        "type": "cakeWalkText",
                        "startText":"${payload.text.start}",
                        "middleText":"${payload.text.middle}",
                        "endText":"${payload.text.end}"
                    },
                    {
                        "type": "AlexaImage",
                        "alignSelf": "center",
                        "imageSource": "${payload.assets.cake}",
                        "imageRoundedCorner": false,
                        "imageScale": "best-fill",
                        "imageHeight":"40vh",
                        "imageAspectRatio": "square",
                        "imageBlurredBackground": false
                    }
                ],
                "height": "100%",
                "width": "100%",
                "when": "${@viewportProfile != @hubRoundSmall}"
            },
            {
                "type": "Container",
                "paddingTop": "75dp",
                "items": [
                    {
                        "type": "AlexaBackground",
                        "backgroundImageSource": "${payload.assets.backgroundURL}"
                    },
                    {
                        "type": "cakeWalkText",
                        "startText":"${payload.text.start}",
                        "middleText":"${payload.text.middle}",
                        "endText":"${payload.text.end}"
                    }
                ],
                "height": "100%",
                "width": "100%",
                "when": "${@viewportProfile == @hubRoundSmall}"
            }
        ]
    }
}

Why are we still importing alexa-layouts if imports are transitive? In general, it’s best to explicitly declare the dependencies you are directly using. If the cakewalk apl package decides to no longer use alexa-layouts, your document will break! Therefore, your document also has a dependency on alexa-layouts

B. Open the developer portal to your Cake walk skill.

C. Save over your current launchDocument.json with this new document in the code tab of your skill.

Now, let’s make our final document.

3. Adding a Special Birthday Video

Our birthdayDocument will use a full screen video instead of the image component with the text removed, making sure to provide the same layout on a spot. The video component has a simple structure for our use case.

A. In the code tab, create a new document called birthdayDocument.json and make it a copy of our old document.

B. Replace the image component in your birthdayDocument.json with the following video component inside a container.

{
    "type": "Container",
    "paddingTop":"3vh",
    "alignItems": "center",
    "items": [{
        "type": "Video",
        "height": "85vh",
        "width":"90vw",
        "source": "${payload.assets.video}",
        "autoplay": true
    }]
}

We want to add this container so that we can center the component in our APL Document. The padding is so we see some of the background at the top of the viewport. Notice, we also removed the text object on this first container! We want the video to be front and center. The video we will be using is of an animated birthday cake with Alexa Singing in the background. It looks like this:

This video really wants to be played fullscreen which is why we made our height 85% of the viewport height and width 90% of the viewport. However, now our text is no longer wanted when we display the video.

C. Remove the CakeWalkText component in the first container (when ${@viewportProfile != @hubRoundSmall}).

D. Save this in your skill code as a new file, birthdayDocument.json.

4. Wiring up the Backend

Let’s swap back over to the index.js file and wire up our other APL screens.

The only difference in our current launch document and our known birthday document is the content. Let’s start to modify our HasBirthdayLaunchRequestHandler to conditionally use our launchDocument.json file or our birthdayDocument.json file depending on the situation. We want it to look like this:

A. We want to avoid duplicating code, so let's make a helper function to get the background image based upon our key. This will also be used to fetch a new background image for the alternate document. In addition, we will use it to contain our device screen size to asset size logic. Add this helper function to your index.js anywhere outside of another function or object:

function getBackgroundURL(handlerInput, fileNamePrefix) {
    const viewportProfile = Alexa.getViewportProfile(handlerInput.requestEnvelope);
    const backgroundKey = viewportProfile === 'TV-LANDSCAPE-XLARGE' ? "Media/"+fileNamePrefix+"_1920x1080.png" : "Media/"+fileNamePrefix+"_1280x800.png";
    return util.getS3PreSignedUrl(backgroundKey);
}

This is beneficial since it provides a single place where our assumptions of filenames live. If we want to add more viewport profile detection or we decide to change hosting from our S3 bucket, we have a single place to do so.

B. We now need to refactor our LaunchRequestHandler.handle() code to use the new function. Our new data payload will now have a new value for the backgroundURL key:

backgroundURL: getBackgroundURL(handlerInput, "lights")

And you can delete the lines:

const viewportProfile = Alexa.getViewportProfile(handlerInput.requestEnvelope);
const backgroundKey = viewportProfile === 'TV-LANDSCAPE-XLARGE' ? "Media/lights_1920x1080.png" : "Media/lights_1280x800.png";

C. Since we will be using the same launch doc, we already have the import to the JSON representing our document. Add a block in HasBirthdayLaunchRequestHandler similar to our LaunchRequestHandler directly before the return statement.

// Add APL directive to response
if (Alexa.getSupportedInterfaces(handlerInput.requestEnvelope)['Alexa.Presentation.APL']) {
    // Create Render Directive
}

D. We are going to define a variable to use in our data payload, numberDaysString. This is a variable string which will be something like "1 day" or "234 days". You can represent this by the expression:

const numberDaysString = diffDays === 1 ? "1 day": diffDays + " days";

Add this variable just below our // Add APL directive to response comment.

E. Now, underneath // Create Render Directive, add our directive:

handlerInput.responseBuilder.addDirective({
    type: 'Alexa.Presentation.APL.RenderDocument',
    document: launchDocument,
    datasources: {
        text: {
            type: 'object',
            start: "Your Birthday",
            middle: "is in",
            end: numberDaysString
        },
        assets: {
            cake: util.getS3PreSignedUrl('Media/alexaCake_960x960.png'),
            backgroundURL: getBackgroundURL(handlerInput, "lights")
        }
    }
});

Notice we are now using the numberDaysString in our data payload, so this will change based on our input and the day of the year. In addition, we are making use of our helper function to construct the lights url to fetch the proper signed URL.

F. Test it out! You will have to go through the whole flow to enter your month, day, and year of birth before you can see this screen.

5. Wiring Up the Birthday Video

A. Once you have verified this is working, let’s build the other path for when it is your birthday. This will make use of our new document, birthdayDocument.json, so let’s start by importing that as birthdayDocument at the top.

const birthdayDocument = require('./documents/birthdayDocument.json');

B. Now, we will need to add some conditional logic to our new code to switch between APL documents depending on if it is our birthday or not. Underneath the comment, // Create Render Directive, inside the HasBirthdayLaunchRequestHandler handle method, add

if (currentDate.getTime() !== nextBirthday) {
    //TODO Move the old directive here.
} else {
    //TODO Write a birthday specific directive here.
}

C. Cut the handlerInput.responseBuilder.addDirectiveReplace({…​}) you added in the last section and replace the comment, //TODO Move the old directive here. with this.

D. Inside the else block we can add our new directive using the birthdayDocument you imported above. We will be using the "confetti" picture. Add this full directive in the else block:

// Create Render Directive
handlerInput.responseBuilder.addDirective({
    type: 'Alexa.Presentation.APL.RenderDocument',
    document: birthdayDocument,
    datasources: {
        text: {
            type: 'object',
            start: "Happy Birthday!",
            middle: "From,",
            end: "Alexa <3"
        },
        assets: {
            video: "https://public-pics-muoio.s3.amazonaws.com/video/Amazon_Cake.mp4",
            backgroundURL: getBackgroundURL(handlerInput, "confetti")
        }
    }
});

This new directive differs in the data provided for the text object, the image is replaced with a video, and the background uses the confetti asset. We still need to input start, middle, and end text because our variant for the Hub Round Small device uses this.

E. Now, Test your skill. Clear your user data in S3 and lie so today is your birthday! If you aren’t lying, well…​ Happy Birthday! :)

When this is working, you can go to the final module to learn about commands.

Complete code in Github