Architecture V2 (WIP)
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
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.
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.
Flutter framework contributors, Dart + Flutter developer experience engineers.
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.
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.
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
}
The Widget Previews feature involves multiple components spanning the Dart and Flutter projects. This includes:
The flutter widget-preview start command will be responsible for creating the preview scaffold and managing the preview environment for a given project.
The first time the command is run for a project on the user’s machine, the tool will perform the following tasks:
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(),
),
];
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.
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.
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.
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.
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
The Preview annotation class is used to mark a function or constructor as a widget preview.
This annotation currently allows for developers to provide:
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;
}
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;
}
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,
});
/// 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 brightness can be toggled from within the preview environment itself to quickly switch between light and dark modes.
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 brightness can be toggled from within the preview environment itself to quickly switch between light and dark modes.
At the time of writing, the following annotations have not been designed or implemented, but would provide obvious QoL benefits[h]:
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:
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.
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;
}
}
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;
}
}
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,
);
},
);
}
}
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.
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:
If a previewed widget results in a rendering or layout error, the usual ErrorWidget will be rendered within the preview card.[k]
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.
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).
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:
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.
TBD
Prior Versions
Architecture V1
Implementing Widget Previews for Flutter
Author: Ben Konyi (bkonyi)
Go Link: flutter.dev/go/widget-previews-architecture
Created: November 2024 / Last updated: December 2024
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.
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.
Flutter framework contributors, Dart + Flutter developer experience engineers.
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.
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.
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.
The Widget Previews feature involves multiple components spanning the Flutter project. This includes:
The flutter widget-preview will be responsible for creating the preview scaffold and managing the preview environment for a given project.
The first time the command is run for a project on the user’s machine, the tool will perform the following tasks:
// 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(),
];
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.
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>.
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();
}
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:
This interface may also include additional properties related to locale selection and custom theming, but this still needs to be explored.
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;
}
}
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:
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.
The interaction protocol allows for the following interactions to be forwarded to the preview environment:
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;
}
/// ...
}
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;
}
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.
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.
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.