flutter_android_widgets
Define Android home screen widgets entirely in Dart.
No XML. No Kotlin. No manual manifest editing. Just Dart.
Quick Start • Live Updates • How It Works • API Reference • Example • Full Documentation
The Problem
Adding a home screen widget to a Flutter app today requires you to:
- Write an XML layout file (
res/layout/widget_*.xml) - Write an XML metadata file (
res/xml/appwidget_provider_*.xml) - Write a Kotlin
AppWidgetProviderclass with SharedPreferences boilerplate - Manually register a
<receiver>in AndroidManifest.xml - 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-levelfinalvariables.
✅ 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:
- Modify
my_widgets.dart - Run
dart run build_runner build - Run
flutter run(reinstalls the APK with new compiled values) - 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):
-
WidgetAnalyzer scans your Dart source files using regex-based pattern matching. It finds every
final/constvariable of typeAndroidWidgetand extracts the metadata (WidgetInfo) and the full layout tree. -
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. -
XmlGenerator takes the node tree and produces:
- A
res/layout/widget_<name>.xmlfile with proper Android namespaces, only using RemoteViews-compatible elements - A
res/xml/appwidget_provider_<name>.xmlmetadata file
- A
-
KotlinGenerator produces a complete
AppWidgetProviderKotlin class that:- Reads from
FlutterSharedPreferencesusing the exact same key prefix Flutter uses - Binds
${key}placeholders toTextViewcontent viaRemoteViews.setTextViewText() - Wires up
PendingIntentbroadcast receivers for button clicks - Handles
onReceive()to re-read SharedPreferences and update the widget
- Reads from
-
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:
Option A — previewImagePath (recommended)
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,
previewImagePathtakes precedence — it overwritespreviewImagewith the auto-derived drawable name.
Example
The example/ directory contains a complete, runnable Flutter app that demonstrates the full workflow:
example/lib/my_widgets.dart— widget definition in Dartexample/lib/main.dart— Flutter UI that pushes data to the widget
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, noCanvasdrawing. - 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 buildafter 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.