Architecture V2 (WIP)

SUMMARY

Implementing Widget Previews for Flutter

Author: Ben Konyi (bkonyi)

Go Link: flutter.dev/go/widget-previews-architecture

Created: November 2024   /  Last updated: Aug 5, 2025

WHAT PROBLEM IS THIS SOLVING?

While one of the main selling points of Flutter is its rapid iterative development cycle that’s enabled by hot reload, it does require developers to have an active target device or simulator to work. In addition, it only allows for viewing the UI in a single configuration at a time without running an application on multiple devices, making it difficult for developers to visually verify the impact of variables like screen size, orientation, font size, and locale on their application.

See https://github.com/flutter/flutter/issues/115704, and https://forum.itsallwidgets.com/t/support-for-preview-feature-in-flutter/

Flutter Widget Previews is a development tool designed to improve the Flutter development workflow by allowing developers to quickly visualize and iterate on their widgets in various configurations without running the full application. This tool aims to significantly reduce development time and improve the overall developer experience by providing a fast and convenient way to experiment with widget variations, themes, and different device settings.

BACKGROUND

An interesting feature provided by both Jetpack Compose and SwiftUI is the ability to preview components of an application’s UI in the IDE. Developers can specify which components should be previewed and create wrapper components that provide mock data to the UI. Individual components can have multiple previews with different configurations that are rendered in the same previewer, making them easy to compare.

Example of a Jetpack Compose component preview from Android Studio. This previews a single composable with multiple configurations applied, including locale and different font sizes.

XCode’s SwiftUI preview

Alongside its view preview, XCode also provides support for editing properties of Views using a UI, applying changes to the code and the preview instantly.

Audience

Flutter framework contributors, Dart + Flutter developer experience engineers.

Glossary

  • Widget Preview: an isolated widget displayed in a preview environment for use in development workflows
  • Preview Scaffold: the generated Flutter application used to display widget previews defined in a project

OVERVIEW

This section aims to describe the overall architecture of the Widget Preview feature, as well as some examples of how developers could possibly create previews in their projects.

Non-goals

Additional UX research needs to be done to determine the optimal UI for rendering previews in the preview environment. How the previews are actually displayed is trivial, so this document focuses on outlying the underlying business logic of the widget previewer scaffold and the widget preview command implementation in the Flutter tool.

USAGE EXAMPLES

Widget previews are defined directly in the developer’s project as a function marked with an annotation. Preview annotations can be applied to top-level and static functions that return either a Widget or WidgetBuilder, and can also be applied to constructors which have no required parameters, as seen in this simple example:

import ’package:flutter/material.dart’;

import ’package:flutter/widget_previews.dart’;

@Preview(name: 'Top-level preview')

Widget preview() => const Text('Foo');

@Preview(name: 'Builder preview')

WidgetBuilder builderPreview() {

  return (BuildContext context) {

    return const Text('Builder');

  };

}

class MyWidget extends StatelessWidget {

  @Preview(name: 'Constructor preview')

  const MyWidget.preview({super.key});

  @Preview(name: 'Factory constructor preview')

  factory MyWidget.factoryPreview() => const MyWidget.preview();

  @Preview(name: 'Static preview')

  static Widget previewStatic() => const Text('Static');

  @override

  Widget build(BuildContext context) {

    return const Text('MyWidget');

  }

}

The resulting previews created from the minimalistic sample above.

Developers can then interact with their previews in the preview environment embedded in their IDE. Widget previews are rendered in a Flutter application, meaning they are fully interactive and can be used to preview UI layouts and animations. Previews also support zooming and panning, making it easy to inspect UIs at a pixel-level.

Early Widget Preview prototype using the old preview definition pattern showcases previewing a full application and individual UI element within an IDE.

Important Note: the frame rate of these GIFs are not representative of the actual frame rate of the preview environment, which renders previews at ~60 FPS.

Previews have built-in support for Flutter’s currently supported design languages (Material and Cupertino), and allows for toggling between light and dark modes for individual previews:

import 'package:flutter/cupertino.dart';

import 'package:flutter/material.dart';

import 'package:flutter/widget_previews.dart';

PreviewThemeData previewTheme() => PreviewThemeData(

      materialLight: ThemeData(

        colorScheme: const ColorScheme.light(primary: Colors.red),

      ),

      materialDark: ThemeData(

        colorScheme: const ColorScheme.dark(primary: Colors.blue),

      ),

      cupertinoLight: const CupertinoThemeData(primaryColor: Colors.yellow),

      cupertinoDark: const CupertinoThemeData(primaryColor: Colors.purple),

    );

class ThemeExample extends StatelessWidget {

  @override

  Widget build(BuildContext context) {

    final brightness = MediaQuery.platformBrightnessOf(context);

    return Column(

      children: [

        Text('Current Brightness: $brightness'),

        const SizedBox(height: 10),

        FilledButton(child: Text('Material'), onPressed: () => null),

        const SizedBox(height: 10),

        CupertinoButton.filled(child: Text('Cupertino'), onPressed: () => null),

      ],

    );

  }

}

@Preview(name: 'Dark', theme: previewTheme, brightness: Brightness.dark)

@Preview(name: 'Light', theme: previewTheme, brightness: Brightness.light)

Widget preview() => ThemeExample();

Previews displaying Material and Cupertino widgets using light and dark mode themes. Preview brightness can also be toggled with the brightness button under each preview.

State can be injected into the widget tree above the previewed element to allow for dependency injection and data mocking. For example, previewed widgets can be wrapped with a Provider using a wrapper callback to inject application state needed by the widget, as seen in this example in the Veggie Seasons sample application:

class VeggieCard extends StatelessWidget {

  const VeggieCard(this.veggie, this.isInSeason, this.isPreferredCategory,

      {super.key});

  static Widget stateWrapper(Widget child) {

    return MultiProvider(providers: [

      ChangeNotifierProvider.value(

        value: AppState(),

      ),

      ChangeNotifierProvider(

        create: (_) => Preferences()..load(),

      ),

    ], child: child);

  }

  @Preview(name: 'Veggie Card', wrapper: stateWrapper)

  static WidgetBuilder preview() => (context) {

        final appState = Provider.of<AppState>(context);

        return VeggieCard(appState.allVeggies.first, true, true);

      };

  /// Widget iplementation details omitted for brevity

}

DETAILED DESIGN

The Widget Previews feature involves multiple components spanning the Dart and Flutter projects. This includes:

  • The addition of a flutter widget-preview command to the Flutter tool, which is responsible for generating the preview scaffold for a project and interacting with the preview environment
  • The addition of classes needed to define widget previews to the framework
  • Support in the Dart analyzer to detect invalid widget preview definitions
  • IDE plugin support for VSCode and Intellij

flutter widget-preview start

The flutter widget-preview start command will be responsible for creating the preview scaffold and managing the preview environment for a given project.

Generating the initial preview environment

