A Flutter app can't draw its own home screen widget. That surprises people the first time they try it. On iOS the widget is a SwiftUI view rendered by WidgetKit inside a separate extension process, and on Android it's a RemoteViews or Jetpack Glance layout hosted by the launcher. Your Dart code never runs on the home screen. So building a Flutter home screen widget is really three jobs: write a small native widget per platform, put the data somewhere both the app and the widget can read it, and tell the OS when that data has changed.

We shipped native iOS and Android widgets (plus lock-screen widgets and Live Activities) in Crumb Count, a Flutter nutrition app, and this is the setup we'd use again.

How a Flutter home screen widget gets its data

The widget process and the app process don't share memory. They share storage. On iOS that means an App Group: a shared container that both the Runner target and the widget extension are entitled to. You create the group in your Apple Developer account, add it under Signing and Capabilities on both targets in Xcode, and then both sides read and write the same UserDefaults(suiteName: "group.your.id"). On Android there is no extension boundary in the same sense. The widget's receiver runs inside your app's package, so it can read the app's SharedPreferences directly.

The home_widget package on pub.dev wraps both of these behind one Dart API. The minimum on the Flutter side looks like this:

await HomeWidget.setAppGroupId('group.com.example.app');
await HomeWidget.saveWidgetData<String>('today_summary', '1,420 kcal');
await HomeWidget.updateWidget(
  iOSName: 'TodayWidget',
  androidName: 'TodayWidgetReceiver',
);

Skip setAppGroupId on iOS and the package's own docs say saveWidgetData will return an error, because there's nowhere shared to write to. Most "my widget shows nothing" bugs we've seen trace back to the group ID being different in the Dart call, the Runner entitlement and the extension entitlement. Three places. One typo.

iOS: WidgetKit side

Your widget extension has a TimelineProvider with placeholder, getSnapshot and getTimeline. In getTimeline you read from the shared suite and return entries:

let prefs = UserDefaults(suiteName: "group.com.example.app")
let summary = prefs?.string(forKey: "today_summary") ?? "No meals yet"

When Dart calls updateWidget, the package asks WidgetKit to reload that widget kind's timelines. WidgetKit decides when it actually redraws. Apple budgets reloads, so a widget that tries to refresh every few seconds will simply stop being refreshed on schedule. Design for "correct within a few minutes", and push anything that needs to be live (a timer, a delivery ETA) into a Live Activity instead. We wrote up that side separately in our Flutter Live Activities post.

Android: Glance or classic AppWidget

On Android you can still write a classic AppWidgetProvider with RemoteViews XML, and home_widget ships a HomeWidgetProvider base class for that. For new widgets we'd use Jetpack Glance, which lets you write the layout in a Compose-style Kotlin API. The package supports it: your GlanceAppWidget overrides stateDefinition to return HomeWidgetGlanceStateDefinition(), your receiver extends HomeWidgetGlanceWidgetReceiver, and inside the widget you read currentState.preferences like any SharedPreferences. You still register the receiver in AndroidManifest.xml with the APPWIDGET_UPDATE intent filter and a metadata XML that sets sizes and the update period.

That update period has a floor. Android won't deliver updatePeriodMillis updates more often than every 30 minutes, so if you need fresher data, update from the app (or from a background job) and call updateWidget yourself.

Decisions that matter more than the setup

The wiring above takes a day. The choices below decide whether the widget is still pleasant to maintain a year later.

  • Store a view model, not your database. Write the exact strings and numbers the widget renders ("1,420 / 1,800 kcal", a progress fraction, a last-updated timestamp). Don't make Swift and Kotlin re-implement your Dart business logic. If the widget needs to compute something, you now have three copies of it.
  • Use one JSON blob with a version field once you have more than three or four keys. Old widget code reading new data is a real situation after an app update, and a version number lets the native side fall back cleanly instead of crashing in a process you can't attach a debugger to easily.
  • Write on every state change that the widget shows, not on app pause. iOS can suspend your app before a pause handler finishes. In Crumb Count the write happens right after a meal is logged locally, before any network sync, so the widget agrees with the app even offline.
  • Keep secrets out of the shared container. App Group defaults and SharedPreferences are not a keychain. Auth tokens stay in secure storage; the widget gets display data only.
  • Plan for the empty state. A fresh install, a logged-out user and a user who hasn't opened the app today are three different screens. Placeholder text that says "Open the app to set up" beats a zero that looks like a bug.

Interactivity, images and other edge cases

Tapping a widget should usually deep link into a specific screen. home_widget can launch the app with a URI you read in Dart, and it also supports interactive widgets that call back into Dart in the background (iOS 17 App Intents and Android broadcast actions under the hood). Check the package docs for the current setup, because this part has changed between major versions and needs extra native registration.

If your widget needs a chart that would be painful to rebuild in SwiftUI and Glance, the package can render a Flutter widget to an image and save it to the shared container. It works, but it's a static PNG: it won't follow dark mode or dynamic type unless you render both variants. For anything text-heavy we'd write native views. They look right on the home screen and respect system font sizes.

Testing is the awkward part. Widgets don't show up in Flutter's widget tests. What we do instead: unit test the Dart function that builds the widget payload, use Xcode's widget previews and Android Studio's Glance preview for the native views, and keep a manual checklist (fresh install, logged out, offline, after an app update, dark mode, largest font size) that someone actually runs before a release.

Is a Flutter home screen widget worth building?

For apps people open daily (habits, nutrition, finance balances, deliveries) a widget is often the feature users mention first in reviews. For apps opened once a month it's usually wasted effort. The cost is mostly the native code and the release testing across two platforms, and that cost is ongoing: every OS release changes something about widgets.

If you're planning one and want a second opinion on the data model or the native side, orithLabs builds Flutter apps with Swift and Kotlin where the platform needs it. Our mobile development page explains how we work, and a first call costs nothing.