glance_widget
Create instant-updating home screen widgets for Android and iOS. Built with Jetpack Glance (Android) and WidgetKit (iOS).
Why glance_widget?
Unlike other packages (e.g., home_widget) that only provide a data bridge and require you to write all widget UI in native Swift/Kotlin, glance_widget is a complete solution — zero native code required.
| glance_widget | home_widget | |
|---|---|---|
| Widget UI | 7 ready-to-use templates | Write native code yourself |
| Type Safety | sealed class + generic controllers — compile-time errors |
String keys — runtime errors |
| Real-time Updates | DebouncedWidgetController — 100ms coalescing, auto-flush on app background |
Not available — build it yourself |
| Background Updates | Built-in GlanceBackground (WorkManager + Timeline) |
Requires external flutter_workmanager |
| iOS Push | Built-in iOS 26+ APNs support | Not available |
| Platform Safety | GlanceConfig.strictMode — graceful on Web/desktop |
Crashes on unsupported platforms |
| Native Code | Zero | Required for every widget |
Features
- Instant Updates - Widgets update in < 1 second on both platforms
- Cross-Platform - Same API for Android and iOS
- 7 Widget Templates - Simple, Progress, List, Image, Chart, Calendar, and Gauge
- Theme Support - Light/Dark themes with full customization
- Deep Links - All widgets support custom deep link URIs
- Interactive Actions - Tap, checkbox toggle, item tap handling
- Background Updates - Android widgets update even when app is closed (WorkManager)
- Timeline Refresh - iOS widgets refresh periodically via WidgetKit timeline policy
- iOS 26+ Push Updates - Server-triggered widget updates via APNs
- Lock Screen Widgets - Android widgets on home screen and lock screen
- Real-time Data - Debounced controller for high-frequency updates (crypto, stocks)
- Widget Configuration - Handle widget setup flow when users add widgets
Platform Comparison
| Feature | Android (Jetpack Glance) | iOS (WidgetKit) |
|---|---|---|
| Update Speed | < 1 second | < 1 second (app foreground) |
| Background Updates | WorkManager (15 min+) | Timeline-based (.after policy) |
| Server Push | N/A | iOS 26+ (APNs) |
| Lock Screen | Supported (keyguard) | N/A |
| Interactive Actions | ActionCallback | URL-based actions |
| Min Version | Android 8.0 (API 26) | iOS 16.0 |
Widget Templates
| Template | Description | Use Cases |
|---|---|---|
| SimpleWidget | Title + Value + Subtitle | Crypto prices, weather, stats |
| ProgressWidget | Circular/Linear progress | Downloads, goals, battery |
| ListWidget | Scrollable item list with checkboxes | To-do, shopping, activities |
| ImageWidget | Photo with title and subtitle | Photo of the day, album art |
| ChartWidget | Line, bar, or sparkline chart | Revenue trends, analytics |
| CalendarWidget | Date header with event list | Daily schedule, meetings |
| GaugeWidget | Radial or dashboard metrics | CPU usage, performance scores |
Installation
Add to your pubspec.yaml:
dependencies:
glance_widget: ^1.0.0
Requirements
| Platform | Minimum Version |
|---|---|
| Flutter | 3.27+ |
| Dart | 3.6+ |
| Android | API 26 (Android 8.0) |
| iOS | 16.0 |
Android Setup
1. Configure Manifest
Add widget receivers to android/app/src/main/AndroidManifest.xml:
<application>
<!-- Simple Widget -->
<receiver
android:name="com.example.glance_widget_android.templates.SimpleWidgetReceiver"
android:exported="true">
<intent-filter>
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
</intent-filter>
<meta-data
android:name="android.appwidget.provider"
android:resource="@xml/simple_widget_info" />
</receiver>
<!-- Progress Widget -->
<receiver
android:name="com.example.glance_widget_android.templates.ProgressWidgetReceiver"
android:exported="true">
<intent-filter>
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
</intent-filter>
<meta-data
android:name="android.appwidget.provider"
android:resource="@xml/progress_widget_info" />
</receiver>
<!-- List Widget -->
<receiver
android:name="com.example.glance_widget_android.templates.ListWidgetReceiver"
android:exported="true">
<intent-filter>
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
</intent-filter>
<meta-data
android:name="android.appwidget.provider"
android:resource="@xml/list_widget_info" />
</receiver>
<!-- Image Widget -->
<receiver
android:name="com.example.glance_widget_android.templates.ImageWidgetReceiver"
android:exported="true">
<intent-filter>
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
</intent-filter>
<meta-data
android:name="android.appwidget.provider"
android:resource="@xml/image_widget_info" />
</receiver>
<!-- Chart Widget -->
<receiver
android:name="com.example.glance_widget_android.templates.ChartWidgetReceiver"
android:exported="true">
<intent-filter>
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
</intent-filter>
<meta-data
android:name="android.appwidget.provider"
android:resource="@xml/chart_widget_info" />
</receiver>
<!-- Calendar Widget -->
<receiver
android:name="com.example.glance_widget_android.templates.CalendarWidgetReceiver"
android:exported="true">
<intent-filter>
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
</intent-filter>
<meta-data
android:name="android.appwidget.provider"
android:resource="@xml/calendar_widget_info" />
</receiver>
<!-- Gauge Widget -->
<receiver
android:name="com.example.glance_widget_android.templates.GaugeWidgetReceiver"
android:exported="true">
<intent-filter>
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
</intent-filter>
<meta-data
android:name="android.appwidget.provider"
android:resource="@xml/gauge_widget_info" />
</receiver>
</application>
2. Create Widget Info XML
Create android/app/src/main/res/xml/simple_widget_info.xml (repeat for each template):
<?xml version="1.0" encoding="utf-8"?>
<appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android"
android:minWidth="180dp"
android:minHeight="110dp"
android:targetCellWidth="3"
android:targetCellHeight="2"
android:resizeMode="horizontal|vertical"
android:widgetCategory="home_screen|keyguard"
android:updatePeriodMillis="0" />
3. Set SDK Versions
In android/app/build.gradle.kts:
android {
compileSdk = 36
defaultConfig {
minSdk = 26
}
}
iOS Setup
1. Create Widget Extension
In Xcode:
- Open
ios/Runner.xcworkspace - File → New → Target → Widget Extension
- Name:
GlanceWidgets - Click Finish
2. Configure App Groups
Both targets need the same App Group:
- Select
Runnertarget → Signing & Capabilities → + App Groups - Add:
group.com.yourcompany.yourapp - Select
GlanceWidgetstarget → repeat with same App Group ID
3. Add Widget Files
Copy files from glance_widget_ios/example/ios/GlanceWidgets/ to your extension:
GlanceWidgets.swiftSharedModels.swift(updateappGroupId!)SimpleWidget.swiftProgressWidget.swiftListWidget.swiftImageWidget.swiftChartWidget.swiftCalendarWidget.swiftGaugeWidget.swift
4. Configure URL Scheme
Add to ios/Runner/Info.plist:
<key>CFBundleURLTypes</key>
<array>
<dict>
<key>CFBundleURLSchemes</key>
<array>
<string>glancewidget</string>
</array>
</dict>
</array>
See iOS Widget Setup Guide for detailed instructions.
Usage
Simple Widget
import 'package:glance_widget/glance_widget.dart';
await GlanceWidget.simple(
id: 'crypto_btc',
title: 'Bitcoin',
value: '\$94,532.00',
subtitle: '+2.34%',
subtitleColor: Colors.green,
deepLinkUri: 'myapp://crypto/btc',
);
Type-Safe Controllers (v1.0)
For advanced use cases, use generic type-safe controllers:
import 'package:glance_widget/glance_widget.dart';
// Convenience controller — compile-time type safety
final controller = SimpleWidgetController(widgetId: 'crypto_btc');
await controller.update(SimpleWidgetData(
title: 'Bitcoin',
value: '\$94,532.00',
subtitle: '+2.34%',
subtitleColor: Colors.green,
));
// Listen for widget interactions
controller.onAction.listen((action) {
print('Widget tapped: ${action.type}');
});
// Don't forget to dispose
controller.dispose();
Available controllers:
SimpleWidgetControllerProgressWidgetControllerListWidgetControllerImageWidgetControllerChartWidgetControllerCalendarWidgetControllerGaugeWidgetController
Or use the generic form directly:
final ctrl = GlanceWidgetController<ChartWidgetData>(widgetId: 'chart1');
await ctrl.update(ChartWidgetData(
title: 'Revenue',
dataPoints: [12, 19, 15, 25, 22, 30, 28],
));
ctrl.dispose();
Progress Widget
await GlanceWidget.progress(
id: 'daily_goal',
title: 'Steps Today',
progress: 0.75,
subtitle: '7,500 / 10,000',
progressType: ProgressType.circular,
progressColor: Colors.green,
);
List Widget
await GlanceWidget.list(
id: 'todo_list',
title: 'Today\'s Tasks',
items: [
GlanceListItem(text: 'Buy groceries', checked: true),
GlanceListItem(text: 'Call mom', checked: false),
],
showCheckboxes: true,
);
Image Widget
await GlanceWidget.image(
id: 'photo',
title: 'Photo of the Day',
imageBase64: base64EncodedImage,
subtitle: 'Beautiful sunset',
fit: ImageFit.cover,
);
Chart Widget
await GlanceWidget.chart(
id: 'revenue',
title: 'Revenue',
dataPoints: [12, 19, 15, 25, 22, 30, 28],
chartType: ChartType.line,
color: Colors.blue,
subtitle: 'Last 7 days',
);
Calendar Widget
await GlanceWidget.calendar(
id: 'events',
title: 'Today\'s Events',
date: DateTime.now(),
events: [
CalendarEvent(time: '09:00', title: 'Standup', color: Colors.green),
CalendarEvent(time: '14:00', title: 'Review', color: Colors.blue),
],
);
Gauge Widget
await GlanceWidget.gauge(
id: 'monitor',
title: 'System Monitor',
metrics: [
GaugeMetric(label: 'CPU', value: 45, maxValue: 100, color: Colors.green, unit: '%'),
GaugeMetric(label: 'Memory', value: 72, maxValue: 100, color: Colors.orange, unit: '%'),
],
gaugeType: GaugeType.radial,
);
Theme Configuration
await GlanceWidget.setTheme(GlanceTheme.dark());
// Or custom theme
await GlanceWidget.setTheme(GlanceTheme(
backgroundColor: Color(0xFF1A1A2E),
textColor: Colors.white,
secondaryTextColor: Color(0xFFB0B0B0),
accentColor: Colors.orange,
borderRadius: 16.0,
isDark: true,
));
Handle Widget Actions
GlanceWidget.onAction.listen((action) {
switch (action.type) {
case GlanceActionType.tap:
print('Widget ${action.widgetId} tapped');
break;
case GlanceActionType.checkboxToggle:
print('Item ${action.itemIndex} toggled to ${action.value}');
break;
case GlanceActionType.itemTap:
print('Item ${action.itemIndex} tapped');
break;
case GlanceActionType.configure:
// Show configuration UI, then:
GlanceWidget.completeWidgetConfiguration(action.widgetId);
break;
default:
break;
}
});
Deep Links
All widget types support deepLinkUri parameter:
await GlanceWidget.simple(
id: 'btc',
title: 'Bitcoin',
value: '\$94,532',
deepLinkUri: 'myapp://crypto/btc', // Opens when widget is tapped
);
Background Updates (Android)
await GlanceBackground.configureUpdate(
widgetId: 'crypto_btc',
template: GlanceTemplate.simple,
apiUrl: 'https://api.coingecko.com/api/v3/simple/price?ids=bitcoin&vs_currencies=usd',
intervalMinutes: 15,
title: 'Bitcoin',
valuePath: r'$.bitcoin.usd',
valuePrefix: r'$',
);
// Cancel
await GlanceBackground.cancelUpdate('crypto_btc');
// Check status
final status = await GlanceBackground.getUpdateStatus('crypto_btc');
Timeline Refresh (iOS)
await GlanceBackground.configureTimelineRefresh(
widgetId: 'weather',
intervalMinutes: 30,
);
await GlanceBackground.cancelTimelineRefresh('weather');
DebouncedWidgetController (Real-time Data)
For high-frequency updates like crypto prices or live scores:
final controller = DebouncedWidgetController<SimpleWidgetData>(
widgetId: 'crypto_btc',
theme: GlanceTheme.dark(),
debounceInterval: Duration(milliseconds: 100),
maxWaitTime: Duration(milliseconds: 500),
stalenessThreshold: Duration(seconds: 15),
);
priceStream.listen((price) {
controller.scheduleUpdate(SimpleWidgetData(
title: 'Bitcoin',
value: '\$${price.toStringAsFixed(2)}',
));
});
// Flushes pending updates automatically when app goes to background
controller.dispose();
iOS 26+ Server Push Updates
if (await GlanceWidget.isWidgetPushSupported()) {
final token = await GlanceWidget.getWidgetPushToken();
if (token != null) {
await api.registerWidgetPushToken(token);
}
}
Platform Safety
glance_widget runs gracefully on unsupported platforms (Web, macOS, Windows, Linux):
// Default: methods return false/empty and log a warning
await GlanceWidget.simple(id: 'test', title: 'T', value: 'V'); // no-op on Web
// Opt-in to strict mode (recommended in debug):
GlanceConfig.strictMode = kDebugMode; // throws UnsupportedError on Web/desktop
Architecture
| Package | Description |
|---|---|
glance_widget |
Main package with cross-platform API |
glance_widget_platform_interface |
Platform-independent interface |
glance_widget_android |
Android implementation (Jetpack Glance) |
glance_widget_ios |
iOS implementation (WidgetKit) |
Example
Check the example directory for a complete demo app showing all 7 widget types.
cd example
flutter run
Contributing
Contributions are welcome! Please read our contributing guidelines before submitting PRs.
License
MIT License - see LICENSE for details.
Libraries
- glance_widget
- Flutter package for creating instant-updating home screen widgets for Android (Jetpack Glance) and iOS (WidgetKit).