flutter_native_widget

Write Android and iOS home screen widgets in Dart. A build_runner generator turns them into real native widgets β€” RemoteViews on Android, WidgetKit/SwiftUI on iOS β€” and wires up both projects for you. You never open Xcode or write Kotlin/Swift.

πŸ“– Full guide, step-by-step video and 14 examples with screenshots: flutter-native-widget.vercel.app (Get started Β· Examples)

@HomeScreenWidget(
  displayName: 'Counter',
  fields: [NWField.integer('count')],
  layout: NWColumn(children: [
    NWText('{count}', style: NWTextStyle(size: 44, weight: NWFontWeight.bold)),
    NWRow(spacing: 12, children: [
      NWButton(action: NWAction.decrement('count', min: 0), child: NWText('βˆ’')),
      NWButton(action: NWAction.increment('count'), child: NWText('+')),
    ]),
  ]),
)
class Counter {}
await CounterWidget.save(count: 5);   // generated, typed

The + and βˆ’ buttons change the value on the home screen, without starting your app (Android, and iOS 17+).

Why native?

The home screen is drawn by the launcher (Android) or SpringBoard (iOS), not by your app. They only accept a description of the UI in their own format β€” RemoteViews or SwiftUI in a separate, short-lived extension β€” so a Flutter engine cannot run there. This package lets you write that description in Dart and generates the native code from it. For anything the DSL cannot express, render a Flutter widget into an image (NWField.image + render<Field>).

Setup

flutter pub add flutter_native_widget
flutter pub add dev:flutter_native_widget_gen dev:build_runner
dart run flutter_native_widget_gen init     # once

init creates flutter_native_widget.yaml, adds the FNWWidgets widget extension target to the Xcode project (build phases, embedding, signing copied from your app, App Group entitlements) and registers background callbacks in AppDelegate.swift. Android needs no Gradle changes.

  1. Declare widgets in any file under lib/ and add the part directive:

    import 'package:flutter_native_widget/flutter_native_widget.dart';
    
    part 'widgets.fnw.dart';
    
  2. Generate (also writes the native code):

    dart run build_runner build      # or: watch
    
  3. Initialize in main:

    import 'home_widgets.g.dart';
    
    Future<void> main() async {
      WidgetsFlutterBinding.ensureInitialized();
      await initializeHomeWidgets();
      runApp(const MyApp());
    }
    

iOS signing: the extension signs like your app. On a real device the App Group in flutter_native_widget.yaml must exist for your team; Xcode's automatic signing creates it.

The layout language

Layouts are const trees of NW* elements. Values that change at runtime come from fields: {key} in text, or field: parameters.

Element What it does
NWColumn, NWRow Stack children; mainAxisAlignment (incl. spaceBetween), crossAxisAlignment (incl. stretch), spacing. Rows follow RTL.
NWStack Children on top of each other, aligned.
NWBox Size, padding, color or gradient, borderColor, radius, expand.
NWSpacer Takes the remaining space.
NWText Text with {field} placeholders, style, maxLines, alignment.
NWRichText Text whose NWSpans have their own size, weight, color, italic.
NWImage An image file from an image field (e.g. a rendered Flutter widget).
NWIcon A Material icon (Icons.favorite) on Android, the matching SF Symbol on iOS.
NWProgress Progress bar from a number field; NWProgress.circular draws a ring.
NWClock Digital time, updated by the system.
NWAnalogClock Clock face with hands; second hand on Android 12+.
NWDateText Today's date (updates at midnight) or a date field, ICU pattern.
NWTimer Live countdown/count-up to a dateTime field, ticking every second.
NWButton Makes its child tappable with an NWAction.
NWIf Shows child or orElse depending on an NWCondition.
NWRepeat Repeats a child; # in keys becomes the index ({title#}).
NWList Like NWRepeat, but scrolls on Android 12+. iOS cannot scroll: it shows the items that fit.

Reuse parts of a layout with top-level const values.

Fields

