How to Build a Watch Tutorial Screen in Flutter (Full Code + Preview)
Some apps need a beat between 'welcome' and 'give me your phone number' — a short video that explains how the thing works. This is that screen from the Finco wallet kit: a tappable video card with a vector play button, a 35px 'Watch Tutorial' heading, three lines of supporting copy, a blue 'Let's Create an account' CTA and a Sign In link underneath. You'll build a layout whose card width is computed from the viewport, whose vertical rhythm comes from weighted Spacers, and whose CTA scales when pressed.

What you'll build
- ✓A video/tutorial card whose width tracks the viewport but is clamped between 220 and 300 logical pixels
- ✓A weighted-Spacer composition (3 / 4 / 5 / 2) that keeps the balance on any device height
- ✓A press-animated blue CTA with a coloured drop shadow that matches the button
- ✓A padded, opaque-hit-test text link so the Sign In tap target is bigger than the glyphs
- ✓The bundled Aileron design font wired through pubspec.yaml
Step-by-step build
Create the file
Add a new file at lib/wallet_watch_tutorial/wallet_watch_tutorial_screen.dart in your Flutter project.
Register the bundled fonts
No external packages — this is pure Flutter. It does bundle its design font (Aileron), so drop the font file into fonts/ and declare it in pubspec.yaml:
flutter:
fonts:
- family: Aileron
fonts:
- asset: fonts/Aileron-Regular.ttfBuild it, piece by piece
Here's how the screen goes together. Each block below is a real slice of the code with a plain-English explanation — paste them in order, or grab the whole file from the next section.
Colour tokens and the responsive card width
import 'package:flutter/material.dart';
import 'widgets/tutorial_card.dart';
/// "Watch Tutorial" screen from the Wallet App UI Kit (Finco): a video card,
/// heading + supporting copy, a blue "Let's Create an account" CTA and a
/// "Sign In" link.
///
/// Self-contained, pure Flutter. Bundles its exact design font (Aileron) and
/// the kit's original play-button drawables. Fully responsive — flex spacers
/// centre the content on tall devices and a scroll fallback prevents overflow
/// on short ones. Renders standalone when pushed as a route.
class WalletWatchTutorialScreen extends StatelessWidget {
const WalletWatchTutorialScreen({super.key});
static const Color _ink = Color(0xFF29304D);
static const Color _blue = Color(0xFF4F62C0);
static const Color _muted = Color(0xFF323232);
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.white,
body: SafeArea(
child: LayoutBuilder(
builder: (BuildContext context, BoxConstraints constraints) {
final double cardWidth =
(constraints.maxWidth * 0.72).clamp(220.0, 300.0);
return SingleChildScrollView(
physics: const ClampingScrollPhysics(),
child: ConstrainedBox(
constraints: BoxConstraints(minHeight: constraints.maxHeight),
child: IntrinsicHeight(Three static const Colors hold the palette: _ink (#29304D) for headings and body, _blue (#4F62C0) for the CTA, and _muted (#323232) for the Sign In link. Inside the LayoutBuilder, cardWidth is computed as (constraints.maxWidth * 0.72).clamp(220.0, 300.0) — the card takes 72% of the available width but is never smaller than 220 or larger than 300 logical pixels, which is what stops it from looking like a postage stamp on a small phone or a billboard on a tablet. The usual scroll-safety trio follows: SingleChildScrollView with ClampingScrollPhysics (no iOS bounce when the content already fits), ConstrainedBox(minHeight: constraints.maxHeight) and IntrinsicHeight.
The tutorial card and the headline
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 24),
child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
children: <Widget>[
const Spacer(flex: 3),
TutorialCard(width: cardWidth, onTap: () {}),
const Spacer(flex: 4),
const Text(
'Watch Tutorial',
textAlign: TextAlign.center,
style: TextStyle(
fontFamily: 'Aileron',
fontWeight: FontWeight.w700,
fontSize: 35,
height: 1.2,
color: _ink,
),
),Everything sits in a centre-aligned Column with 24px of horizontal padding. Spacer(flex: 3) opens the layout, then TutorialCard receives the computed cardWidth and an onTap that would launch your video player. Spacer(flex: 4) separates the card from the 'Watch Tutorial' heading — 35px w700 Aileron at height: 1.2, in _ink. Because the spacers use flex weights rather than fixed SizedBoxes, the gaps grow proportionally on a tall device and shrink on a short one, which is why the composition never looks top-heavy.
Supporting copy and the CTA group
const SizedBox(height: 21),
const Text(
'If you are new on this and need\n'
'help, watch this short tutorial clip to\n'
'get started.',
textAlign: TextAlign.center,
style: TextStyle(
fontFamily: 'Aileron',
fontWeight: FontWeight.w400,
fontSize: 20,
height: 1.45,
color: _ink,
),
),
const Spacer(flex: 5),
_PrimaryButton(
label: 'Let’s Create an account',
onTap: () {},
),
const SizedBox(height: 28),
GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: () {},
child: const Padding(
padding: EdgeInsets.symmetric(
horizontal: 16,
vertical: 4,
),
child: Text(
'Already have an account? - Sign In',
textAlign: TextAlign.center,
style: TextStyle(
fontFamily: 'Aileron',
fontWeight: FontWeight.w400,
fontSize: 15,
color: _muted,
),
),
),
),
const Spacer(flex: 2),The explainer is three adjacent string literals joined with \n — Dart concatenates them at compile time, so the line breaks match the design exactly instead of depending on device width. It renders at 20px regular with height: 1.45 for comfortable leading. Then Spacer(flex: 5), the largest weight in the file, opens the biggest gap in the layout right before the actions. The _PrimaryButton carries the 'Let's Create an account' label, and 28px below it a GestureDetector wraps the Sign In text. That GestureDetector uses HitTestBehavior.opaque with 16px horizontal / 4px vertical padding, which is what makes the padding tappable — without opaque, taps in the padding would pass straight through. A trailing Spacer(flex: 2) keeps the group off the bottom edge.
The press-animated primary button
class _PrimaryButton extends StatefulWidget {
const _PrimaryButton({required this.label, required this.onTap});
final String label;
final VoidCallback onTap;
@override
State<_PrimaryButton> createState() => _PrimaryButtonState();
}
class _PrimaryButtonState extends State<_PrimaryButton> {
bool _pressed = false;
@override
Widget build(BuildContext context) {
return GestureDetector(
onTapDown: (_) => setState(() => _pressed = true),
onTapUp: (_) => setState(() => _pressed = false),
onTapCancel: () => setState(() => _pressed = false),
onTap: widget.onTap,
child: AnimatedScale(
scale: _pressed ? 0.98 : 1,
duration: const Duration(milliseconds: 150),
child: Container(
width: double.infinity,
margin: const EdgeInsets.symmetric(horizontal: 32),
padding: const EdgeInsets.symmetric(vertical: 20),
decoration: BoxDecoration(
color: WalletWatchTutorialScreen._blue,
borderRadius: BorderRadius.circular(15),
boxShadow: <BoxShadow>[
BoxShadow(
color: WalletWatchTutorialScreen._blue.withValues(alpha: 0.30),
blurRadius: 18,
offset: const Offset(0, 10),
),
],
),
child: Text(
widget.label,
textAlign: TextAlign.center,
style: const TextStyle(
fontFamily: 'Aileron',
fontWeight: FontWeight.w700,
fontSize: 17,
color: Colors.white,
),
),
),
),
);
}
}_PrimaryButton is a small StatefulWidget whose entire state is one _pressed boolean, flipped in onTapDown, onTapUp and onTapCancel — onTapCancel matters because it resets the button when the user drags a finger off it instead of leaving it stuck small. AnimatedScale reads _pressed and shrinks the button to 0.98 over 150ms, which is a subtle enough scale to read as physical rather than bouncy. The Container is filled with _blue, rounded to 15px, and carries a BoxShadow in the same blue at 30% alpha with an 18px blur offset 10px downward — tinting the shadow with the button's own colour is what makes it glow rather than look dirty. The 32px horizontal margin sits on top of the page's 24px padding, matching the kit's inset CTA.
Full code
The complete, ready-to-paste source (2 files). Free to use in your projects — one click copies it all.
import 'package:flutter/material.dart';
import 'widgets/tutorial_card.dart';
/// "Watch Tutorial" screen from the Wallet App UI Kit (Finco): a video card,
/// heading + supporting copy, a blue "Let's Create an account" CTA and a
/// "Sign In" link.
///
/// Self-contained, pure Flutter. Bundles its exact design font (Aileron) and
/// the kit's original play-button drawables. Fully responsive — flex spacers
/// centre the content on tall devices and a scroll fallback prevents overflow
/// on short ones. Renders standalone when pushed as a route.
class WalletWatchTutorialScreen extends StatelessWidget {
const WalletWatchTutorialScreen({super.key});
static const Color _ink = Color(0xFF29304D);
static const Color _blue = Color(0xFF4F62C0);
static const Color _muted = Color(0xFF323232);
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.white,
body: SafeArea(
child: LayoutBuilder(
builder: (BuildContext context, BoxConstraints constraints) {
final double cardWidth =
(constraints.maxWidth * 0.72).clamp(220.0, 300.0);
return SingleChildScrollView(
physics: const ClampingScrollPhysics(),
child: ConstrainedBox(
constraints: BoxConstraints(minHeight: constraints.maxHeight),
child: IntrinsicHeight(
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 24),
child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
children: <Widget>[
const Spacer(flex: 3),
TutorialCard(width: cardWidth, onTap: () {}),
const Spacer(flex: 4),
const Text(
'Watch Tutorial',
textAlign: TextAlign.center,
style: TextStyle(
fontFamily: 'Aileron',
fontWeight: FontWeight.w700,
fontSize: 35,
height: 1.2,
color: _ink,
),
),
const SizedBox(height: 21),
const Text(
'If you are new on this and need\n'
'help, watch this short tutorial clip to\n'
'get started.',
textAlign: TextAlign.center,
style: TextStyle(
fontFamily: 'Aileron',
fontWeight: FontWeight.w400,
fontSize: 20,
height: 1.45,
color: _ink,
),
),
const Spacer(flex: 5),
_PrimaryButton(
label: 'Let’s Create an account',
onTap: () {},
),
const SizedBox(height: 28),
GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: () {},
child: const Padding(
padding: EdgeInsets.symmetric(
horizontal: 16,
vertical: 4,
),
child: Text(
'Already have an account? - Sign In',
textAlign: TextAlign.center,
style: TextStyle(
fontFamily: 'Aileron',
fontWeight: FontWeight.w400,
fontSize: 15,
color: _muted,
),
),
),
),
const Spacer(flex: 2),
],
),
),
),
),
);
},
),
),
);
}
}
class _PrimaryButton extends StatefulWidget {
const _PrimaryButton({required this.label, required this.onTap});
final String label;
final VoidCallback onTap;
@override
State<_PrimaryButton> createState() => _PrimaryButtonState();
}
class _PrimaryButtonState extends State<_PrimaryButton> {
bool _pressed = false;
@override
Widget build(BuildContext context) {
return GestureDetector(
onTapDown: (_) => setState(() => _pressed = true),
onTapUp: (_) => setState(() => _pressed = false),
onTapCancel: () => setState(() => _pressed = false),
onTap: widget.onTap,
child: AnimatedScale(
scale: _pressed ? 0.98 : 1,
duration: const Duration(milliseconds: 150),
child: Container(
width: double.infinity,
margin: const EdgeInsets.symmetric(horizontal: 32),
padding: const EdgeInsets.symmetric(vertical: 20),
decoration: BoxDecoration(
color: WalletWatchTutorialScreen._blue,
borderRadius: BorderRadius.circular(15),
boxShadow: <BoxShadow>[
BoxShadow(
color: WalletWatchTutorialScreen._blue.withValues(alpha: 0.30),
blurRadius: 18,
offset: const Offset(0, 10),
),
],
),
child: Text(
widget.label,
textAlign: TextAlign.center,
style: const TextStyle(
fontFamily: 'Aileron',
fontWeight: FontWeight.w700,
fontSize: 17,
color: Colors.white,
),
),
),
),
);
}
}
Plus bundled 5 binary assets (fonts/images). The CLI and MCP install those for you automatically.
Two faster ways to add it
Copy-paste works, but you can skip it entirely.
1. FlutterKit CLI
One command drops this screen — and its fonts — straight into your project.
$ flutterkit add wallet-watch-tutorial2. AI agent (MCP)
Connect FlutterKit's MCP server in Claude or Cursor and just ask your agent to install wallet-watch-tutorial — it fetches and writes the files for you.
FAQ
Is this Flutter tutorial screen free to use?
Yes. The full Dart source on this page is free to copy into personal or commercial projects. You can also install it with the FlutterKit CLI (flutterkit add wallet-watch-tutorial) or have an AI agent add it via MCP.
How do I make the card play a real video?
The TutorialCard takes an onTap callback that is empty in the demo. Point it at a route containing video_player or chewie, or at a full-screen player sheet. The card itself is a static thumbnail with a vector play button drawn in Flutter, so nothing about the layout changes when you add the player.
Does it need any external packages?
No — the screen is pure Flutter on the material library. It bundles the Aileron font and the kit's play-button drawables, registered in pubspec.yaml as shown in step 2; the CLI and MCP install them for you.
Which Flutter version does it target?
It calls Color.withValues() for the button shadow, so it targets Flutter 3.22+ (Dart 3). On an older SDK, change withValues(alpha: 0.30) to withOpacity(0.30) and it compiles unchanged.