flutter_android_widgets

Define Android home screen widgets entirely in Dart.
No XML. No Kotlin. No manual manifest editing. Just Dart.

Quick StartLive UpdatesHow It WorksAPI ReferenceExampleFull Documentation


The Problem

Adding a home screen widget to a Flutter app today requires you to:

  1. Write an XML layout file (res/layout/widget_*.xml)
  2. Write an XML metadata file (res/xml/appwidget_provider_*.xml)
  3. Write a Kotlin AppWidgetProvider class with SharedPreferences boilerplate
  4. Manually register a <receiver> in AndroidManifest.xml
  5. Wire up a SharedPreferences bridge so Flutter and native code share data

That's 4+ files in 3 different languages just to show a number on the home screen. Every time you change the layout, you have to update the XML, the Kotlin, and potentially the manifest — in lockstep.

The Solution

final myWidget = AndroidWidget(
  info: WidgetInfo(
    widgetClassName: 'MyWidgetProvider',
    widgetName: 'My Widget',
    minWidth: 250,
    minHeight: 100,
    updateInterval: Duration(hours: 1),
  ),
  layout: WColumn(
    backgroundColor: '#1A1A2E',
    padding: 16,
    children: [
      WText('\${userName}', textSize: 18, bold: true, textColor: '#FFFFFF'),
      WText('\${lastUpdated}', textSize: 12, textColor: '#AAAAAA'),
      WButton(label: 'Refresh', actionKey: 'refresh'),
    ],
  ),
  dataKeys: ['userName', 'lastUpdated'],
);

Run dart run build_runner build and you're done. The package generates all four native files and patches your manifest automatically.


Quick Start

1. Install

# pubspec.yaml
dependencies:
  flutter_android_widgets: ^0.0.1

dev_dependencies:
  build_runner: ^2.4.0
flutter pub get

2. Add manifest markers

Open android/app/src/main/AndroidManifest.xml and add two marker comments inside <application>:

<application ...>
    <activity ...>
        <!-- your existing activity -->
    </activity>

    <!-- flutter_android_widgets:start -->
    <!-- flutter_android_widgets:end -->
</application>

3. Define your widget

Create a file (e.g. lib/my_widgets.dart):

import 'package:flutter_android_widgets/flutter_android_widgets.dart';

final myWidget = AndroidWidget(
  info: WidgetInfo(
    widgetClassName: 'StatsWidgetProvider',
    widgetName: 'App Stats',
    minWidth: 250,
    minHeight: 100,
    updateInterval: Duration(hours: 1),
    resizeMode: WidgetResizeMode.horizontal,
  ),
  layout: WColumn(
    backgroundColor: '#1A1A2E',
    padding: 16,
    children: [
      WText('\${title}', textSize: 18, bold: true, textColor: '#FFFFFF'),
      WText('\${subtitle}', textSize: 12, textColor: '#AAAAAA'),
      WButton(label: 'Refresh', actionKey: 'refresh'),
    ],
  ),
  dataKeys: ['title', 'subtitle'],
);

4. Generate

dart run build_runner build

This creates:

Generated file Purpose
res/layout/widget_stats_widget_provider.xml RemoteViews-compatible XML layout
res/xml/appwidget_provider_stats_widget_provider.xml Widget size, update interval, resize mode
kotlin/.../StatsWidgetProvider.kt Full AppWidgetProvider with data bindings
AndroidManifest.xml <receiver> entry injected between markers

5. Push data from Flutter

import 'package:flutter_android_widgets/flutter_android_widgets.dart';

await HomeWidgetData.saveAll({
  'title': 'Active Users',
  'subtitle': '1,247 today',
});

The widget on the home screen updates the next time Android refreshes it, or immediately when the user taps the Refresh button.

But there's a better way — see Live Updates to make data changes appear on the widget instantly.


Live Updates

The package can automatically refresh the home screen widget whenever your Flutter app saves data, resumes from the background, or performs a hot restart — so you see changes on the widget in real time during development.

Setup

Pass your widget definitions to WidgetUpdater.initialize() so that layout styling is synced to SharedPreferences on every hot restart:

void main() {
  WidgetsFlutterBinding.ensureInitialized();
  WidgetUpdater.initialize(widgets: [myWidget]); // ← pass your widget list
  HomeWidgetData.autoUpdate = true;              // ← auto-refresh after every save
  runApp(const MyApp());
}