fields: [
  NWField.string('title', initial: 'β€”'),
  NWField.integer('count'),
  NWField.decimal('progress'),
  NWField.boolean('done', repeat: 4),   // done0..done3, List<bool> in Dart
  NWField.dateTime('nextAt'),
  NWField.image('cover'),
]

For each widget the generator writes a <Name>Widget class with save(...), read(), clear(), update(), saveTimeline(...) and, for image fields, render<Field>(Widget).

Actions

Action Runs Android iOS 17+ iOS 16
openApp([url or '/route']) opens the app βœ… βœ… βœ…
increment, decrement, toggle, set, cycle natively, no Dart βœ… βœ… (App Intent) opens the app, which applies it
callback('name') your @WidgetCallback in a background isolate βœ… βœ… (App Intent) opens the app, which runs it
@WidgetCallback(widget: 'NowPlaying')
Future<void> onMusicButton(WidgetEvent event) async {
  if (event.name == 'next') await NowPlayingWidget.save(title: 'Next song');
}

Launches from widgets arrive in FlutterNativeWidget.launches and FlutterNativeWidget.initialLaunch() with the widget name and route.

Timelines

Schedule values ahead of time; the widget switches on its own while the app is closed β€” prayer times, calendars, "event finished" states:

await PrayerTimesWidget.saveTimeline([
  PrayerTimesTimelineEntry(fajr, next: 'Ψ§Ω„ΨΈΩ‡Ψ±', nextAt: dhuhr),
  PrayerTimesTimelineEntry(dhuhr, next: 'Ψ§Ω„ΨΉΨ΅Ψ±', nextAt: asr),
]);

