flutterware
All guides
Screens and tests

Previews

Look at a screen without running the app to get there. Previews renders every @Preview in your project on a device frame, in a live Flutter engine, with your fonts and your theme.

The previews panel: the demo's screens listed on the left, the menu rendered
        on an iPhone 16 frame on the right

The live panel in the studio is macOS only for now. The command line actions that render without a window, like screenshot and audit, also run on Linux.

Write a preview#

A preview is a function that returns a widget, marked with Flutter's own @Preview annotation:

// demo/shop.dart
import 'package:flutter/widget_previews.dart';

@Preview(name: 'Menu', group: 'Shop', wrapper: wrapInShop)
Widget shopMenu() => const MenuScreen();

@Preview(name: 'Drink', group: 'Shop', wrapper: wrapInShop)
Widget shopDrink() => DrinkScreen(drinks.first);

Nothing from flutterware is needed to declare one, and there's no list to register it in: every .dart file in the package is scanned. A preview written for Flutter's own previewer shows up here unchanged.

fw run previews new --name='Menu' writes a starter file for you.

Turn it on#

// tool/flutterware.dart
fw.use(
  Previews(
    packages: [
      PreviewsPackage(
        app,
        directory: 'demo',
        canvases: [
          PreviewCanvas('', devices: [Devices.iphone16, Devices.androidTall]),
        ],
      ),
    ],
  ),
);

Give previews what the app would#

A screen usually expects a MaterialApp, a theme and localizations above it. The wrapper: of a preview puts them back. Wrap that in a PreviewShell and you also get switches in the previews toolbar, which stay set as you move from one preview to the next:

import 'package:flutter_localizations/flutter_localizations.dart';
import 'package:flutterware/previews.dart';

Widget wrapInShop(Widget child) => PreviewShell(
  'shop',
  builder: (context, axes) => MaterialApp(
    theme: shopTheme(
      axes.flag('dark', false) ? Brightness.dark : Brightness.light,
    ),
    locale: axes.picker('locale', {
      'English': const Locale('en'),
      'Français': const Locale('fr'),
    }, const Locale('en')),
    localizationsDelegates: [
      ShopStrings.delegate,
      ...GlobalMaterialLocalizations.delegates,
    ],
    home: child,
  ),
);

Outside the studio, in your app or in Flutter's previewer, every axis returns its default.

Set things up once for every preview#

Some of what an app needs happens once in main rather than in a widget: turning off a font package's downloads, registering fonts, installing HttpOverrides. Previews never run your main, so name a setup file on the package instead of repeating it in every wrapper::

// tool/flutterware.dart
PreviewsPackage(app, setup: 'lib/preview_setup.dart')
// lib/preview_setup.dart
import 'package:google_fonts/google_fonts.dart';

Future<void> previewSetup() async {
  // Previews render offline, and a test engine answers every download with an
  // error. Use the fonts bundled under assets/ instead.
  GoogleFonts.config.allowRuntimeFetching = false;
}

previewSetup() takes no arguments and can be async. It runs once, after the Flutter binding exists and before the first preview builds, everywhere a preview renders: the studio's panel, screenshot, inspect, audit, compare and the page build-web writes. That is also the place for HttpOverrides.global = …, so previews that fetch get your fake answers. The same file is compiled into the build-web page, so if you use that, keep dart:io behind a conditional import.

If the file is missing or doesn't declare previewSetup, the previews plugin reports it and refuses to render the package rather than render every preview without it. If previewSetup() throws, you get its error instead of the previews. The command line actions pick up an edit to the setup on their next run; the studio's panel runs it when it starts, so reopen the panel after changing it.

Knobs#

A knob is a value you can change while looking at a preview. Ask for one while building:

@Preview(name: 'Order placed', group: 'Shop', wrapper: wrapInShop)
Widget shopConfirmation() => Builder(
  builder: (context) =>
      ConfirmationScreen(name: context.knobs.string('name', 'Ada')),
);

It appears in the Controls tab. A preview's optional parameters become knobs too, so Widget shopConfirmation({String name = 'Ada'}) declares the same one. Outside the studio a knob returns the default written at the call site, so it's safe to leave in code that ships.

A picker is a dropdown. For a switch you flip back and forth while looking, such as light and dark, ask for segments instead and every option stays on screen:

var brightness = context.knobs.picker('theme', {
  'Light': Brightness.light,
  'Dark': Brightness.dark,
}, Brightness.light, style: PickerStyle.segmented);

axes.picker takes the same style:, for a switch in the toolbar. Up to five options are drawn as segments; a picker with more is drawn as a dropdown anyway, so a long list never pushes the rest of the toolbar out of sight.

In the studio#

Pick a preview in the list and it renders on the canvas's default device. The toolbar changes the device, rotates it, raises the software keyboard and zooms. The tabs underneath are:

The menu on an iPhone 16 frame, with the Elements tab open under it on the
        widget tree

Save a file and the preview reloads.

From the command line or an agent#

fw run previews entries                                         # every preview, with its id
fw run previews screenshot --entry='demo/shop.dart#shopMenu'    # render one to a PNG
fw run previews screenshot --entry='demo/shop.dart#shopMenu' \
    --device=android-tall --axes='dark=true,locale=Français'
fw run previews inspect --entry='demo/shop.dart#shopMenu'       # what's on screen, as text
fw run previews audit                                           # render every preview, report failures
fw run previews build-web                                       # your previews as a web page

screenshot is the quickest way for an agent to check how a widget looks: it renders with the real fonts and theme, at the device's pixel ratio, and --node=<name> crops to one widget. Over MCP the same actions go through flutterware_invoke.

A preview that draws and then reports an error, such as a font that fails to download, still gets its picture. screenshot prints the path as usual, says on stderr that the preview reported errors, and lists them under meta.errors in --json. inspect lists them too and answers ok: false. Each error says where it was thrown, preferring your own code over a dependency's, so a fault in a theme or a wrapper points there rather than at the preview. Only a preview that drew nothing is refused.

A preview can also be the source of an image you publish, such as store artwork. On the plain rectangle (--device=fit, or a canvas with no devices), --width and --height are pixels, so they give the exact size a store asks for. --opaque writes the PNG without an alpha channel, which Google Play requires for a feature graphic.

Check every preview in CI#

fw run previews audit renders every preview on its canvas, reports each one that doesn't compile or doesn't render, and exits non-zero if there is one. It also writes an ordinary test file, so the same check can run with the rest of your suite:

fw run previews audit
flutter test build/flutterware/previews_harness.dart

Expect the first audit to find things. A preview that only worked because something above it provided a Directionality or a MediaQuery fails here and nowhere else.

Reference#

Every action and option, including compare against a base branch: flutterware.previews in the capabilities reference.

Edit this page on GitHub