The first time the command is run for a project on the user’s machine, the tool will perform the following tasks:

  1. Create a new Flutter project (currently named widget_preview_scaffold) under the .dart_tool directory using a combination of the app and widget_preview_scaffold project templates.

    This creates a Flutter Web application with
    roughly[a] the following project structure:
  • lib/src/widget_preview_rendering.dart: the true entrypoint to the preview scaffold, responsible for initializing the Dart Tooling Daemon (DTD) services and the scaffolding widgets used to render previews defined in lib/src/generated_preview.dart
  • lib/src/generated_preview.dart: contains the List<WidgetPreview> previews() function, which will eventually return the list of processed Preview applications (as instances of WidgetPreview) to the preview scaffolding
  • lib/src/widget_preview.dart: contains the definition of the WidgetPreview class, which is a data class with a near 1:1 mapping to Preview. This class includes a builder property, which takes a closure that returns the widget to be previewed.
  • lib/src/dtd/*: contains a Dart Tooling Daemon (DTD) server connection, providing widget preview services to tooling and access to services provided by other tools (e.g., for interaction with IDEs, analysis server, etc.)

  1. Initialize preview_scaffold’s pubspec.yaml, adding path dependencies on the developer’s project.
  2. Generate preview_manifest.json at the root of the widget_preview_scaffold. This currently contains information about the current Dart and Flutter SDK revisions, as well as a hash of the user’s pubspec.yaml
  3. Using package:analyzer, perform a search for valid applications of the @Preview()annotation, recording the preview function names and their libraries, as well as all the arguments provided to the annotation.
  4. Generates lib/src/generated_preview.dart using the results of the search for previews in the developer’s project. package:code_builder is used to generate code that imports and calls each of the widget preview functions to return a list of previews. For example:

import 'widget_preview.dart' as _i1;

import 'package:splash/helper.dart' as _i2;

import 'package:splash/main.dart' as _i3;

import 'package:flutter/widgets.dart' as _i4;

import 'dart:ui' as _i5;

List<_i1.WidgetPreview> previews() => [

      _i1.WidgetPreview(

        packageName: 'splash',

        name: 'Top-level preview',

        builder: () => _i2.wrapper(_i3.preview()),

      ),

      _i1.WidgetPreview(

        packageName: 'splash',

        name: 'Builder preview',

        textScaleFactor: 2.0,

        builder: () => _i4.Builder(builder: _i3.builderPreview()),

      ),

      _i1.WidgetPreview(

        packageName: 'splash',

        name: 'Constructor preview',

        theme: _i2.previewTheme(),

        builder: () => _i3.MyWidget.preview(),

      ),

      _i1.WidgetPreview(

        packageName: 'splash',

        name: 'Factory constructor preview',

        size: const _i5.Size(

          100.0,

          200.0,

        ),

        builder: () => _i3.MyWidget.factoryPreview(),

      ),

      _i1.WidgetPreview(

        packageName: 'splash',

        name: 'Static preview',

        brightness: _i5.Brightness.dark,

        builder: () => _i3.MyWidget.previewStatic(),

      ),

    ];

Running and managing the preview environment

Once the preview scaffold has been generated, it’s compiled and run. At this point, the Flutter tool will initialize a file watcher on the developer’s project directory to detect changes to source code.

The analyzer API is used to detect the addition or removal of widget preview definitions in updated files, regenerating lib/src/generated_preview.dart when necessary. At this point the Flutter tool will trigger a hot reload, resulting in the preview environment picking up the changes made in the developer’s project.

The file watcher will also listen for changes to the developer’s pubspec.yaml and update the widget_preview_scaffold pubspec.yaml to reflect any changes made to the set of dependencies and assets in the developer’s project.

Before shutting down, the Flutter tool will clean up the preview environment.

Handling pubspec.yaml changes

On subsequent runs, flutter widget-preview start will generate a hash of the developer’s pubspec.yaml and compare it to the last known pubspec hash stored in preview_manifest.json. If these hashes don’t match, the pubspec.yaml in widget_preview_scaffold is regenerated to ensure new dependencies from the developer’s project are available to the widget preview scaffold.

Handling Flutter SDK upgrades

As part of the startup sequence initiated by flutter widget-preview start, the Dart and Flutter SDK revisions in preview_manifest.json are compared to the currently installed SDKs. If these revisions don’t match, the widget_preview_scaffold project is deleted and regenerated to ensure that the latest version of the widget_preview_scaffold template is used for the previewer.

flutter widget-preview clean

The flutter widget-preview clean subcommand simply deletes the .dart_tool/widget_preview_scaffold/ project, forcing it to be regenerated on the next run of flutter widget-preview start.

Annotations and Data Classes

As mentioned in the previous section, the flutter widget-preview command uses the analyzer APIs to detect @Preview() annotations on functions that return Widget or WidgetBuilder

@Preview()[b][c][d]

The Preview annotation class is used to mark a function or constructor as a widget preview.

This annotation currently allows for developers to provide:

  • A description to be displayed alongside the preview in the preview environment.
  • The size of the preview, which overrides the size returned by MediaQuery to allow for previewing adaptive UIs.
  • Text scaling factors to adjust default font scaling behavior.
  • The initial platform brightness to control theming selection (e.g., light vs dark mode).
  • A callback which returns Material and Cupertino theming data via a PreviewThemeData object.
  • A callback allowing for developers to wrap the previewed widget with additional state.
  • Applying specific locales to the preview via a PreviewLocalizationsData object.

The arguments to the annotation must be compile time constants and not reference private symbols (i.e., all arguments to the annotation must be valid when referenced outside of the library). This is necessary for the preview code generation to work.

Properties that would commonly require the invocation of non-constant constructors (e.g., MaterialTheme.dark() is non-constant) are provided via a static callback (e.g., theme), as the use of compile time constants is not required in the callback itself.

// Copyright 2014 The Flutter Authors. All rights reserved.

// Use of this source code is governed by a BSD-style license that can be

// found in the LICENSE file.

/// @docImport 'package:/flutter/cupertino.dart';

/// @docImport 'package:/flutter/material.dart';

library;

import 'package:flutter/cupertino.dart' show CupertinoThemeData;

import 'package:flutter/material.dart' show Brightness, ThemeData;

import 'package:flutter/widgets.dart';

/// Signature for callbacks that build theming data used when creating a [Preview].

typedef PreviewTheme = PreviewThemeData Function();

/// Signature for callbacks that wrap a [Widget] with another [Widget] when creating a [Preview].

typedef WidgetWrapper = Widget Function(Widget);

/// Signature for callbacks that build localization data used when creating a [Preview].

typedef PreviewLocalizations = PreviewLocalizationsData Function();

/// Annotation used to mark functions that return a widget preview.

///

/// NOTE: this interface is not stable and **will change**.

///

/// {@tool snippet}

///

/// Functions annotated with `@Preview()` must return a `Widget` or

/// `WidgetBuilder` and be public. This annotation can only be applied

/// to top-level functions, static methods defined within a class, and

/// public `Widget` constructors and factories with no required arguments.

///

/// ```dart

/// @Preview(name: 'Top-level preview')

/// Widget preview() => const Text('Foo');

///

/// @Preview(name: 'Builder preview')

/// WidgetBuilder builderPreview() {

///   return (BuildContext context) {

///     return const Text('Builder');

///   };

/// }

///

/// class MyWidget extends StatelessWidget {

///   @Preview(name: 'Constructor preview')

///   const MyWidget.preview({super.key});

///

///   @Preview(name: 'Factory constructor preview')

///   factory MyWidget.factoryPreview() => const MyWidget.preview();

///

///   @Preview(name: 'Static preview')

///   static Widget previewStatic() => const Text('Static');

///

///   @override

///   Widget build(BuildContext context) {

///     return const Text('MyWidget');

///   }

/// }

/// ```

/// {@end-tool}

///

/// **Important Note:** all values provided to the `@Preview()` annotation must

/// be constant and non-private.

// TODO(bkonyi): link to actual documentation when available.

final class Preview {

  /// Annotation used to mark functions that return widget previews.

  const Preview({

    this.name,

    this.size,

    this.textScaleFactor,

    this.wrapper,

    this.theme,

    this.brightness,

    this.localizations,

  });

  /// A description to be displayed alongside the preview.

  ///

  /// If not provided, no name will be associated with the preview.

  final String? name;

  /// Artificial constraints to be applied to the previewed widget.

  ///

  /// If not provided, the previewed widget will attempt to set its own

  /// constraints.

  ///

  /// If a dimension has a value of `double.infinity`, the previewed widget

  /// will attempt to set its own constraints in the relevant dimension.

  ///

  /// To set a single dimension and allow the other to set its own constraints, use

  /// [Size.fromHeight] or [Size.fromWidth].

  final Size? size;

  /// Applies font scaling to text within the previewed widget.

  ///

  /// If not provided, the default text scaling factor provided by [MediaQuery]

  /// will be used.

  final double? textScaleFactor;

  /// Wraps the previewed [Widget] in a [Widget] tree.

  ///

  /// This function can be used to perform dependency injection or setup

  /// additional scaffolding needed to correctly render the preview.

  ///

  /// Note: this must be a reference to a static, public function defined as

  /// either a top-level function or static member in a class.

  // TODO(bkonyi): provide an example.

  final WidgetWrapper? wrapper;

  /// A callback to return Material and Cupertino theming data to be applied

  /// to the previewed [Widget].

  ///

  /// Note: this must be a reference to a static, public function defined as

  /// either a top-level function or static member in a class.

  final PreviewTheme? theme;

  /// Sets the initial theme brightness.

  ///

  /// If not provided, the current system default brightness will be used.

  final Brightness? brightness;

  /// A callback to return a localization configuration to be applied to the

  /// previewed [Widget].

  ///

  /// Note: this must be a reference to a static, public function defined as

  /// either a top-level function or static member in a class.

  final PreviewLocalizations? localizations;

}

@MultiPreview()[e]

The MultiPreview annotation class is an abstract base class which allows for developers to create their own preview annotations. This specifically allows for developers to define annotations that can be used to create multiple previews for a single widget.

 

/// The base class used to define a custom 'multi-preview' annotation.

///

/// Marking functions that return a widget preview with an instance of [MultiPreview] is the

/// equivalent of applying each [Preview] instance in the `previews` field to the function.

///

/// {@tool snippet}

/// This sample shows two ways to define multiple previews for a single preview function.

///

/// The first approach uses a [MultiPreview] implementation that creates previews using light and

/// dark mode themes.

///

/// The second approach uses multiple [Preview] annotations to achieve the same result.

///

/// ```dart

/// final class BrightnessPreview extends MultiPreview {

///   const BrightnessPreview();

///

///   @override

///   // ignore: avoid_field_initializers_in_const_classes

///   final List<Preview> previews = const <Preview>[

///     Preview(name: 'Light', brightness: Brightness.light),

///     Preview(name: 'Dark', brightness: Brightness.dark),

///   ];

/// }

///

/// // Using a multi-preview to create 'Light' and 'Dark' previews.

/// @BrightnessPreview()

/// WidgetBuilder brightnessPreview() {

///   return (BuildContext context) {

///     final ThemeData theme = Theme.of(context);

///     return Text('Brightness: ${theme.brightness}');

///   };

/// }

///

/// // Using multiple Preview annotations to create 'Light' and 'Dark' previews.

/// @Preview(name: 'Light', brightness: Brightness.light)

/// @Preview(name: 'Dark', brightness: Brightness.dark)

/// WidgetBuilder brightnessPreviewManual() {

///   return (BuildContext context) {

///     final ThemeData theme = Theme.of(context);

///     return Text('Brightness: ${theme.brightness}');

///   };

/// }

/// ```

/// {@end-tool}

///

/// **Important Note:** all values provided to the `Preview()` instances included in the

/// `previews` list must be constant and non-private.

abstract base class MultiPreview {

  /// Creates a [MultiPreview] annotation instance.

  const MultiPreview();

  /// The set of [Preview]s to be created for the annotated function.

  ///

  /// **Important Note:** this getter must be overridden with a field, not a getter, otherwise

  /// the list of [Preview]s will not be detected.

  List<Preview> get previews;

}

PreviewThemeData

Relevant PR: https://github.com/flutter/flutter/pull/167001 

The PreviewThemeData class is a data class used to provide ThemeData and CupertinoThemeData instances for light and dark mode to the preview.

/// A collection of [ThemeData] and [CupertinoThemeData] instances for use in

/// widget previews.

base class PreviewThemeData {

  /// Creates a collection of [ThemeData] and [CupertinoThemeData] instances

  /// for use in widget previews.

  ///

  /// If a theme isn't provided for a specific configuration, no theme data

  /// will be applied and the default theme will be used.

  const PreviewThemeData({

    this.materialLight,

    this.materialDark,

    this.cupertinoLight,

    this.cupertinoDark[f][g],

  });

  /// The Material [ThemeData] to apply when light mode is enabled.

  final ThemeData? materialLight;

  /// The Material [ThemeData] to apply when dark mode is enabled.

  final ThemeData? materialDark;

  /// The Cupertino [CupertinoThemeData] to apply when light mode is enabled.

  final CupertinoThemeData? cupertinoLight;

  /// The Cupertino [CupertinoThemeData] to apply when dark mode is enabled.

  final CupertinoThemeData? cupertinoDark;

  /// Returns the pair of [ThemeData] and [CupertinoThemeData] corresponding to

  /// the value of [brightness].

  (ThemeData?, CupertinoThemeData?) themeForBrightness(Brightness brightness) {

    if (brightness == Brightness.light) {

      return (materialLight, cupertinoLight);

    }

    return (materialDark, cupertinoDark);

  }

}

The theme brightness used by the preview is determined by the following (in order of precedence):

  • The value of brightness provided to @Preview(...)
  • The user’s system brightness

The brightness can be toggled from within the preview environment itself to quickly switch between light and dark modes.

PreviewLocalizationsData

Relevant PR: https://github.com/flutter/flutter/pull/169229 

The PreviewLocalizations class is a data class used to provide locale resolution information to the preview.

/// A collection of localization objects and callbacks for use in widget previews.

base class PreviewLocalizationsData {

  /// Creates a collection of localization objects and callbacks for use in

  /// widget previews.

  const PreviewLocalizationsData({

    this.locale,

    this.supportedLocales = const <Locale>[Locale('en', 'US')],

    this.localizationsDelegates,

    this.localeListResolutionCallback,

    this.localeResolutionCallback,

  });

  /// {@macro flutter.widgets.widgetsApp.locale}

  ///

  /// See also:

  ///

  ///  * [localeResolutionCallback], which can override the default

  ///    [supportedLocales] matching algorithm.

  ///  * [localizationsDelegates], which collectively define all of the localized

  ///    resources used by this preview.

  final Locale? locale;

  /// {@macro flutter.widgets.widgetsApp.supportedLocales}

  ///

  /// See also:

  ///

  ///  * [localeResolutionCallback], an app callback that resolves the app's locale

  ///    when the device's locale changes.

  ///  * [localizationsDelegates], which collectively define all of the localized

  ///    resources used by this app.

  ///  * [basicLocaleListResolution], the default locale resolution algorithm.

  final List<Locale> supportedLocales;

  /// The delegates for this preview's [Localizations] widget.

  ///

  /// The delegates collectively define all of the localized resources

  /// for this preview's [Localizations] widget.

  final Iterable<LocalizationsDelegate<Object?>>? localizationsDelegates;

  /// {@macro flutter.widgets.widgetsApp.localeListResolutionCallback}

  ///

  /// This callback considers the entire list of preferred locales.

  ///

  /// This algorithm should be able to handle a null or empty list of preferred locales,

  /// which indicates Flutter has not yet received locale information from the platform.

  ///

  /// See also:

  ///

  ///  * [basicLocaleListResolution], the default locale resolution algorithm.

  final LocaleListResolutionCallback? localeListResolutionCallback;

  /// {@macro flutter.widgets.widgetsApp.localeListResolutionCallback}

  ///

  /// This callback considers only the default locale, which is the first locale

  /// in the preferred locales list. It is preferred to set [localeListResolutionCallback]

  /// over [localeResolutionCallback] as it provides the full preferred locales list.

  ///

  /// This algorithm should be able to handle a null locale, which indicates

  /// Flutter has not yet received locale information from the platform.

  ///

  /// See also:

  ///

  ///  * [basicLocaleListResolution], the default locale resolution algorithm.

  final LocaleResolutionCallback? localeResolutionCallback;

}

The theme brightness used by the preview is determined by the following (in order of precedence):

  • The value of brightness provided to @Preview(...)
  • The user’s system brightness

The brightness can be toggled from within the preview environment itself to quickly switch between light and dark modes.

Possible future annotations

At the time of writing, the following annotations have not been designed or implemented, but would provide obvious QoL benefits[h]:

  • @GlobalPreviewThemeData():  would be applied to a single function returning PreviewThemeData, resulting in the returned theme data being applied to all previews in the project by default. The global theme can be overridden by providing the theme parameter to @Preview(...).
  • @GlobalPreviewLocalizationsData():  would be applied to a single function returning localization data, resulting in the returned locale being applied to all previews in the project by default. The global locale would be overridable by providing a locale parameter to @Preview(...).
  • @GlobalPreviewWrapper():  would be applied to a single function taking a Widget parameter and returning a Widget, resulting in the annotated function being invoked with the previewed Widget for each preview, allowing for developers to wrap each preview with common state (e.g., Providers). The global wrapper can be overridden by providing the wrapper parameter to @Preview(...).

Widget preview scaffold

The widget preview scaffold is a Flutter application that contains the infrastructure needed to host WidgetPreview instances in the preview environment. It’s automatically generated by the flutter widget-preview start command and performs the following functions:

  • Injects the list of WidgetPreview from lib/src/generated_preview.dart into a widget tree that includes a WidgetPreviewWindowConstraints widget to inform the individual previews of the preview environment’s size.
  • Provides a custom PlatformAssetBundle to allow for assets to be loaded from the developer’s project without any modification to asset paths in the project.

Handling assets

In order for assets from the developer’s project to be loaded properly by the preview scaffold without requiring developers to modify asset paths, the preview scaffold overrides the default asset bundle for each preview with the following:

/// Custom [AssetBundle] used to map original asset paths from the parent

/// projects to those in the preview project.

class PreviewAssetBundle extends PlatformAssetBundle {

  PreviewAssetBundle({required this.packageName});

  /// The name of the package in which a preview was defined.

  ///

  /// For example, if a preview is defined in 'package:foo/src/bar.dart', this

  /// will have the value 'foo'.

  ///

  /// This should only be null if the preview is defined in a file that's not

  /// part of a Flutter library (e.g., is defined in a test).

  // TODO(bkonyi): verify what the behavior should be in this scenario.

  final String? packageName;

  // Assets shipped via package dependencies have paths that start with

  // 'packages'.

  static const String _kPackagesPrefix = 'packages';

  // TODO(bkonyi): when loading an invalid asset path that doesn't start with

  // 'packages', this throws a FlutterError referencing the modified key

  // instead of the original. We should catch the error and rethrow one with

  // the original key in the error message.

  @override

  Future<ByteData> load(String key) {

    // These assets are always present or are shipped via a package and aren't

    // actually located in the parent project, meaning their paths did not need

    // to be modified.

    if (key == 'AssetManifest.bin' ||

        key == 'FontManifest.json' ||

        key.startsWith(_kPackagesPrefix) ||

        packageName == null) {

      return super.load(key);

    }

    // Other assets are from the parent project. Map their keys to package

    // paths corresponding to the package containing the preview.

    return super.load(_toPackagePath(key));

  }

  @override

  Future<ImmutableBuffer> loadBuffer(String key) async {

    if (kIsWeb) {

      final ByteData bytes = await load(key);

      return ImmutableBuffer.fromUint8List(Uint8List.sublistView(bytes));

    }

    return await ImmutableBuffer.fromAsset(

      key.startsWith(_kPackagesPrefix) ? key : _toPackagePath(key),

    );

  }

  String _toPackagePath(String key) => '$_kPackagesPrefix/$packageName/$key';

}

This PreviewAssetBundle automatically maps non-package asset paths within the developer’s project to package paths based on the package the preview was defined in. Using package asset paths allows for us to not have to include developer assets directly in the preview scaffold’s pubspec.yaml while also making it possible to support projects that are part of a pub workspace.

Handling MediaQueryData overrides[i][j]

Many of the arguments to @Preview(...) require overriding the MediaQueryData provided to the preview. Each previewed widget is wrapped in a WidgetPreviewMediaQueryOverride to apply overrides for properties like height, width, theme brightness, and text scaling factor:

/// Wraps the previewed [child] with the correct [MediaQueryData] overrides

/// based on [preview] and the current device [Brightness].

class WidgetPreviewMediaQueryOverride extends StatelessWidget {

  const WidgetPreviewMediaQueryOverride({

    super.key,

    required this.preview,

    required this.brightnessListenable,

    required this.child,

  });

  /// The preview specification used to render the preview.

  final WidgetPreview preview;

  /// The currently set brightness for this preview instance.

  final ValueListenable<Brightness> brightnessListenable;

  final Widget child;

  @override

  Widget build(BuildContext context) {

    return ValueListenableBuilder<Brightness>(

      valueListenable: brightnessListenable,

      builder: (context, brightness, _) {

        return MediaQuery(

          data: _buildMediaQueryOverride(

            context: context,

            brightness: brightness,

          ),

          child: child,

        );

      },

    );

  }

  MediaQueryData _buildMediaQueryOverride({

    required BuildContext context,

    required Brightness brightness,

  }) {

    var mediaQueryData = MediaQuery.of(

      context,

    ).copyWith(platformBrightness: brightness);

    if (preview.textScaleFactor != null) {

      mediaQueryData = mediaQueryData.copyWith(

        textScaler: TextScaler.linear(preview.textScaleFactor!),

      );

    }

    var size = Size(

      preview.width ?? mediaQueryData.size.width,

      preview.height ?? mediaQueryData.size.height,

    );

    if (preview.width != null || preview.height != null) {

      mediaQueryData = mediaQueryData.copyWith(size: size);

    }

    return mediaQueryData;

  }

}

Handling theming

Relevant PR: https://github.com/flutter/flutter/pull/167001 

In order to support themes, each previewed widget is wrapped in WidgetPreviewTheming. This applies Theme and CupertinoTheme to the previewed widget when necessary, choosing the correct themes for the current platform brightness (Note: this widget is always a descendant of WidgetPreviewMediaQueryOverride and can simply look up the correct preview brightness from MediaQuery). If no PreviewThemeData is provided, the default Material and Cupertino themes for the current platform brightness setting should be used.

/// Applies theming defined in [theme] to [child].

class WidgetPreviewTheming extends StatelessWidget {

  const WidgetPreviewTheming({

    super.key,

    required this.theme,

    required this.child,

  });

  final Widget child;

  /// The set of themes to be applied to [child].

  final PreviewThemeData? theme;

  @override

  Widget build(BuildContext context) {

    final themeData = theme;

    if (themeData == null) {

      return child;

    }

    final (materialTheme, cupertinoTheme) = themeData.themeForBrightness(

      MediaQuery.platformBrightnessOf(context),

    );

    Widget result = child;

    if (materialTheme != null) {

      result = Theme(data: materialTheme, child: result);

    }

    if (cupertinoTheme != null) {

      result = CupertinoTheme(data: cupertinoTheme, child: result);

    }

    return result;

  }

}

Handling localization

Relevant PR: https://github.com/flutter/flutter/pull/169229 

In order to support localization, each previewed widget is wrapped in WidgetPreviewLocalizations. This widget effectively wraps the preview with a Localizations widget, using LocalizationsResolver initialized with data from the provided PreviewLocalizationsData to provide the same localization resolution logic used by WidgetsApp.

If no PreviewLocalizationsData is provided, the localization applied to the widget preview scaffold will be applied to the preview (i.e., system defaults should be used).

/// Wraps [child] with a [Localizations] with localization data from

/// [localizationsData].

class WidgetPreviewLocalizations extends StatefulWidget {

  const WidgetPreviewLocalizations({

    super.key,

    required this.localizationsData,

    required this.child,

  });

  final PreviewLocalizationsData? localizationsData;

  final Widget child;

  @override

  State<WidgetPreviewLocalizations> createState() =>

      _WidgetPreviewLocalizationsState();

}

class _WidgetPreviewLocalizationsState

    extends State<WidgetPreviewLocalizations> {

  PreviewLocalizationsData get _localizationsData => widget.localizationsData!;

  late final LocalizationsResolver _localizationsResolver =

      LocalizationsResolver(

        supportedLocales: _localizationsData.supportedLocales,

        locale: _localizationsData.locale,

        localeListResolutionCallback:

            _localizationsData.localeListResolutionCallback,

        localeResolutionCallback: _localizationsData.localeResolutionCallback,

        localizationsDelegates: _localizationsData.localizationsDelegates,

      );

  @override

  void didUpdateWidget(WidgetPreviewLocalizations oldWidget) {

    super.didUpdateWidget(oldWidget);

    final PreviewLocalizationsData? localizationsData =

        widget.localizationsData;

    if (localizationsData == null) {

      return;

    }

    _localizationsResolver.update(

      supportedLocales: localizationsData.supportedLocales,

      locale: localizationsData.locale,

      localeListResolutionCallback:

          localizationsData.localeListResolutionCallback,

      localeResolutionCallback: localizationsData.localeResolutionCallback,

      localizationsDelegates: localizationsData.localizationsDelegates,

    );

  }

  @override

  Widget build(BuildContext context) {

    if (widget.localizationsData == null) {

      return widget.child;

    }

    return ListenableBuilder(

      listenable: _localizationsResolver,

      builder: (context, _) {

        return Localizations(

          locale: _localizationsResolver.locale,

          delegates: _localizationsResolver.localizationsDelegates.toList(),

          child: widget.child,

        );

      },

    );

  }

}

Handling unbounded widgets

As described in V1 of this document, unbounded widgets currently have constraints forced on them using some render object black magic that probably doesn’t work in all situations. This implementation will likely change, but options need to be investigated.

Handling errors: Widget tree construction

Relevant PR: https://github.com/flutter/flutter/pull/166005 

If the construction of a previewed widget tree results in an exception being thrown, a custom error widget (_WidgetPreviewErrorWidget at time of writing) is rendered in place of the previewed widget, providing information about the exception within the preview environment itself.

For example, when a user attempts to call Directory.current in a widget constructor (an unsupported operation in the context of Flutter Web), the following is displayed in the widget previewer:

Handling errors: Layout and rendering

If a previewed widget results in a rendering or layout error, the usual ErrorWidget will be rendered within the preview card.[k]

Handling changes to widget state: “Soft restart”

Relevant PR: https://github.com/flutter/flutter/pull/166846 

In the situation where a change is made that isn’t picked up by hot reload (e.g., initState() in a State object is updated with new logic), users are typically required to perform a hot restart to rebuild the state. In order to prevent the users from having to perform a hot restart on the entire widget preview environment[l], each rendered preview is able to perform a “soft restart” on the previewed widget:

A “soft restart” simply removes the previewed widget from the widget tree for a single frame before reinserting it, causing the state to be reinitialized. This is enough to resolve most issues which would normally require a hot restart, but users will also be provided a way to perform an actual hot restart on the environment to cover the remaining cases.

IDE support

The IDE plugins will be responsible for starting the preview environment for the active project using the flutter widget-preview start command. The IDE will provide the –dtd-uri flag with a URI pointing to the DTD instance hosted by the IDE, allowing for the widget previewer to listen for events and adjust its UI accordingly (e.g., we may only want to display previews defined in the currently focused source file in the IDE).

Once the widget previewer is started, the IDE will open it within a webview hosted in the IDE, similar to how Dart/Flutter DevTools is embedded in IDEs. This is currently blocked as DWDS (the web VM service implementation) requires an open Chrome Debug port to run, which isn’t available in IDEs like VSCode. DWDS is required for hot reload support, so there is an ongoing effort to support running DWDS without a Chrome Debug port while maintaining hot reload support (dart-lang/webdev#2605).

Analyzer support

Relevant CLs: https://dart-review.googlesource.com/c/sdk/+/423860 and https://dart-review.googlesource.com/c/sdk/+/422520 

The Dart analyzer has been updated to include warnings for:

  • Invalid applications of the @Preview(...) annotation, including but not limited to functions that:
  • Are not top-level or defined in a class
  • Are non-static
  • Are private
  • Have required parameters
  • Are abstract or external
  • Do not return subtypes of Widget or WidgetBuilder
  • Invalid use of private symbols when providing arguments to @Preview(...)

OPEN QUESTIONS[m][n]

[o]
  • How do previews handle widgets that rely on native plugins? What’s the best failure mode?
  • How do we communicate limitations of the environment to developers? In particular, no access to dart:io, native plugins, etc.

TESTING PLAN

Testing will be done using the existing Flutter infrastructure, with related widgets being tested in the framework tests, flutter widget-preview functionality tested in the flutter_tools test suites, etc.

DOCUMENTATION PLAN

TBD

Prior Versions

Architecture V1

SUMMARY

Implementing Widget Previews for Flutter

Author: Ben Konyi (bkonyi)

Go Link: flutter.dev/go/widget-previews-architecture

Created: November 2024   /  Last updated: December 2024

WHAT PROBLEM IS THIS SOLVING?

While one of the main selling points of Flutter is its rapid iterative development cycle that’s enabled by hot reload, it does require developers to have an active target device or simulator to work. In addition, it only allows for viewing the UI in a single configuration at a time without running an application on multiple devices, making it difficult for developers to visually verify the impact of variables like screen size, orientation, font size, and locale on their application.

See https://github.com/flutter/flutter/issues/115704, and https://forum.itsallwidgets.com/t/support-for-preview-feature-in-flutter/

Flutter Widget Previews is a development tool designed to improve the Flutter development workflow by allowing developers to quickly visualize and iterate on their widgets in various configurations without running the full application. This tool aims to significantly reduce development time and improve the overall developer experience by providing a fast and convenient way to experiment with widget variations, themes, and different device settings.

BACKGROUND

An interesting feature provided by both Jetpack Compose and SwiftUI is the ability to preview components of an application’s UI in the IDE. Developers can specify which components should be previewed and create wrapper components that provide mock data to the UI. Individual components can have multiple previews with different configurations that are rendered in the same previewer, making them easy to compare.

Example of a Jetpack Compose component preview from Android Studio. This previews a single composable with multiple configurations applied, including locale and different font sizes.[p]

XCode’s SwiftUI preview

Alongside its view preview, XCode also provides support for editing properties of Views using a UI, applying changes to the code and the preview instantly.

Audience

Flutter framework contributors, Dart + Flutter developer experience engineers.

Glossary

  • Widget Preview: an isolated widget displayed in a preview environment for use in development workflows
  • Preview Scaffold: the generated Flutter application used to display widget previews defined in a project
  • Preview Environment: the native Flutter desktop application hosting the preview scaffold
  • Preview Viewer: a Flutter web application that streams frames from and user interactions to the preview environment

OVERVIEW

This section aims to describe the overall architecture of the Widget Preview feature, as well as some examples of how developers could possibly create previews in their projects.

Non-goals

The exact UI provided by the preview scaffold and how widget previews will be defined in code are still in development and will not be explored deeply in this document at this time.

USAGE EXAMPLES

Widget previews will be defined directly in the developer’s project as a top-level function marked with an annotation:

@Preview()

List<WidgetPreview> myFirstPreview() {

 return <WidgetPreview>[

     WidgetPreview(

       name: 'Full App Preview',

       height: 700,

       device: Devices.ios.all.first,

       child: GalleryApp(),

     ),

 ];

}

Developers can then interact with their previews in the preview environment embedded in their IDE. Widget previews are rendered in a Flutter application, meaning they are fully interactive and can be used to preview UI layouts and animations.

Early Widget Preview prototype that showcases displaying a simple application in device frames for multiple platforms

Early Widget Preview prototype that showcases previewing a full application and individual UI element within an IDE, streamed from a Flutter Desktop application to a web-based viewer.

Important Note: the frame rate of these GIFs are not representative of the actual frame rate of the preview environment, which renders previews at ~60 FPS.

Previews support zooming and panning, making it easy to inspect UIs at a pixel-level. Developers can also make use of package:device_frame to wrap their widgets in a device frame which renders the widget using the device’s display characteristics.

DETAILED DESIGN

The Widget Previews feature involves multiple components spanning the Flutter project. This includes:

  • The addition of a flutter widget-preview command to the Flutter tool, which is responsible for generating the preview scaffold for a project and interacting with the preview environment
  • The addition of widgets and classes needed to define widget previews to the framework
  • IDE plugin support for VSCode and Intellij, which involves streaming frames and interactions between a web based viewer and the preview environment[q]

flutter widget-preview

The flutter widget-preview will be responsible for creating the preview scaffold and managing the preview environment for a given project.

Generating the initial preview environment

The first time the command is run for a project on the user’s machine, the tool will perform the following tasks:

  1. Create a new Flutter project (currently named preview_scaffold) under the .dart_tool directory. This is currently configured as a Flutter Desktop application.
  2. Overwrite lib/main.dart with the preview scaffolding entrypoint capable of hosting widget previews from the developer’s project. This file imports lib/generated_preview.dart, which will contain a function that returns the set of widget previews for the project.
  3. Initialize preview_scaffold’s pubspec.yaml, adding path dependencies on the developer’s project as well as listing the assets in the developer’s project. This step also handles importing package:flutter_gen for localization support, although this may not be needed in the future.
  4. Using package:analyzer, perform a search for instances of the @Preview() annotation on top-level functions that return List<WidgetPreview>, recording the preview function names and their libraries.
  5. Generates lib/generated_preview.dart using the results of the search for previews in the developer’s project. package:code_builder is used to generate code that imports and calls each of the widget preview functions to return a list of previews. For example:

// ignore_for_file: no_leading_underscores_for_library_prefixes

import 'package:gallery/main.dart' as _i1;

import 'package:gallery/demos/material/list_demo.dart' as _i2;

import 'package:flutter/widget_preview.dart';

List<WidgetPreview> previews() => [

     ..._i1.preview(),

     ..._i2.preview(),

   ];

Running and managing the preview environment

Once the preview scaffold has been generated and built, it’s compiled and run with --machine provided. At this point, the Flutter tool will initialize a file watcher on the developer’s project directory to detect changes to source code.

The analyzer API is used to detect the addition or removal of widget preview definitions in updated files, regenerating lib/generated_preview.dart when necessary. At this point the Flutter tool will communicate with the preview environment using the Flutter tools daemon protocol to trigger a hot reload, resulting in the preview environment picking up the changes made in the developer’s project.

Before shutting down, the Flutter tool will clean up the preview environment.

Widgets and Annotations

As mentioned in the previous section, the flutter widget-preview command uses the analyzer APIs to detect @Preview() annotations on functions that return List<WidgetPreview>.

@Preview()

Currently, the Preview annotation class is simply a marker indicating that the following function should be imported and displayed by the preview environment. In the future, this annotation could be used to specify properties that should be applied to the contents of the widget preview (e.g., locales, theming details, etc.) or generate multiple previews programmatically (see the Jetpack Composable Previews Preview annotation for some examples).

/// Annotation used to mark functions that return widget previews.

class Preview {

 const Preview();

}

WidgetPreview

The WidgetPreview class is a wrapper that initializes various states and properties that allows for widgets to be rendered in the widget preview environment. This class (currently) has the following interface:

class WidgetPreview {

 const WidgetPreview({

   required this.child,

   this.name,

   this.width,

   this.height,

   this.device,

   this.orientation,

   this.textScaleFactor,

   this.platformBrightness,

 });

 /// A description to be displayed alongside the preview.

 final String? name;

 /// The [Widget] to be rendered in the preview.

 final Widget child;

 /// Artificial constraints to be applied to the [child].

 final double? width;

 final double? height;

 /// An optional device configuration.

 final DeviceInfo? device;

 /// The orientation of [device].

 final Orientation? orientation;

 /// Applies font scaling to text within the [child].

 final double? textScaleFactor;

 /// Light or dark mode (defaults to platform theme).

 final Brightness? platformBrightness;

}

This interface allows for developers to specify properties like:

  • A description to be displayed alongside the preview in the preview environment.
  • The height and width of the preview, which overrides the size returned by MediaQuery to allow for previewing adaptive UIs.
  • A device from package:device_frame[r][s][t], which renders the previewed widget in a device frame with the correct display properties applied. An orientation can also be applied to specify whether or not the device should be initially displayed in landscape or portrait mode.
  • Text scaling factors to adjust default font scaling behavior.
  • The platform brightness to control theming selection (e.g., light vs dark mode).

This interface may also include additional properties related to locale selection and custom theming, but this still needs to be explored.

Handling Unconstrained Widgets[u][v][w]

When WidgetPreview is used to wrap widgets, it uses a SingleChildRenderObjectWidget named WidgetPreviewWrapper to wrap the widget in a custom WidgetPreviewWrapperBox RenderBox. This WidgetPreviewWrapper is used to create a custom render object that determines whether or not the wrapped widget has an unconstrained height:

/// Wrapper applying a custom render object to force constraints on

/// unconstrained widgets.

@visibleForTesting

class WidgetPreviewWrapper extends SingleChildRenderObjectWidget {

 @visibleForTesting

 // ignore: public_member_api_docs

 const WidgetPreviewWrapper({

   super.key,

   super.child,

   required this.previewerConstraints,

 });

 /// The ratio of the max height provided by a parent

 /// [WidgetPreviewerWindowConstraints] that an unconstrained child should

 /// be allowed to occupy.

 @visibleForTesting

 static const double unconstrainedChildScalingRatio = 0.5;

 /// The size of the previewer render surface.

 final BoxConstraints previewerConstraints;

 @override

 RenderObject createRenderObject(BuildContext context) {

   return WidgetPreviewWrapperBox(

     previewerConstraints: previewerConstraints,

     child: null,

   );

 }

 @override

 void updateRenderObject(

   BuildContext context,

   WidgetPreviewWrapperBox renderObject,

 ) {

   renderObject.setPreviewerConstraints(previewerConstraints);

 }

}

/// Custom render box that forces constraints onto unconstrained widgets.

@visibleForTesting

class WidgetPreviewWrapperBox extends RenderShiftedBox {

 // ignore: public_member_api_docs

 WidgetPreviewWrapperBox({

   required RenderBox? child,

   required BoxConstraints previewerConstraints,

 })  : _previewerConstraints = previewerConstraints,

       super(child);

 BoxConstraints _constraintOverride = const BoxConstraints();

 BoxConstraints _previewerConstraints;

 /// Updates the constraints for the child based on changes to the constraints

 /// provided by the parent [WidgetPreviewerWindowConstraints].

 void setPreviewerConstraints(BoxConstraints previewerConstraints) {

   if (_previewerConstraints == previewerConstraints) {

     return;

   }

   _previewerConstraints = previewerConstraints;

   markNeedsLayout();

 }

 @override

 void layout(

   Constraints constraints, {

   bool parentUsesSize = false,

 }) {

   if (child != null && constraints is BoxConstraints) {

     double minInstrinsicHeight;

     try {

       minInstrinsicHeight = child!.getMinIntrinsicHeight(

         constraints.maxWidth,

       );

     } on Object {

       minInstrinsicHeight = 0.0;

     }

     // Determine if the previewed widget is vertically constrained. If the

     // widget has a minimum intrinsic height of zero given the widget's max

     // width, it has an unconstrained height and will cause an overflow in

     // the previewer. In this case, apply finite constraints (e.g., the

     // constraints for the root of the previewer). Otherwise, use the

     // widget's actual constraints.

     _constraintOverride = minInstrinsicHeight == 0

         ? _previewerConstraints

         : const BoxConstraints();

   }

   super.layout(

     constraints,

     parentUsesSize: parentUsesSize,

   );

 }

 @override

 void performLayout() {

   final RenderBox? child = this.child;

   if (child == null) {

     size = Size.zero;

     return;

   }

   final BoxConstraints updatedConstraints =

       _constraintOverride.enforce(constraints);

   child.layout(

     updatedConstraints,

     parentUsesSize: true,

   );

   size = constraints.constrain(child.size);

 }

}

If the wrapped widget is constrained, WidgetPreviewWrapper has no effect. However, in the case where the widget is unconstrained (i.e., where its minimum intrinsic height is zero for a given maximum width constraint), WidgetPreviewWrapper forces vertical constraints on the widget based on the height of the preview environment, which is made available through WidgetPreviewerWindowConstraints:

/// An [InheritedWidget] that propagates the current size of the

/// WidgetPreviewScaffold.

///

/// This is needed when determining how to put constraints on previewed widgets

/// that would otherwise have infinite constraints.

class WidgetPreviewerWindowConstraints extends InheritedWidget {

 /// Propagates the current size of the widget previewer.

 const WidgetPreviewerWindowConstraints({

   super.key,

   required super.child,

   required BoxConstraints constraints,

 }) : _constraints = constraints;

 final BoxConstraints _constraints;

 /// Returns the constraints representing the current size of the widget previewer.

 static BoxConstraints getRootConstraints(BuildContext context) {

   final WidgetPreviewerWindowConstraints? result = context

       .dependOnInheritedWidgetOfExactType<WidgetPreviewerWindowConstraints>();

   assert(

     result != null,

     'No WidgetPreviewerWindowConstraints founds in context',

   );

   return result!._constraints;

 }

 @override

 bool updateShouldNotify(WidgetPreviewerWindowConstraints oldWidget) {

   return oldWidget._constraints != _constraints;

 }

}

Widget Preview Scaffold

The widget preview scaffold is a Flutter application that contains the infrastructure needed to host WidgetPreview instances in the preview environment. It’s automatically generated by the flutter widget-preview command and performs the following functions:

  • Injects the list of WidgetPreview from lib/generated_preview.dart into a widget tree that includes a WidgetPreviewWindowConstraints widget to inform the individual previews of the preview environment’s size.
  • Provides a custom PlatformAssetBundle to allow for assets to be loaded from the developer’s project without any modification to asset paths in the project.
  • Starts a web server which:
  • Exposes an “interaction protocol” that allows for remotely interacting with the preview environment using JSON RPC invocations
  • Streams frames via a web socket connection from the preview environment for display in a client
  • Note: this functionality is required for embedding previews in IDEs. See the IDE Support section for more details.

Handling Assets

Assets from the developer’s project are included in the preview scaffold project’s pubspec.yaml using relative paths. In order for assets to be loaded properly by the preview scaffold without requiring developers to modify asset paths, the preview scaffold overrides the default asset bundle for the application with the following:

/// Custom [AssetBundle] used to map original asset paths from the parent

/// project to those in the preview project.

class PreviewAssetBundle extends PlatformAssetBundle {

 // Assets shipped via package dependencies have paths that start with

 // 'packages'.

 static const String _kPackagesPrefix = 'packages';

 @override

 Future<ByteData> load(String key) {

   // These assets are always present or are shipped via a package and aren't

   // actually located in the parent project, meaning their paths did not need

   // to be modified.

   if (key == 'AssetManifest.bin' ||

       key == 'AssetManifest.json' ||

       key == 'FontManifest.json' ||[x][y][z][aa]

       key.startsWith(_kPackagesPrefix)) {

     return super.load(key);

   }

   // Other assets are from the parent project. Map their keys to those found

   // in the pubspec.yaml of the preview environment.

   return super.load('../../$key');

 }

 @override

 Future<ImmutableBuffer> loadBuffer(String key) async {

   return await ImmutableBuffer.fromAsset(

     key.startsWith(_kPackagesPrefix) ? key : '../../$key',

   );

 }

}

This PreviewAssetBundle automatically maps non-package asset paths within the developer’s project to the relative paths found in the preview scaffold’s pubspec.yaml.

Remote actions using the Interaction Protocol[ab]

The interaction protocol allows for the following interactions to be forwarded to the preview environment:

  • Pointer location
  • Hover location
  • Tap up / down
  • Scrolling with a mouse wheel
  • Scrolling with a trackpad
  • Keypress events (down, up, repeated)
  • Window size changes

Pointer and mouse related interactions are replayed by the preview environment using an instance of LiveWidgetController and TestPointer from package:flutter_test.

Key presses and text input are forwarded to both the HardwareKeyboard instance and a custom PreviewTextInput class, which handles forwarding virtual keypress events to active text input clients (e.g., a selected TextField) by registering itself as a TextInputControl.

When the preview viewer size changes, it notifies the preview environment that it needs to resize its RenderView and rebuild its UI via the custom PreviewWidgetsFlutterBinding, which extends WidgetsFlutterBinding:

/// A custom [WidgetsFlutterBinding] that allows for changing the size of the

/// root [RenderView] to match the size of the preview viewer window.

class PreviewWidgetsFlutterBinding extends WidgetsFlutterBinding {

 /// ...

 Size _physicalConstraints = Size.zero;

 Size _logicalConstraints = Size.zero;

 double _devicePixelRatio = 0.0;

 /// Sets the size of the root [RenderView].

 ///

 /// Calling this method has the same effect as changing the size of a window,

 /// invoking [handleMetricsChanged] and rebuilding the [ViewConfiguration]

 /// for each [RenderView].

 void setViewSize({

   required Size physicalConstraints,

   required Size logicalConstraints,

   required double devicePixelRatio,

 }) {

   _assertBindingInitialized();

   _physicalConstraints = physicalConstraints;

   _logicalConstraints = logicalConstraints;

   _devicePixelRatio = devicePixelRatio;

   handleMetricsChanged();

 }

 @override

 ViewConfiguration createViewConfigurationFor(RenderView renderView) {

   _assertBindingInitialized();

   final ViewConfiguration config = ViewConfiguration(

     physicalConstraints: BoxConstraints.expand(

       width: _physicalConstraints.width,

       height: _physicalConstraints.height,

     ),

     logicalConstraints: BoxConstraints.expand(

       width: _logicalConstraints.width,

       height: _logicalConstraints.height,

     ),

     devicePixelRatio: _devicePixelRatio,

   );

   return config;

 }

 /// ...

}

Frame Streaming

When a client connects to the preview environment’s web server, the server immediately registers a persistent frame callback responsible for capturing each frame as it’s rendered. The captured frame is then forwarded to the client over the web socket connection:

 /// Sends the current frame to the preview viewer for rendering.

 Future<void> sendFrame() async {

   if (_sendingFrame) {

     return;

   }

   _sendingFrame = true;

   final RenderView renderView =

       WidgetsBinding.instance.rootElement!.renderObject! as RenderView;

   final OffsetLayer layer = renderView.debugLayer! as OffsetLayer;

   final ui.Image image = await layer.toImage(

     Offset.zero & (renderView.size * renderView.flutterView.devicePixelRatio),

   );

   final Uint8List data = (await image.toByteData())!.buffer.asUint8List();

   image.dispose();

   _client.sendFrame(frame: data);

   _sendingFrame = false;

 }

 

IDE Support

The IDE plugins will be responsible for starting the preview environment for the active project using the flutter widget-preview command.

In order to display the contents of the preview environment within IDEs, the preview environment needs to stream frames and interaction events to and from a web based application (VSCode only supports embedding web-based tooling). This web application will be a minimalistic Flutter web application that renders frames sent by the preview environment and capture and forward user interactions (e.g., cursor movement, clicks, scrolling, and key presses) using existing widgets such as KeyboardListener and Listener.

Streaming will be done over a websocket connection using the JSON RPC protocol to communicate user interactions to the preview environment.

OPEN QUESTIONS

  • Where should the Preview, WidgetPreview, and custom widgets required by WidgetPreview live?
  • Ideally these widgets are made available in a core Flutter package shipped with the framework as these widgets are meant to live in developer’s application code. They shouldn’t be shipped separately alongside the preview scaffolding code as this will result in users using WidgetPreview adding multiple unnecessary transitive dependencies to their projects (e.g., package:json_rpc_2, package:code_builder, etc).
  • How will the preview environment be shipped?
  • The prototype generates and builds a Flutter Desktop application to host the preview scaffold[ac][ad], which requires users to have support for building Flutter Desktop applications to use the widget preview feature. This isn’t ideal.
  • Using the existing flutter_tester device to host the preview scaffold works and would be ideal, but it currently only supports software rendering which results in poor frame rates. How difficult would it be to add hardware rendering support to the tester?[ae]
  • We don’t want the preview environment application to pop up in a window outside of IDEs which will involve modifying the preview environment code to not create a window. How difficult will this be?[af]

TESTING PLAN

Testing will be done using the existing Flutter infrastructure, with related widgets being tested in the framework tests, flutter widget-preview functionality tested in the flutter_tools test suites, etc.

DOCUMENTATION PLAN

TBD

[a]There's a few other files here with utilities, but they aren't worth explicitly mentioning here

[b]I’ve noticed that @Preview and @UseCase (from the Widgetbook package) are conceptually very close, which is promising. One point of friction is the repetitive definition of theme parameters — something we’re glad to see you're addressing with upcoming QoL annotations.

However, Widgetbook is often used with custom design systems, so lack of generic theme support could limit the adoption of Preview. Do you have a timeline for that?

[c]I definitely want to make sure we can support generic theming, especially with our upcoming work to decouple material and cupertino from the core framework: flutter.dev/go/decouple-design

I'm not sure what our theming story is going to look like in the future, but I'd be interested in hearing if/how Widgetbook handles this. I'm also more than happy to take suggestions or take some time to meet and discuss!

[d]Great to hear! And I love the decision to decouple material and Cupertino from the flutter core!

Sure, I‘d be happy to share it with you. Are you up for a call next Monday at 10:00am Ontario time? Alternatively, please feel free to choose a time slot in my calendar: https://cal.com/lucasjosefiak/30min

[e]TODO: add an example with screenshots

[f]I wish we could make these more generic - it's something we have on the roadmap for the framework so design languages other than material and cupertino can have a solid theming story. I don't think we can do anything about this now, but maybe later this can be refactored to a builder that returns the generic theme, which I imagine will be a super class of material and cupertino themes.

[g]I totally agree. I _really_ didn't want to pull in dependencies on both cupertino and material, but given there was no common ancestor of ThemeData and CupertinoThemeData, there really wasn't any way around it. We should be able to evolve this interface in the future once we're better at supporting arbitrary design languages.

[h]2 total reactions

Kate Lovett reacted with 💯 at 2025-04-29 16:38 PM

Jens Horstmann reacted with 💯 at 2025-07-31 10:05 AM

[i]Maybe for a later feature, I wonder how this impacts things built in to widgets like safe area, and whether or not developers will want to mock out notches and the like in previews. Dunno if there will be a high demand for it

[j]Developers should still be able to do this when defining their previews. package:device_frame also supports notches and other safe areas for predefined devices and we'd like to provide similar functionality at some point.

[k]We might want to look into using a custom error widget that matches the theming of the preview environment, but ErrorWidget has the benefit of being recognizable and familiar to existing developers

1 total reaction

Kate Lovett reacted with 👍 at 2025-04-29 16:59 PM

[l]This would result in the widget previewer losing its state, which could lead to a poor UX due to loss of navigation state. We could also consider serializing important state before performing a hot restart on the entire environment and reinitialize using that state after the restart.

[m]Can third-party tools like Widgetbook integrate with Widget Preview? If so, how?

Our main goal: Enabling integrators like us to generate a Widgetbook from code already written for Widget Preview, without requiring developers to define use cases manually. This idea aligns closely with your current work on the @Preview annotation and syntax.

[n]I'll add some more comments which I hope will be useful. I'm speaking from our experience building Widgetbook.

What we love about Preview:

- Widgets can be displayed in multiple configurations, locales, and themes simultaneously — something not currently possible with Widgetbook.

- The @Preview annotation is very similar to Widgetbook’s @UseCase, which our community is already comfortable with.

- The integration into the dev workflow via the generated .dart_tool preview project is seamless.

- IDE support eliminates the need for running a simulator.

What we think is missing:

A component catalog for:

- Stakeholders outside of development

- Browsing and discovery of existing widgets

(We understand this isn’t a core focus, but believe generating one would be feasible with small changes to @Preview.)

Stronger support for:

- Devices, locales, and generic themes

- Runtime property configuration (e.g., knobs in Widgetbook) to reduce the need for multiple preview methods.

- Customization of the widget tree to inject theming, locales, and especially utilities (e.g., Widgetbook addons).

[o]I think it would be great if widget preview were integrated with MCP, so the AI can evaluate the design

Like what the Dart MCP server did

[p]the best and worst part of Compose is that it forces you to mock a ton of things (which is a bit boring at first) but consequently forces you to have a better codebase (and you reuse the mocks on tests). So it is a bit "more work" than "automatic", but the benefits are great.

7 total reactions

Victor Eronmosele reacted with 👍 at 2024-11-22 22:53 PM

Taha Tesser reacted with 👍 at 2024-11-26 16:12 PM

Mateusz Wojtczak reacted with 👍 at 2024-11-27 13:27 PM

Ariba Hussain reacted with 👍 at 2024-12-03 16:37 PM

Lucas Josefiak reacted with 👍 at 2025-01-10 08:59 AM

Brylie Oxley reacted with 👍 at 2025-02-07 09:57 AM

Anupam Gupta reacted with 👍 at 2025-06-23 07:21 AM

[q]We're no longer taking this approach and will focus on running in a standalone desktop application for now with the eventual goal of moving to use Flutter web.

[r]The device_frame package is a third-party package, though. According to pub, the last update was 5 months ago. I worry that this will cause trouble in the future, especially since now the flutter tool will indirectly depend on it.

[s]I share the same concerns, so rest assured that we won't move forward with depending on this package unless we know it'll be supported in one way or another and won't cause issues in the future. I plan on reaching out to the project maintainer to see what their plans are and discuss whether or not we can pull some base interfaces into the framework.

1 total reaction

Lucas Josefiak reacted with 👍 at 2025-01-10 09:12 AM

[t]We likely won't have built-in support for device frames at launch but will investigate this further.

[u]After discussion with @goderbauer@google.com, we've decided to take a different approach for handling unconstrained widgets. I'll update this section once the prototype is updated.

[v]_Marked as resolved_

[w]_Re-opened_

[x]Don't forget the upcoming native assets manifest, from https://github.com/flutter/flutter/pull/159322

[y]I literally just reviewed that code after I wrote this document, but thanks for the reminder! :)

[z]_Marked as resolved_

[aa]_Re-opened_

[ab]We're no longer going to take a streaming approach as we aim to ship this feature with Flutter web when we can.

[ac]would WASM solve this?

[ad]Using Flutter web in general would solve this, but we require hot reload support to make it work which is the main blocker for that approach.

[ae]This is not going to happen.

[af]We're no longer going to stream frames, so we'll have to deal with a separate Flutter desktop application window until we can ship with Flutter web.