Android wakes the widget with AlarmManager (exact when allowed, otherwise within the system's idle windows); iOS uses WidgetKit timeline entries.

Image fields work in timelines too. Render each image to its own file and put the paths in the entries (a photo album that changes every hour):

final paths = [
  for (final (i, photo) in photos.indexed)
    await FlutterNativeWidget.renderImage(photo,
        widget: PhotoAlbumWidget.name, field: 'photo$i',
        logicalSize: const Size(360, 360)),
];
await PhotoAlbumWidget.saveTimeline([
  for (var i = 1; i < paths.length; i++)
    PhotoAlbumTimelineEntry(start.add(Duration(hours: i)), photo: paths[i]),
]);

Sizes and themes

sizes: [NWSize.small, NWSize.medium, NWSize.large], with optional smallLayout / mediumLayout / largeLayout. Every NWColor takes a dark: variant. Text and rows follow the device's reading direction.

Icons, rich text and fonts

import 'package:flutter/material.dart' show Icons;

NWRow(spacing: 4, children: [
  NWIcon(Icons.local_fire_department, size: 16, color: NWColor('#FF6B00')),
  NWRichText([
    NWSpan('{steps}', size: 20, weight: NWFontWeight.bold),
    NWSpan(' / {goal}', color: NWColor.secondaryLabel),
  ]),
  NWText('Hi', style: NWTextStyle(font: 'assets/fonts/Cairo-Bold.ttf')),
])
  • Icons: Android gets a small vector drawable of each icon used, not the whole font. iOS maps about 100 common icons to SF Symbols; for others pass sfSymbol: 'star.fill' (the generator tells you which).
  • Fonts: a .ttf/.otf path relative to the project root. It is copied to res/font and the iOS widget extension; Android 8+.

Choices per widget copy

@HomeScreenWidget(
  fields: [NWField.string('temp')],
  options: [NWOption('city', label: 'City', choices: ['Cairo', 'Giza'])],
  layout: NWText('{city}: {temp}'),
)
class Weather {}

await WeatherWidget.saveForCity('Giza', temp: '31Β°');

The user picks the city when placing the widget on Android (and from "Reconfigure" on Android 12+), and from "Edit widget" on iOS 17+. Each copy shows {city} and the values saved for its choice.

Data from the web, with the app closed

refresh: NWRefresh(
  url: 'https://wttr.in/{city}?format=j1',
  every: Duration(minutes: 30),
  values: {'temp': 'current_condition.0.temp_C'},
),

The download runs natively (Kotlin on Android, the widget extension on iOS), so it works without the Flutter engine. values maps fields to dot paths in the JSON; 'title#': 'items.#.title' fills a repeated field. With an option in the URL, each choice is downloaded on its own. The system picks the exact time: at least every 15 minutes, and iOS allows about 40–70 refreshes a day. iOS needs https. A failed download is retried after 5 minutes.

Lock Screen (iOS)

NWSize.circular, NWSize.rectangular and NWSize.inline add the widget to the iOS Lock Screen (and StandBy), with circularLayout / rectangularLayout / inlineLayout:

@HomeScreenWidget(
  sizes: [NWSize.small, NWSize.circular, NWSize.inline],
  layout: NWClock(),
  circularLayout: NWAnalogClock(size: 64),
  inlineLayout: NWDateText(pattern: 'EEE d MMM'),
)
class Clock {}

The system draws these in one tint, so your colors only change brightness; background and padding are not applied. inline shows only the first text of its layout. Android has no Lock Screen widgets: these sizes are skipped there, and a widget with only these sizes does not exist on Android.

Platform notes

Android iOS
Minimum API 23 (home_widget) 16.0 for the extension
Live clock / timer TextClock, Chronometer timeline every minute, Text(.timer)
Analog second hand Android 12+ not possible (no animation in widgets)
Buttons without opening the app βœ… iOS 17+
Updating images in the background βœ… (callbacks) only while the app runs
Scrolling lists (NWList) Android 12+ not possible (the items that fit)
Choices per copy (NWOption) when placed; Reconfigure on 12+ iOS 17+ (Edit widget)
Downloads (NWRefresh) βœ… βœ… (https only)
Custom fonts Android 8+ βœ…

Widgets cannot animate on either platform, and only NWList scrolls (Android).

CLI

dart run flutter_native_widget_gen init     # one-time project setup
dart run flutter_native_widget_gen sync     # rewrite native code from the last build
dart run flutter_native_widget_gen doctor   # check the wiring
dart run flutter_native_widget_gen clean    # remove everything it added

init keeps a copy of the original project file as ios/Runner.xcodeproj/project.pbxproj.fnw-backup.

What gets generated

Where What
lib/**.fnw.dart typed widget classes (part files)
lib/home_widgets.g.dart specs, callbacks, initializeHomeWidgets()
android/app/src/main/kotlin/<namespace>/fnw/FnwWidgets.kt providers + runtime
android/app/src/main/res/*/fnw_* layouts, widget info, colors, drawables, strings
AndroidManifest.xml receivers, between flutter_native_widget markers
ios/FNWWidgets/ SwiftUI widgets, Info.plist, entitlements
ios/Runner/FNWCallbackIntent.swift App Intent that runs Dart callbacks

Commit them like any other source. Nothing outside the markers and these files is touched.

flutter_native_widget or home_widget?

This package is built on home_widget. home_widget stores data and tells the widget to update, but you write the widget itself in Kotlin + XML (Android) and Swift (iOS). flutter_native_widget writes that native code for you from one Dart layout.

Use flutter_native_widget when

  • you want one layout for both platforms and no Kotlin, XML or Swift;
  • the widget is made of text, icons, images, progress bars, clocks, lists and buttons (most widgets are);
  • you want a typed save() / read() and buttons that work without opening the app, without wiring them yourself.

Use home_widget directly when

  • you already have native widgets, or you prefer writing Kotlin and Swift;
  • the design needs native features the DSL does not have and an image will not do, e.g. Jetpack Glance/Compose, Swift Charts, custom drawing, your own App Intents or widget configuration;
  • the widget extension needs native SDKs (HealthKit, location, …);
  • your iOS project already has a hand-made widget extension or uses flavors (not tested with init yet), or you do not want a tool editing project.pbxproj;
  • you only need to send data to a widget someone else built natively.

You can also mix them: home_widget calls such as requestPinWidget still work next to this package.

Built on

home_widget for storage, updates, background callbacks and rendering Flutter widgets to images.

Libraries

dsl
The declarative widget language, without the Flutter runtime.
flutter_native_widget
Home screen widgets for Android and iOS, written in Dart.