flutter_2yaml

pub package pub points License: MIT Dart 3 Platform

Convert Flutter .dart widget files into compact YAML representations optimized for LLM/AI token consumption. Supports bidirectional conversion — Dart to YAML and YAML back to Dart.

Why?

When feeding Flutter code to AI models, full .dart files waste tokens on boilerplate — imports, @override, super.key, BuildContext context, const keywords, semicolons, brackets. flutter_2yaml strips all that and produces structured YAML using CSS-like shorthands, pipe syntax, and arrow notation.

Measured across the fixtures in this repo, that saves 40-65% of tokens — around 50% on average at the default standard level, more on simple screens and at --level minimal, less on property-dense ones.

Before (Dart — 45 lines)

class SplashScreen extends StatefulWidget {
  const SplashScreen({super.key});
  @override
  State<SplashScreen> createState() => _SplashScreenState();
}

class _SplashScreenState extends State<SplashScreen> {
  bool _isLoading = true;

  @override
  void initState() {
    super.initState();
    Future.delayed(const Duration(seconds: 3), () {
      Navigator.pushReplacementNamed(context, '/home');
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      backgroundColor: Colors.white,
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            Image.asset('assets/logo.png', width: 200, height: 200),
            const SizedBox(height: 20),
            if (_isLoading)
              const CircularProgressIndicator(color: Colors.blue),
          ],
        ),
      ),
    );
  }
}

After (YAML — 14 lines, standard level)

page: SplashScreen
type: StatefulWidget
state: [_isLoading: bool = true]
lifecycle:
  initState: [Future.delayed(3s)]
build:
  Scaffold:
    bg: white
    body:
      Center:
        child:
          Column(center):
            children:
              - Image.asset: assets/logo.png | 200x200
              - SizedBox: { h: 20 }
              - if _isLoading: CircularProgressIndicator: { color: blue }

Compact Format Features

Pipe Syntax |

Text: "Hello World" | 20 | bold | white    # text | fontSize | fontWeight | color
Image.asset: logo.png | 200x200            # source | dimensions
Icon: search | onTap → goSearch()          # icon | callback

Icon takes no callbacks of its own, so an icon with a callback is generated as the widget that does: onPressed becomes an IconButton, any other callback a GestureDetector wrapping the Icon.

CSS-Like Shorthands

Shorthand Flutter Equivalent
bg backgroundColor / color (in BoxDecoration)
br borderRadius: BorderRadius.circular(N)
p padding: EdgeInsets.all(N)
px, py EdgeInsets.symmetric(horizontal/vertical: N)
h, w height, width
full double.infinity
t, l, r, b top, left, right, bottom (Positioned)
gap, runGap spacing, runSpacing (Wrap)
shadow boxShadow: [BoxShadow(...)]
border Border.all(...){c: color, w: width}

Arrow Callback Notation

onTap → goSearch()          # onTap: () => controller.goSearch()
onPressed → handleSubmit    # onPressed: handleSubmit

Parenthetical Alignment

Row(spaceBetween)     # Row(mainAxisAlignment: MainAxisAlignment.spaceBetween)
Column(start)         # Column(crossAxisAlignment: CrossAxisAlignment.start)
Column(center)        # Column(mainAxisAlignment: MainAxisAlignment.center)

Scaffold Named Children

Scaffold:
  bg: white
  appBar:
    AppBar: { title: Text: "Home" | 20 | bold, bg: blue }
  drawer:
    Drawer: { ... }
  body:
    Center: { ... }
  floatingActionButton:
    FloatingActionButton: { onPressed → add(), child: Icon: add }
  bottomNavigationBar:
    BottomNavigationBar: { ... }

Auto-Detection

  • Page vs Widget: page: for Scaffold-containing widgets, widget: for components
  • State Management: Auto-detects GetX, Riverpod, Bloc, Provider, MobX from imports
  • Color Shorthand: Colors.blueblue, Color(0xFF123456)#123456
  • Icon Shorthand: Icons.menumenu, Icons.searchsearch
  • Dimension Shorthand: width: 80, height: 8080x80, double.infinityfull
  • Theme Shorthand: Theme.of(context).textTheme.headlinetheme.headline
  • MediaQuery Shorthand: MediaQuery.of(context).size.widthscreen.w

Component References (figma2flutter)

children:
  - <QuickAcess:Property 1=Default>           # Component with variant
  - <B-NavBar:Status=Home, Mode=Light>        # Multi-property variant
  - <B-NavBar>                                # Component without variant

Component tags (<>) are generated by the figma2flutter plugin. Full resolution requires the MCP server. Offline reverse produces Text('ComponentName (Variant)') placeholders with a warning.

Collection Elements

children:
  - ...items                                  # Spread operator
  - for(item in products): ProductCard: {}    # For-in loop
  - if _isLoading: CircularProgressIndicator  # Conditional
  - if _isLoading: Spinner: {}                # Conditional with alternative
    else: Text: "Done"

Named Child Slots

Any single-widget slot is a key of its own; list-valued slots keep their own name so the reverse converter emits the argument the widget actually declares.

Scaffold:
  appBar:
    AppBar:
      title:
        Text: "Home"
      actions:                                # not `children:` — AppBar has none
        - Icon: search
  body:
    Center: { }
BottomNavigationBar:
  items: []                                   # empty lists are preserved
ListView.builder:
  itemCount: items.length
  itemBuilder:                                # rebuilt as (context, index) => …
    ProductCard: { }

Switch Arms

A switch in a build method becomes a switch(...) node with one arm per case:

build:
  switch(status):
    - case Status.loading: CircularProgressIndicator: {}
    - case Status.error: Text: "failed"
    - default: HomeView: {}

Raw Expression Escape Hatch

Anything the compact format has no shape for is preserved verbatim rather than dropped, and round-trips unchanged:

build:
  expr: _buildBody(context)

Multiple Widget Classes Per File

A .dart file declaring several widget classes produces one YAML document per class, separated by ---. Reversing such a file writes all classes back into a single .dart with the imports emitted once.

widget: Header
type: StatelessWidget
build:
  Text: title
---
widget: Footer
type: StatelessWidget
build:
  Text: "footer"

Generic Widgets and Top-Level Declarations

Generic widget classes keep their type parameters, and non-widget declarations the widget refers to (enums, typedefs) travel with it at --level full:

widget: Boxed<T extends Comparable<T>>
type: StatelessWidget
declarations:
  - enum Status {loading, error, done}

Installation

dart pub global activate flutter_2yaml

Or add to your project's dev_dependencies:

dev_dependencies:
  flutter_2yaml: ^0.6.0

Requires Dart 3.11 or newer (the analyzer package this tool is built on requires it).

Usage

CLI — Forward (Dart → YAML)

# Convert a single file
flutter_2yaml lib/screens/splash_screen.dart

# Convert all .dart files in a directory
flutter_2yaml lib/screens/ --recursive

# Choose verbosity level
flutter_2yaml lib/screens/ --level minimal    # Widget tree only
flutter_2yaml lib/screens/ --level standard   # + state & lifecycle (default)
flutter_2yaml lib/screens/ --level full       # + imports, constructor, methods

# Custom output directory
flutter_2yaml lib/screens/ --output yaml_output/

# Watch mode — auto-regenerate on file changes
flutter_2yaml lib/screens/ --watch

CLI — Reverse (YAML → Dart)

# Convert a single YAML file back to Dart
flutter_2yaml reverse splash_screen.yaml

# Convert a directory of YAML files
flutter_2yaml reverse yaml_output/ --recursive

# Custom output directory
flutter_2yaml reverse yaml_output/ --output lib/screens/

Programmatic API

import 'package:flutter_2yaml/flutter_2yaml.dart';

// Forward: Dart → YAML
final analyzer = DartFileAnalyzer();
final model = analyzer.analyze(dartSource, 'my_widget.dart');
final yamlGenerator = YamlGenerator();
final yaml = yamlGenerator.generate(model!, VerbosityLevel.standard);

// Reverse: YAML → Dart
final yamlParser = YamlParser();
final reverseModel = yamlParser.parse(yamlSource, 'my_widget.yaml');
final dartGenerator = DartGenerator();
final dartCode = dartGenerator.generate(reverseModel);

// Files declaring several widget classes: one model per class
final models = analyzer.analyzeAll(dartSource, 'my_widgets.dart');
final multiYaml = yamlGenerator.generateAll(models, VerbosityLevel.standard);

final reverseModels = yamlParser.parseAll(multiYaml, 'my_widgets.yaml');
final multiDart = dartGenerator.generateAll(reverseModels);

// File-level conversion (handles multi-class files automatically)
final converter = Converter();
converter.convertFile('lib/screens/home.dart');

final reverseConverter = ReverseConverter();
reverseConverter.reverseFile('home.yaml');

Verbosity Levels

Level Includes Use Case
minimal Widget tree + shorthands Quick UI structure overview
standard + state, lifecycle Full widget understanding (default)
full + imports, constructor, all methods, enums/typedefs Complete file representation — use this for a round trip

Supported Widget Types

  • StatelessWidget, StatefulWidget
  • ConsumerWidget, ConsumerStatefulWidget (Riverpod)
  • GetView, GetWidget (GetX)
  • Non-widget classes are automatically skipped

Supported Widget Features

  • Scaffold with all named slots (appBar, drawer, FAB, bottomNav, etc.)
  • Container with BoxDecoration (bg, br, shadow, gradient, border, shape)
  • Stack + Positioned with coordinate shorthands
  • ListView.builder / GridView.builder (itemBuilder extraction)
  • Wrap with spacing shorthands
  • Text with full TextStyle pipe syntax
  • Conditional children (if / if-else)
  • Spread and for-in elements in children lists
  • All callback types with arrow notation
  • switch expressions and statements in build()
  • Multiple widget classes per file (one --- document each)
  • Generic widget classes, with type parameters preserved
  • Colors as Colors.x, #RRGGBB, or #AARRGGBB (alpha preserved)
  • theme.* and screen.* context lookups

Known Limitations

The compact format is a summary, so some detail is deliberately not carried:

  • Method bodies are summarised, not preserved. initState becomes a list of action descriptions; on the way back, actions that dropped information (an assignment, a call whose arguments were elided, control flow) are emitted as comments rather than as code that would not compile.
  • Callback bodies with more than one statement are reduced to an empty lambda of the right arity (validator → (value) {}).
  • Only enums and typedefs are carried among non-widget top-level declarations, and only at --level full; other top-level code is skipped.
  • Field mutability is not recorded — a final state field comes back non-final.
  • minimal and standard omit imports and constructors by design, so reversing at those levels produces a widget without its constructor parameters. Use --level full for a round trip that preserves the class surface.

Contributing

Contributions are welcome! Please see CONTRIBUTING.md for guidelines.

License

MIT License — see LICENSE for details.

Libraries

flutter_2yaml
Convert Flutter .dart widget files into compact YAML representations optimized for LLM/AI token consumption.