How it works

Trigger What happens
HomeWidgetData.save() / saveAll() With autoUpdate = true, automatically calls WidgetUpdater.updateAll() after saving
Hot restart (Ctrl+Shift+F5) main() re-runs → styles synced to SharedPreferences → updateAll() → widget refreshes
App resumes from background Styles re-synced → updateAll() → widget shows latest styling and data

What updates on hot restart

Hot restart re-runs main(), which re-syncs all style properties to SharedPreferences before triggering a widget refresh. Hot reload does not work for layout changes — it does not re-evaluate top-level final variables.

✅ Updates immediately on hot restart

These properties are read from SharedPreferences at widget update time via the RemoteViews API:

Property Applies to
backgroundColor All nodes (containers, text, buttons, images, progress bars)
padding All nodes
gravity WColumn, WRow, WText
textColor WText, WButton
textSize WText, WButton
maxLines WText
label (button text) WButton
progress WProgressBar
Data values (via HomeWidgetData.save) WText with ${key} bindings

❌ Requires rebuild + re-add widget (cannot update at runtime)

These properties are compiled into the APK and cannot be changed by any runtime API:

Property Why it can't update at runtime
minWidth / minHeight Stored in appwidget_provider_*.xml — a compiled APK resource. The Android launcher reads this once when the widget is first placed to determine its grid cell size. No Android API exists to change widget dimensions after placement.
resizeMode Same as above — part of the provider XML metadata read by the launcher.
updatePeriodMillis Registered as a system alarm from the provider XML. Cannot be changed without reinstalling.
widgetName The label shown in the widget picker comes from AndroidManifest.xml.
bold (text style) RemoteViews has no API to change Typeface/text style at runtime.

To apply any of the above changes:

  1. Modify my_widgets.dart
  2. Run dart run build_runner build
  3. Run flutter run (reinstalls the APK with new compiled values)
  4. For minWidth/minHeight/resizeMode: remove the widget from the home screen and re-add it — the launcher caches the old size at placement time

Manual updates

You can also trigger updates manually:

// Update all widgets
await WidgetUpdater.updateAll();

// Update a specific widget
await WidgetUpdater.updateWidget('MyWidgetProvider');

What gets generated

When you run build_runner, the package also generates:

File Purpose
FlutterAndroidWidgetsChannel.kt MethodChannel handler that sends APPWIDGET_UPDATE broadcasts
MainActivity.kt (patched) Registers the channel in configureFlutterEngine

How It Works

┌──────────────────────────────────────────────────────────────┐
│                        YOUR DART CODE                        │
│                                                              │
│   AndroidWidget(                                             │
│     info: WidgetInfo(...),                                   │
│     layout: WColumn([ WText(...), WButton(...) ]),           │
│     dataKeys: ['key1', 'key2'],                              │
│   )                                                          │
└─────────────────────────┬────────────────────────────────────┘
                          │
                    build_runner
                          │
            ┌─────────────┼──────────────┐
            ▼             ▼              ▼
    ┌──────────────┐ ┌──────────┐ ┌──────────────┐
    │ XmlGenerator │ │ Kotlin   │ │ Manifest     │
    │              │ │ Generator│ │ Patcher      │
    │ • layout.xml │ │          │ │              │
    │ • provider   │ │ • .kt    │ │ • <receiver> │
    │   .xml       │ │   class  │ │   injection  │
    └──────────────┘ └──────────┘ └──────────────┘
            │             │              │
            ▼             ▼              ▼
    ┌──────────────────────────────────────────────┐
    │              ANDROID PROJECT                  │
    │  res/layout/   res/xml/   kotlin/   Manifest │
    └──────────────────────────────────────────────┘

