How It Works
The generation pipeline
Your Dart file (AndroidWidget declaration)
│
build_runner (AndroidWidgetBuilder.build())
│
┌────┼────────────────────────────┐
│ │ │
▼ ▼ ▼
XmlGenerator KotlinGenerator ManifestPatcher
│ │ │
▼ ▼ ▼
res/layout/ kotlin/.../ AndroidManifest.xml
*.xml *.kt (patched)
+ ChannelGenerator
│
▼
FlutterAndroidWidgetsChannel.kt
MainActivity.kt (patched)Step-by-step
1. Discovery
AndroidWidgetBuilder receives each .dart file in lib/. It first calls WidgetAnalyzer.hasWidgets(), which checks for:
(?:final|const)\s+\w+\s*=\s*AndroidWidget\s*\(Files without this pattern are skipped immediately.
2. Analysis
For matching files, WidgetAnalyzer.analyze() extracts metadata via regex:
widgetClassName,widgetName,minWidth,minHeightupdateInterval(parsesDuration(hours: n)/Duration(minutes: n))resizeMode(maps enum values)dataKeyslist
3. Layout parsing
WidgetAnalyzer.parseLayout() uses a bracket-depth tokenizer to parse the nested layout: argument into an actual WidgetNode tree. It handles:
- All 7 node types
- All properties
- Arbitrarily deep nesting
- Template strings with
\${key}placeholders
4. Node collection
NodeCollector.collect() walks the parsed tree and produces:
- A flat node list with auto-generated Android view IDs
- The set of all
\${key}references (data bindings) - The set of all button
actionKeyvalues - The set of all drawable references from
WImage
5. XML generation
Two XML files per widget:
- Layout XML —
res/layout/widget_<name>.xmlusing only RemoteViews-compatible elements (LinearLayout,FrameLayout,TextView,Button,ImageView,ProgressBar) - Provider info XML —
res/xml/appwidget_provider_<name>.xmlwith size, update interval, resize mode
6. Kotlin generation
A complete AppWidgetProvider class:
- Package declaration + all necessary imports
companion objectwithPREFS_NAME,KEY_PREFIX, and action constantsonUpdate()→ iteratesappWidgetIds→ callsupdateAppWidget()onReceive()(only when buttons exist) → handles action broadcastsupdateAppWidget():- Opens
FlutterSharedPreferences - Reads each data binding key with the
flutter.prefix - Calls
setTextViewText()for each binding - Reads style override keys and applies via
RemoteViewsAPI - Sets up
PendingIntentfor each button click
- Opens
7. Manifest patching
ManifestPatcher replaces everything between the marker comments with <receiver> blocks — including <intent-filter> for APPWIDGET_UPDATE + all action broadcasts, and a <meta-data> pointing to the provider XML. Re-running is fully idempotent.
The data bridge
Flutter app Android widget
│ ▲
│ HomeWidgetData.save('key', 'val') │
▼ │
SharedPreferences ──────────────────────────┘
FlutterSharedPreferences.xml
Key: "flutter.flutter_android_widgets_key"Flutter writes via shared_preferences. The generated Kotlin reads from the same FlutterSharedPreferences XML file using the same key construction. No platform channel. No serialization overhead. Works even when the Flutter engine is not running.
Key format
flutter.flutter_android_widgets_<yourKey>
│ │ └── your data key
│ └── package prefix
└── Flutter shared_preferences prefix (always "flutter.")Design decisions
Regex analysis vs. package:analyzer
The Dart analyzer package adds significant build time and dependency weight. Since widget definitions follow a predictable declarative pattern, regex extracts 99% of real-world definitions in milliseconds.
Limitation: Computed values (e.g. minWidth: calculateWidth()) cannot be evaluated. Use literal values in widget definitions.
Marker comments vs. Gradle plugin
Marker comments are transparent — you can see exactly what was generated in your manifest. A Gradle plugin would be more magical but harder to debug.
SharedPreferences vs. platform channels
Platform channels require the Flutter engine to be running. Home screen widgets update on a system timer regardless of whether the app is open. SharedPreferences is always available and is already used by the shared_preferences plugin.
build_to: source vs. cache
Generated files (*.xml, *.kt) must be inside the Android project for Gradle to compile them. The build runner cache is not visible to Gradle. Generated files are deterministic — safe to commit to version control.
Package structure
flutter_android_widgets/
├── lib/
│ ├── flutter_android_widgets.dart # Public API barrel
│ ├── builder.dart # build_runner registration
│ └── src/
│ ├── widget_info.dart # WidgetInfo, WidgetResizeMode
│ ├── widget_primitives.dart # WColumn, WRow, WText, etc.
│ ├── android_widget.dart # AndroidWidget
│ ├── home_widget_data.dart # Runtime SharedPreferences helper
│ ├── widget_updater.dart # Runtime MethodChannel
│ ├── generator/
│ │ ├── node_collector.dart # Tree walker + ID assignment
│ │ ├── xml_generator.dart # Dart tree → Android XML
│ │ ├── kotlin_generator.dart # Dart tree → Kotlin class
│ │ ├── manifest_patcher.dart # Manifest marker injection
│ │ └── channel_generator.dart # MethodChannel + MainActivity
│ └── builder/
│ ├── widget_analyzer.dart # Source scanner + layout parser
│ └── android_widget_builder.dart # build_runner orchestrator
├── build.yaml # Builder registration
└── test/ # 89 unit testsCompile-time vs. runtime split:
- Compile-time (used by
build_runner):android_widget.dart,widget_info.dart,widget_primitives.dart, all generators — pure Dart, no Flutter imports - Runtime (used in your app):
home_widget_data.dart(needsshared_preferences),widget_updater.dart(needs Flutter services)
This split is critical — build_runner runs in a headless Dart VM and cannot import Flutter framework code.