flutter_android_widgets

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, minHeight
  • updateInterval (parses Duration(hours: n) / Duration(minutes: n))
  • resizeMode (maps enum values)
  • dataKeys list

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 actionKey values
  • The set of all drawable references from WImage

5. XML generation

Two XML files per widget:

  • Layout XMLres/layout/widget_<name>.xml using only RemoteViews-compatible elements (LinearLayout, FrameLayout, TextView, Button, ImageView, ProgressBar)
  • Provider info XMLres/xml/appwidget_provider_<name>.xml with size, update interval, resize mode

6. Kotlin generation

A complete AppWidgetProvider class:

  • Package declaration + all necessary imports
  • companion object with PREFS_NAME, KEY_PREFIX, and action constants
  • onUpdate() → iterates appWidgetIds → calls updateAppWidget()
  • onReceive() (only when buttons exist) → handles action broadcasts
  • updateAppWidget():
    • Opens FlutterSharedPreferences
    • Reads each data binding key with the flutter. prefix
    • Calls setTextViewText() for each binding
    • Reads style override keys and applies via RemoteViews API
    • Sets up PendingIntent for each button click

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 tests

Compile-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 (needs shared_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.

On this page

No Headings