The pipeline (what happens when you run build_runner):

  1. WidgetAnalyzer scans your Dart source files using regex-based pattern matching. It finds every final/const variable of type AndroidWidget and extracts the metadata (WidgetInfo) and the full layout tree.

  2. NodeCollector walks the layout tree and assigns unique Android view IDs, extracts ${key} data bindings from text nodes, collects action keys from buttons, and catalogs drawable references from images.

  3. XmlGenerator takes the node tree and produces:

    • A res/layout/widget_<name>.xml file with proper Android namespaces, only using RemoteViews-compatible elements
    • A res/xml/appwidget_provider_<name>.xml metadata file
  4. KotlinGenerator produces a complete AppWidgetProvider Kotlin class that:

    • Reads from FlutterSharedPreferences using the exact same key prefix Flutter uses
    • Binds ${key} placeholders to TextView content via RemoteViews.setTextViewText()
    • Wires up PendingIntent broadcast receivers for button clicks
    • Handles onReceive() to re-read SharedPreferences and update the widget
  5. ManifestPatcher injects <receiver> XML blocks between the <!-- flutter_android_widgets:start --> / <!-- flutter_android_widgets:end --> marker comments — including all necessary <intent-filter> and <meta-data> entries.

The data bridge at runtime:

Flutter App                              Android Widget
    │                                         ▲
    │  HomeWidgetData.save('key', 'value')    │
    │          │                              │
    ▼          ▼                              │
  SharedPreferences ──────────────────────────┘
  (FlutterSharedPreferences XML file)
  Key: "flutter.flutter_android_widgets_key"

Flutter writes via shared_preferences. The generated Kotlin reads from the same FlutterSharedPreferences file using the key prefix flutter.flutter_android_widgets_. No platform channels, no method calls, no serialization — just a shared file.


API Reference

Core Classes

AndroidWidget

The top-level definition. One variable = one home screen widget.

const AndroidWidget({
  required WidgetInfo info,       // metadata (name, size, interval)
  required WidgetNode layout,     // the visual tree
  List<String> dataKeys = const [],  // keys for data binding
})

WidgetInfo

Metadata that maps to appwidget_provider.xml attributes.

const WidgetInfo({
  required String widgetClassName,    // Kotlin class name (PascalCase)
  required String widgetName,         // Label in the widget picker
  required int minWidth,              // Minimum width in dp
  required int minHeight,             // Minimum height in dp
  required Duration updateInterval,   // Auto-refresh interval (min 30 min)
  WidgetResizeMode resizeMode = WidgetResizeMode.both,
  String? previewImage,               // Manual drawable name for widget picker preview
  String? previewImagePath,           // Path to a local image file — auto-copied to res/drawable/
})

See Widget Picker Preview for details on both fields.

HomeWidgetData

Runtime API for pushing data from Flutter to the widget.

Method Description
HomeWidgetData.save(key, value) Save a single String value
HomeWidgetData.read(key) Read the current value (returns String?)
HomeWidgetData.remove(key) Delete a key
HomeWidgetData.saveAll(map) Save multiple key-value pairs at once
HomeWidgetData.autoUpdate Set to true to auto-refresh widgets after every save

WidgetUpdater

Triggers widget refreshes from Flutter via a MethodChannel.

Method Description
WidgetUpdater.initialize({widgets}) Enable auto-refresh on hot restart & app resume; pass widget list to sync styles
WidgetUpdater.updateAll() Send update broadcast to all widget providers
WidgetUpdater.updateWidget(name) Update a specific widget by class name

Layout Primitives

Containers

Class Android View Description
WColumn LinearLayout (vertical) Stack children vertically
WRow LinearLayout (horizontal) Arrange children horizontally
WStack FrameLayout Layer children on top of each other

All containers accept: children, padding, backgroundColor, id, gravity (WColumn/WRow only).

Leaf Views

Class Android View Key Properties
WText TextView text, textSize, textColor, bold, maxLines, gravity
WButton Button label, actionKey, textSize, textColor
WImage ImageView drawableName, width, height, contentDescription
WProgressBar ProgressBar horizontal, progress, width, height

All leaf views accept: id, padding, backgroundColor.

Data Binding

Use ${key} in any WText to bind it to a SharedPreferences value:

WText('\${userName}')                           // Simple binding
WText('Hello, \${name}! You have \${count}.')   // Template binding

The Kotlin generator automatically reads the right SharedPreferences key and replaces the placeholder at widget update time.

Resize Modes

WidgetResizeMode.none        // Fixed size
WidgetResizeMode.horizontal  // Resizable horizontally only
WidgetResizeMode.vertical    // Resizable vertically only
WidgetResizeMode.both        // Resizable in both directions (default)

Widget Picker Preview

Android shows a preview image in the widget picker when the user long-presses the home screen. There are two ways to provide one:

