APL Integration¶
AlexaVoxCraft provides comprehensive support for Alexa Presentation Language (APL), enabling rich visual interfaces with synchronized voice and touch interactions through a fluent document builder API.
🎯 Trivia Skill Examples: All code examples demonstrate building visual trivia questions with multiple-choice answers, score displays, and interactive touch/voice responses.
Features¶
Fluent Document Builder: Intuitive API for creating complex APL documents
Multi-Slide Presentations: Sequential slide management with voice synchronization
Interactive Components: Touch-enabled elements with event handling
Voice-Touch Sync: Coordinated audio and visual experiences
Custom Layouts: Reusable layout components and transformers
- :iphone: Device Adaptive: Responsive design for different screen sizes
Basic Usage¶
Document Builder Setup¶
// Register DocumentBuilder service
services.AddScoped<IVisualBuilder, VisualBuilder>();
// In your handler
public class MyHandler : IRequestHandler<LaunchRequest>
{
private readonly IVisualBuilder _visualBuilder;
public MyHandler(IVisualBuilder visualBuilder)
{
_visualBuilder = visualBuilder;
}
public async Task<SkillResponse> Handle(IHandlerInput input, CancellationToken cancellationToken)
{
// Create APL document
var (renderDirective, executeDirective) = _visualBuilder
.AddTextSlide("Welcome to Trivia Challenge!", "Say 'start' to begin")
.GetDirectives();
return await input.ResponseBuilder
.AddDirective(renderDirective)
.AddDirective(executeDirective)
.Speak("Welcome to Trivia Challenge! Ready to start?")
.GetResponse(cancellationToken);
}
}
Simple Text Slide¶
var (renderDirective, executeDirective) = _visualBuilder
.AddTextSlide("Game Over! Your score was 4 out of 5.")
.GetDirectives();
input.ResponseBuilder
.AddDirective(renderDirective)
.AddDirective(executeDirective);
Document Builder API¶
Text Slides¶
Create informational slides with speech synchronization:
// Basic text slide
_visualBuilder.AddTextSlide("Welcome to Trivia Challenge!");
// Text slide with footer hint
_visualBuilder.AddTextSlide(
speechText: "Correct! That's the right answer.",
footerText: "Say 'next' for the next question"
);
// Multiple text slides
var (renderDirective, executeDirective) = _visualBuilder
.AddTextSlide("Round 1 Complete!", "show me the scores")
.AddTextSlide($"Your score: {currentScore} out of {totalQuestions}", "ready for round 2?")
.GetDirectives();
Question Slides¶
Interactive multiple-choice questions:
var questionText = "What is the capital of France?";
var choices = new[] { "London", "Berlin", "Paris", "Madrid" };
var (renderDirective, executeDirective) = _visualBuilder
.AddQuestionSlide(
speechText: $"Question 1: {questionText}",
questionText: questionText,
choices: choices,
footerText: "Select an answer or say the number"
)
.GetDirectives();
Complete Visual Builder Example¶
Generic implementation for skill visual interfaces:
public class VisualBuilder : IVisualBuilder
{
private readonly RenderDocumentDirective _renderDocumentDirective;
private readonly ExecuteCommandsDirective _executeCommandsDirective;
private readonly APLDocument _document;
private readonly List<APLCommand> _commands;
private readonly Dictionary<string, Layout> _layouts;
private readonly List<APLComponent> _components;
private readonly Dictionary<string, object> _properties;
private readonly List<APLTransformer> _transformers;
public VisualBuilder()
{
_commands = new List<APLCommand>();
_layouts = new Dictionary<string, Layout>();
_components = new List<APLComponent>();
_properties = new Dictionary<string, object>();
_transformers = new List<APLTransformer>();
// Create main document structure
_document = new APLDocument(APLDocumentVersion.V2022_2)
{
Theme = ViewportTheme.Dark,
Imports = new List<Import> { Import.AlexaLayouts },
Resources = Resources.GetSkillResources(),
Layouts = _layouts,
MainTemplate = new Layout(new Container
{
Width = "100vw",
Height = "100vh",
Items = new List<APLComponent>
{
new Pager
{
Id = "skillPager",
Navigation = "wrap",
Height = "100%",
Width = "100%",
Items = _components
}
}
})
};
// Setup data sources and directives
_renderDocumentDirective = new RenderDocumentDirective
{
Token = "skillToken",
DataSources = new Dictionary<string, APLDataSource>
{
{
"skillData", new ObjectDataSource
{
ObjectId = "skillData",
TopLevelData = new Dictionary<string, object>
{
{ "backgroundImage", "https://example.com/background.jpg" },
{ "titleText", "Trivia Challenge" },
{ "logoUrl", "https://example.com/logo.png" }
},
Properties = _properties,
Transformers = _transformers
}
}
},
Document = _document
};
}
public IVisualBuilder AddTextSlide(string speechText, string? footerText = null)
{
_layouts["TextSlide"] = Layouts.TextSlideLayout;
var pageId = $"page{_components.Count + 1}";
AddSpeechCommand(pageId);
_components.Add(new CustomComponent("TextSlide")
{
Properties = new Dictionary<string, object>
{
{ "backgroundColor", "" },
{ "backgroundImageSource", "${payload.skillData.backgroundImage}" },
{ "title", "${payload.skillData.titleText}" },
{ "logoUrl", "${payload.skillData.logoUrl}" },
{ "text", $"${{payload.skillData.properties.{pageId}.speechText}}" },
{ "speech", $"${{payload.skillData.properties.{pageId}.speechTextAsSpeech}}" },
{ "hintText", $"${{payload.skillData.properties.{pageId}.hintText}}" },
{ "speechId", pageId }
}
});
var items = new Dictionary<string, object>
{
{ "speechText", $"<speak>{speechText}</speak>" }
};
if (!string.IsNullOrWhiteSpace(footerText))
{
items["footerHintText"] = footerText;
_transformers.Add(new APLTransformer("textToHint", $"{pageId}.footerHintText", "hintText"));
}
_properties[pageId] = items;
_transformers.Add(new APLTransformer("ssmlToSpeech", $"{pageId}.speechText", "speechTextAsSpeech"));
return this;
}
public IVisualBuilder AddQuestionSlide(string speechText, string questionText,
IEnumerable<string> choices, string? footerText = null)
{
_layouts["MultipleChoiceItem"] = Layouts.MultipleChoiceItemLayout;
_layouts["MultipleChoice"] = Layouts.MultipleChoiceLayout;
var pageId = $"page{_components.Count + 1}";
AddSpeechCommand(pageId);
_components.Add(new CustomComponent("MultipleChoice")
{
Properties = new Dictionary<string, object>
{
{ "backgroundColor", "" },
{ "backgroundImageSource", "${payload.skillData.backgroundImage}" },
{ "title", "${payload.skillData.titleText}" },
{ "logoUrl", "${payload.skillData.logoUrl}" },
{ "text", $"${{payload.skillData.properties.{pageId}.speechText}}" },
{ "speech", $"${{payload.skillData.properties.{pageId}.speechTextAsSpeech}}" },
{ "hintText", $"${{payload.skillData.properties.{pageId}.hintText}}" },
{ "primaryText", $"${{payload.skillData.properties.{pageId}.primaryText}}" },
{ "choices", $"${{payload.skillData.properties.{pageId}.choices}}" },
{ "choiceListType", $"${{payload.skillData.properties.{pageId}.choiceListType}}" },
{ "speechId", pageId }
}
});
var items = new Dictionary<string, object>
{
{ "speechText", $"<speak>{speechText}</speak>" },
{ "primaryText", questionText },
{ "choices", choices },
{ "choiceListType", "ordinal" }
};
if (!string.IsNullOrWhiteSpace(footerText))
{
items["footerHintText"] = footerText;
_transformers.Add(new APLTransformer("textToHint", $"{pageId}.footerHintText", "hintText"));
}
_properties[pageId] = items;
_transformers.Add(new APLTransformer("ssmlToSpeech", $"{pageId}.speechText", "speechTextAsSpeech"));
return this;
}
private void AddSpeechCommand(string componentId)
{
if (_commands.Any())
{
_commands.Add(new SetPage
{
ComponentId = "skillPager",
Position = SetPagePosition.Relative,
DelayMilliseconds = 100,
Value = 1
});
}
_commands.Add(new SpeakItem { ComponentId = componentId });
}
public (RenderDocumentDirective?, ExecuteCommandsDirective?) GetDirectives()
{
_executeCommandsDirective.Commands = new List<APLCommand>
{
new Sequential { Commands = _commands }
};
return (_renderDocumentDirective, _executeCommandsDirective);
}
}
Layout Components¶
Text Slide Layout¶
public static class Layouts
{
public static Layout TextSlideLayout => new Layout(
new Container
{
Width = "100vw",
Height = "100vh",
AlignItems = "center",
JustifyContent = "center",
Items = new List<APLComponent>
{
new Image
{
Source = "${backgroundImageSource}",
Scale = "bestFill",
Width = "100vw",
Height = "100vh",
Position = "absolute"
},
new Container
{
Width = "100vw",
Height = "100vh",
AlignItems = "center",
JustifyContent = "spaceBetween",
PaddingTop = "@marginTop",
PaddingBottom = "@marginBottom",
Items = new List<APLComponent>
{
new Header(),
new Text
{
Text = "${text}",
Style = "textStyleBody",
TextAlign = "center",
MaxLines = 8
},
new Footer()
}
}
}
})
{
Description = "Text slide with background image and header",
Parameters = new List<Parameter>
{
"backgroundColor",
"backgroundImageSource",
"title",
"logoUrl",
"text",
"hintText"
}
};
public static Layout MultipleChoiceLayout => new Layout(
new Container
{
// Multiple choice layout implementation
})
{
Description = "Interactive multiple choice question",
Parameters = new List<Parameter>
{
"backgroundColor",
"backgroundImageSource",
"title",
"logoUrl",
"primaryText",
"choices",
"choiceListType",
"hintText"
}
};
}
Interactive Events¶
Touch Event Handling¶
public class AnswerHandler : IRequestHandler<UserEventRequest>
{
public Task<bool> CanHandle(IHandlerInput input, CancellationToken cancellationToken = default)
{
return Task.FromResult(input.RequestEnvelope.Request is UserEventRequest);
}
public async Task<SkillResponse> Handle(IHandlerInput input, CancellationToken cancellationToken)
{
var userEvent = (UserEventRequest)input.RequestEnvelope.Request;
var answer = GetAnswerFromEvent(userEvent.Arguments);
// Process the touch-selected answer
var isCorrect = await _gameManager.UpdateScore(answer, cancellationToken);
// Continue with response...
return await BuildResponse(isCorrect, input, cancellationToken);
}
private int GetAnswerFromEvent(object[]? arguments)
{
if (arguments == null || arguments.Length <= 1)
return 0;
// Check for "answer" event type
if (!arguments.Any(arg => arg is string str && str == "answer"))
return 0;
// Extract answer value (1-4)
if (arguments[1] is int answer && answer is > 0 and <= 4)
return answer;
return 0;
}
}
APL Commands¶
// Sequential commands for slide transitions
var commands = new List<APLCommand>
{
new SetPage
{
ComponentId = "pagerId",
Position = SetPagePosition.Relative,
Value = 1,
DelayMilliseconds = 100
},
new SpeakItem
{
ComponentId = "questionSlide"
},
new Parallel
{
Commands = new List<APLCommand>
{
new AnimateItem
{
ComponentId = "choices",
Duration = 1000,
Value = new List<AnimatedProperty>
{
new AnimatedProperty { Property = "opacity", To = 1 }
}
}
}
}
};
Data Sources and Transformers¶
Object Data Source¶
var dataSource = new ObjectDataSource
{
ObjectId = "triviaData",
TopLevelData = new Dictionary<string, object>
{
{ "backgroundImage", "https://example.com/bg.jpg" },
{ "titleText", "Trivia Challenge" },
{ "logoUrl", "https://example.com/logo.png" }
},
Properties = _properties,
Transformers = _transformers
};
Data Transformers¶
// Transform SSML to speech-ready text
_transformers.Add(new APLTransformer(
"ssmlToSpeech",
$"{pageId}.speechText",
"speechTextAsSpeech"
));
// Transform footer text to hint format
_transformers.Add(new APLTransformer(
"textToHint",
$"{pageId}.footerHintText",
"hintText"
));
Device Support¶
APL Capability Check¶
public static class SkillRequestExtensions
{
public static bool APLSupported(this SkillRequest request)
{
return request.Context?.System?.Device?.SupportedInterfaces?.ContainsKey("Alexa.Presentation.APL") == true;
}
}
// Usage in handlers
if (input.RequestEnvelope.APLSupported())
{
var (renderDirective, executeDirective) = _documentBuilder
.AddQuestionSlide(speechText, questionText, choices)
.GetDirectives();
input.ResponseBuilder
.AddDirective(renderDirective)
.AddDirective(executeDirective);
}
Responsive Design¶
// Different layouts for different screen sizes
var layout = viewport.Shape == ViewportShape.Round
? Layouts.RoundScreenLayout
: Layouts.RectangleScreenLayout;
Best Practices¶
1. Progressive Enhancement¶
Always provide voice-only fallbacks:
var speechText = "What's your answer?";
var repromptText = "Say a number from 1 to 4, or say 'I don't know'.";
if (input.RequestEnvelope.APLSupported())
{
// Add visual components
var (renderDirective, executeDirective) = _documentBuilder
.AddQuestionSlide(speechText, questionText, choices)
.GetDirectives();
// ...
}
return await input.ResponseBuilder
.Speak(speechText)
.Reprompt(repromptText)
.GetResponse(cancellationToken);
2. Coordinate Voice and Visual¶
Ensure speech matches visual content:
var speechText = "Question 2: Who created Mickey Mouse? Your choices are: 1. Walt Disney, 2. Ub Iwerks, 3. Roy Disney, 4. Carl Barks.";
var questionText = "Who created Mickey Mouse?";
var choices = new[] { "Walt Disney", "Ub Iwerks", "Roy Disney", "Carl Barks" };
_documentBuilder.AddQuestionSlide(speechText, questionText, choices);
3. Handle Both Voice and Touch¶
Support multiple interaction modes:
public class AnswerHandler : IRequestHandler<IntentRequest>, IRequestHandler<UserEventRequest>
{
public Task<bool> CanHandle(IHandlerInput input, CancellationToken cancellationToken = default)
{
return Task.FromResult(
input.RequestEnvelope.Request is UserEventRequest ||
(input.RequestEnvelope.Request is IntentRequest intent &&
intent.Intent.Name == "AnswerIntent"));
}
}
4. Use Meaningful IDs¶
Examples¶
For complete APL integration examples, see the Examples section with the trivia skill's visual interface implementation.