Skip to content

Templates

Describe a video shape once, then render it for many inputs. A VideoTemplate turns a Props value into a Video, so the same definition makes a personalized intro per user or a stat reel per metric. Lesson 11 reuses the built-in StatHighlight template and lifts its scene into a reel:

Scene _statCardScene() => const StatHighlight().build(_stat).scenes.single;

The props feeding it are plain data:

const _stat = StatHighlightProps(value: 48230, label: 'minutes listened');

Change _stat and the same template builds a different card. The rest of this page covers writing your own template, the two built-ins, and rendering a batch.

Subclass VideoTemplate<Props> and implement build. The build is a pure function of its props: it reads the value and returns a Video, with no IO and no wall-clock:

class GreetingProps {
const GreetingProps({required this.name});
final String name;
}
class GreetingTemplate extends VideoTemplate<GreetingProps> {
const GreetingTemplate();
@override
Video build(GreetingProps props) => Video(
size: VideoSize.reels,
scenes: [
Scene.centered(
duration: const Time.seconds(3),
background: Background.color(const Color(0xFF0E1116)),
child: Text(
'Hi ${props.name}',
style: const TextStyle(fontSize: 80, color: Color(0xFF55EFC4)),
).animate([Animation.pop()]),
),
],
);
}

A template is const and holds no state. Every choice flows through the props, which keeps the definition a pure function of its input. Make your Props value-equal (immutable, with == and hashCode by field), so two equal props compare equal and the cache can trust them.

Two templates ship on the public API. Each is built only on the public element API, so it doubles as a worked example of a VideoTemplate.

TitleIntro is a centered title that pops in, with an optional subtitle that slides in after:

const TitleIntroProps(title: '2025', subtitle: 'Year in review');

StatHighlight is a Counter headline counting up to a value, with its label beneath:

const StatHighlightProps(value: 48230, label: 'minutes listened');

Both props carry an optional accent color and background color, so you brand a card without rewriting it. Read the source of either as a starting point for your own template.

renderTemplate builds template.build(props) and runs the result through the same offline capture path that render(video, aspect:) uses. It is a separate free function from render, because Dart has no overloading. To render a batch, map each data row onto its props and call renderTemplate once per row:

Future<void> renderGreetings({
required List<String> names,
required RenderService service,
required ShellMount pumpWidget,
required ShellFramePump pumpFrame,
}) async {
for (final name in names) {
await renderTemplate(
const GreetingTemplate(),
props: GreetingProps(name: name),
frameCount: 90,
outDir: Directory('out/$name'),
service: service,
pumpWidget: pumpWidget,
pumpFrame: pumpFrame,
);
}
}

renderTemplate defaults aspect to Aspect.reels, so a template renders vertical without a per-call aspect. Pass another aspect to fan one template out across formats. The service, pumpWidget, and pumpFrame are the host’s render wiring; the example app and the CLI supply them for you.

The same template rendered for the same props produces the same render. This holds because build is a pure function of its props: the same props build the same tree, the same tree captures the same picture, and value-equal props let the frame cache share work across renders. Different props produce different frames, as you would expect. Pass a different value and the count changes; pass an equal Props and you get the exact same bytes.

This is what makes data-driven batch rendering safe. Render a thousand personalized intros from a thousand rows, re-run the batch tomorrow, and every unchanged row produces the same file it did today.

  • Multi-aspect: fan one template out to reels, square, landscape, and portrait with the same props.
  • Exporting your video: pick the container each rendered template writes.