Point to any image in your project. build_runner copies it to res/drawable/ automatically:

final myWidget = AndroidWidget(
  info: WidgetInfo(
    widgetClassName: 'MyWidgetProvider',
    widgetName: 'My Widget',
    minWidth: 250,
    minHeight: 100,
    updateInterval: Duration(hours: 1),
    previewImagePath: 'assets/previews/my_widget.png', // ← path relative to project root
  ),
  layout: WColumn(...),
  dataKeys: [...],
);

The file is copied to android/app/src/main/res/drawable/widget_preview_my_widget_provider.png and android:previewImage is set automatically.

Any Android-supported format works: PNG, JPG, WEBP.

Option B — previewImage (manual)

If you've already placed an image in res/drawable/ yourself, reference its name without extension:

previewImage: 'my_existing_drawable',  // references res/drawable/my_existing_drawable.png

If both are set, previewImagePath takes precedence — it overwrites previewImage with the auto-derived drawable name.


Example

The example/ directory contains a complete, runnable Flutter app that demonstrates the full workflow:

cd example
flutter pub get
dart run build_runner build
flutter run

After installing, long-press your home screen → Widgets → find "Widget Demo" → add it. Then use the app to update the widget's data in real time.


Multiple Widgets

Define as many widgets as you want — each top-level AndroidWidget variable becomes its own widget:

final weatherWidget = AndroidWidget(
  info: WidgetInfo(
    widgetClassName: 'WeatherWidgetProvider',
    widgetName: 'Weather',
    minWidth: 250, minHeight: 100,
    updateInterval: Duration(hours: 1),
  ),
  layout: WColumn(children: [
    WText('\${temperature}', textSize: 32, bold: true, textColor: '#FFFFFF'),
    WText('\${condition}', textSize: 14, textColor: '#CCCCCC'),
  ]),
  dataKeys: ['temperature', 'condition'],
);

final todoWidget = AndroidWidget(
  info: WidgetInfo(
    widgetClassName: 'TodoWidgetProvider',
    widgetName: 'Todo List',
    minWidth: 250, minHeight: 200,
    updateInterval: Duration(minutes: 30),
  ),
  layout: WColumn(children: [
    WText('Tasks', textSize: 18, bold: true),
    WText('\${task1}', textSize: 14),
    WText('\${task2}', textSize: 14),
    WText('\${task3}', textSize: 14),
  ]),
  dataKeys: ['task1', 'task2', 'task3'],
);

Each generates its own XML, Kotlin, and manifest entry.


Test Coverage

94 unit tests across 6 files:

Test file Tests
xml_generator_test.dart 13
kotlin_generator_test.dart 8
manifest_patcher_test.dart 9
widget_analyzer_test.dart 16
data_model_test.dart 37
channel_generator_test.dart 12

Run with flutter test from the package root.


Constraints

Android home screen widgets are rendered using RemoteViews, which has strict limitations:

  • Limited views — Only the views listed in the primitives table are supported. No RecyclerView, no custom views, no Canvas drawing.
  • No interactivity beyond clicks — Buttons can trigger broadcasts, but there's no text input, no scroll (on older APIs), and no animations.
  • 30-minute minimum update interval — Android enforces this. The package clamps any shorter interval to 30 minutes. For more frequent updates, trigger updates from your app via AppWidgetManager.
  • SharedPreferences data types — Data binding values must be strings. Convert numbers, dates, etc. to strings before saving.
  • Build runner required — You must run dart run build_runner build after changing your widget definition. The generated files are checked into source control.

Project Structure

your_app/
├── lib/
│   └── my_widgets.dart         ← You write this
├── android/app/src/main/
│   ├── AndroidManifest.xml     ← Add markers once, auto-patched after
│   ├── res/
│   │   ├── layout/
│   │   │   └── widget_*.xml    ← Generated
│   │   └── xml/
│   │       └── appwidget_provider_*.xml  ← Generated
│   └── kotlin/.../
│       ├── *Provider.kt        ← Generated
│       ├── FlutterAndroidWidgetsChannel.kt  ← Generated
│       └── MainActivity.kt     ← Patched (adds MethodChannel registration)
└── pubspec.yaml

License

BSD 3-Clause. See LICENSE for details.

Libraries

builder
flutter_android_widgets
Flutter Android Widgets — Define Android home screen widgets entirely in Dart.