Flutter · Android · iOS

Home Screen Widgets حقيقية، مكتوبة بـ Dart بس Real home screen widgets, written only in Dart

بتوصف الـ widget بـ Dart، و build_runner بيولّد Kotlin و RemoteViews على أندرويد، و SwiftUI و WidgetKit على iOS. مش هتفتح Xcode، ومش هتكتب سطر Kotlin ولا Swift.

You describe the widget in Dart, and build_runner generates Kotlin and RemoteViews for Android and SwiftUI and WidgetKit for iOS. You never open Xcode or write a line of Kotlin or Swift.

flutter pub add flutter_native_widget
Counter widget Water widget Watch face widget
لقطات حقيقية من Android 15Real screenshots from Android 15

ليه native؟Why native?

الـ widget اللي على الشاشة الرئيسية مش بيرسمها الأبليكيشن بتاعك. بيرسمها الـ launcher على أندرويد و SpringBoard على iOS، وهما مابيقبلوش غير وصف للشكل بلغتهم: RemoteViews على أندرويد، و SwiftUI جوه Widget Extension صغيرة وعمرها قصير على iOS. علشان كده محرك Flutter مايقدرش يشتغل هناك.

A home screen widget is not drawn by your app. The launcher (Android) or SpringBoard (iOS) draws it, and they only accept a description of the UI in their own format: RemoteViews on Android, SwiftUI inside a small, short-lived Widget Extension on iOS. That is why the Flutter engine cannot run there.

Native بجدTruly native

النص والساعة والأزرار views حقيقية، مش صورة. بتتظبط لوحدها مع dark mode والحجم والـ RTL.

Text, clocks and buttons are real views, not a picture. They follow dark mode, size and RTL on their own.

شغّال والأبليكيشن مقفولWorks with the app closed

الأزرار، والـ timeline، والساعة، والتحميل من النت كلهم بيشتغلوا من غير ما Flutter يصحى.

Buttons, timelines, clocks and downloads all run without waking Flutter up.

كود في مكان واحدOne source of truth

بتكتب الـ widget مرة واحدة بـ Dart. لو غيّرتها، شغّل build_runner والمنصتين يتحدّثوا مع بعض.

Write the widget once in Dart. Change it, run build_runner, and both platforms update together.

إزاي بتشتغل: وقت الـ buildHow it works: at build time

الـ generator بيقرا الـ @HomeScreenWidget بتاعك كـ constant (من غير ما يشغّله)، ويحوّله لشكل وسيط (IR)، وبعدين يكتب منه 3 حاجات.

The generator reads your @HomeScreenWidget as a constant (without running it), turns it into an intermediate representation (IR), and writes three things from it.

lib/widgets.dart

annotation فيه layout const وحقول.

An annotation with a const layout and fields.

reader → IR

الـ analyzer بيقرا الشجرة، ويتأكد منها، ويطلّع أخطاء واضحة.

The analyzer reads the tree, validates it and reports clear errors.

emitters

emitter لكل منصة بيكتب الكود بتاعها.

One emitter per platform writes its code.

Android

Kotlin providers و RemoteViews XML و drawables وتعديلات في الـ manifest

Kotlin providers, RemoteViews XML, drawables, manifest entries

iOS

SwiftUI و WidgetKit providers و App Intents

SwiftUI, WidgetKit providers, App Intents

Dart

API typed زي CounterWidget.save(count: 5)

A typed API like CounterWidget.save(count: 5)

الداتا بتتنقل إزاي؟How data moves

الأبليكيشن والـ widget عمرهم ما بيكلّموا بعض على طول. الاتنين بيقروا ويكتبوا في مخزن مشترك: SharedPreferences على أندرويد، و UserDefaults الخاصة بالـ App Group على iOS. كل قيمة بتتخزن كنص تحت المفتاح fnw.<Widget>.<field>.

The app and the widget never talk to each other directly. Both read and write a shared store: SharedPreferences on Android and the App Group's UserDefaults on iOS. Every value is stored as a string under fnw.<Widget>.<field>.

الأبليكيشن (Dart)Your app (Dart)

بتحفظ القيمةSaves a value

await CounterWidget.save(count: 5);

الـ API المتولّد بيحوّل كل حقل لنص، وبعدين بيطلب إعادة رسم.

The generated API turns each field into a string, then asks for a redraw.

←→
المخزن المشتركShared store

مفتاح لكل حقلOne key per field

fnw.Counter.count: "5"
fnw.Tasks.title0: "Buy bread"
fnw.Tasks.done0: "true"
fnw.PrayerTimes.__timeline: "[…]"

ده مكان الحقيقة الوحيد. اللي بيكتب فيه ممكن يكون الأبليكيشن، أو زرار native، أو callback، أو download.

The single source of truth. The app, a native button, a callback or a download can write here.

←→
الـ widget (native)The widget (native)

بيقرا ويرسمReads and draws

// Android: AppWidgetProvider
prefs.getString("fnw.Counter.count")
// iOS: TimelineProvider
store.string(forKey: "fnw.Counter.count")

بيبدّل {count} بالقيمة، ويقيّم الـ NWIf، ويرسم.

Replaces {count} with the value, evaluates each NWIf, and draws.

الطرق اللي الداتا بتوصل بيها للـ widgetEvery way data reaches a widget

كل طريقة في الآخر بتكتب في نفس المخزن. الفرق بينهم: مين اللي بيكتب، وإمتى.

They all end up writing the same store. What differs is who writes, and when.

1

من الأبليكيشنFrom the app

  1. XWidget.save(...) بيكتب كل حقل في المخزن.
  2. بعدها بينادي update()، فيتبعت broadcast لـ AppWidgetProvider على أندرويد، و WidgetCenter.reloadTimelines على iOS.
  3. الـ widget تقرا القيم وتترسم من جديد.
  1. XWidget.save(...) writes each field to the store.
  2. It then calls update(): a broadcast to the AppWidgetProvider on Android, WidgetCenter.reloadTimelines on iOS.
  3. The widget reads the values and redraws.
savereadclearupdate: false
2

أزرار native (من غير Dart)Native buttons (no Dart)

  1. الضغطة على NWAction.increment/toggle/set/cycle بتتحول لـ URI زي fnw://action?w=Counter&op=inc&k=count.
  2. على أندرويد: PendingIntent بيروح للـ provider، اللي بيعدّل القيمة في SharedPreferences. على iOS 17+: AppIntent جوه الـ extension بيعمل نفس الحاجة.
  3. الـ widget بتترسم من جديد على طول، ومحرك Flutter مابيشتغلش خالص.
  1. A tap on NWAction.increment/toggle/set/cycle becomes a URI such as fnw://action?w=Counter&op=inc&k=count.
  2. Android: a PendingIntent reaches the provider, which changes the value in SharedPreferences. iOS 17+: an AppIntent in the extension does the same.
  3. The widget redraws right away; the Flutter engine never starts.
iOS 16: بيفتح الأبليكيشن اللي بتطبّق الأكشنiOS 16: opens the app, which applies it
3

Callbacks بـ DartDart callbacks

  1. NWAction.callback('next') بيبعت URI للـ HomeWidgetBackgroundIntent على أندرويد، أو لـ FNWCallbackIntent على iOS.
  2. الـ home_widget بيشغّل Dart في background isolate وينادي الـ @WidgetCallback بتاعك ومعاه WidgetEvent.
  3. انت تحسب اللي محتاجه وتنادي save()، فالـ widget تتحدّث.
  1. NWAction.callback('next') sends a URI to HomeWidgetBackgroundIntent on Android, or to FNWCallbackIntent on iOS.
  2. home_widget runs Dart in a background isolate and calls your @WidgetCallback with a WidgetEvent.
  3. You compute what you need and call save(); the widget updates.
مالوش access لـ state الأبليكيشنNo access to your app's state
4

Timeline

  1. saveTimeline([...]) بيرتّب الـ entries ويحفظها JSON في fnw.<Widget>.__timeline.
  2. على أندرويد: الـ provider بيختار الـ entry الحالية، ويضبط AlarmManager على ميعاد الـ entry اللي بعدها (exact لو مسموح).
  3. على iOS: كل entry بتبقى TimelineEntry في WidgetKit، والنظام بيبدّل بينهم لوحده.
  1. saveTimeline([...]) sorts the entries and stores them as JSON in fnw.<Widget>.__timeline.
  2. Android: the provider picks the current entry and sets an AlarmManager alarm for the next one (exact when allowed).
  3. iOS: each entry becomes a WidgetKit TimelineEntry; the system switches between them.
5

من النت (NWRefresh)From the web (NWRefresh)

  1. الكود الـ native بيحمّل الـ URL بنفسه: HttpURLConnection على أندرويد، و URLSession جوه الـ extension على iOS.
  2. كل حقل بياخد قيمته من مسار في الـ JSON، زي current_condition.0.temp_C، و items.#.title لحقل متكرر.
  3. بيحمّل كل every (15 دقيقة على الأقل)، ولو فشل بيحاول تاني بعد 5 دقايق.
  1. Native code downloads the URL itself: HttpURLConnection on Android, URLSession in the extension on iOS.
  2. Each field takes a value from a JSON path, such as current_condition.0.temp_C, or items.#.title for repeated fields.
  3. It downloads every every (15 minutes at least) and retries 5 minutes after a failure.
6

اختيار لكل نسخة (NWOption)Per-copy choice (NWOption)

  1. المستخدم يختار قيمة (مدينة مثلًا) لكل نسخة من الـ widget. على أندرويد ده بيحصل في Configure activity، وعلى iOS 17+ من Edit widget.
  2. القيم بتتخزن لكل اختيار لوحده: WeatherWidget.saveForCity('Cairo', …).
  3. كل نسخة بتعرض {city} والقيم الخاصة بالاختيار بتاعها.
  1. The user picks a value (a city, say) for each copy of the widget: a Configure activity on Android, Edit widget on iOS 17+.
  2. Values are stored per choice: WeatherWidget.saveForCity('Cairo', …).
  3. Each copy shows {city} and the values of its own choice.
7

صور مرسومة بـ FlutterImages drawn by Flutter

  1. renderChart(MyChart()) بيرسم أي Flutter widget ويحفظها PNG في الـ container المشترك.
  2. مسار الملف بيتحفظ في الحقل (NWField.image).
  3. الكود الـ native بيقرا الملف، وعلى أندرويد بيصغّره لحد 800px، ويعرضه في NWImage.
  1. renderChart(MyChart()) draws any Flutter widget and saves it as a PNG in the shared container.
  2. The file path is saved into the field (NWField.image).
  3. Native code loads the file (downsampled to 800px on Android) and shows it in NWImage.
8

من الـ widget للأبليكيشنFrom the widget back to the app

  1. NWAction.openApp('/tasks') بيفتح الأبليكيشن بـ URI فيه اسم الـ widget والـ route.
  2. FlutterNativeWidget.initialLaunch() بيرجّعلك الـ launch اللي فتح الأبليكيشن، و launches stream بيجيلك عليه كل launch والأبليكيشن مفتوح.
  3. انت تعمل navigate للـ route.
  1. NWAction.openApp('/tasks') opens the app with a URI carrying the widget name and route.
  2. FlutterNativeWidget.initialLaunch() gives you a cold start, and the launches stream each launch while the app is running.
  3. You navigate to the route.

إيه اللي يتحط في dev_dependencies؟What goes in dev_dependencies?

مش كله. الباكدج الأساسية لازم تبقى dependency عادية، لأن الكود بتاعك والكود المتولّد بيستخدموها والأبليكيشن شغّال. الـ generator و build_runner بس هما اللي بيتحطوا dev، لأنهم بيشتغلوا وقت الـ build بس، ومش بيدخلوا في الأبليكيشن اللي بتنشره.

Not all of them. The main package must be a normal dependency, because your code and the generated code use it while the app runs. Only the generator and build_runner are dev dependencies: they run at build time and never ship in your app.

الباكدجPackageفينWhereليهWhy
flutter_native_widgetdependencies فيها الـ DSL (NWText…) اللي بتعمله import في lib/، والـ runtime اللي الكود المتولّد بيستخدمه (save و update و callbacks).Holds the DSL (NWText…) you import in lib/ and the runtime the generated code calls (save, update, callbacks).
flutter_native_widget_gendev_dependencies الـ generator والـ CLI. بيشتغلوا على جهازك بس.The generator and the CLI. They only run on your machine.
build_runnerdev_dependencies هو اللي بيشغّل الـ generator.Runs the generator.
home_widgetمش لازمNot needed بييجي لوحده مع flutter_native_widget. ضيفه بس لو هتستخدمه انت مباشرةً، زي HomeWidget.requestPinWidget في المثال.Comes with flutter_native_widget. Add it only if you call it yourself, like HomeWidget.requestPinWidget in the example.
dependencies:
  flutter:
    sdk: flutter
  flutter_native_widget: ^0.1.0      # runtime + DSL

dev_dependencies:
  flutter_native_widget_gen: ^0.1.0  # generator + CLI
  build_runner: ^2.4.0

الحدودLimitations

فيه نوعين: حدود المنصة نفسها، ودي موجودة في أي باكدج أو حتى لو كتبت native بإيدك. وحدود الباكدج دلوقتي، ودي ممكن تتحل بعدين.

Two kinds: limits of the platforms themselves, which every package (and hand-written native code) shares, and current limits of this package, which can be lifted later.

حدود المنصةPlatform limits

✕

مفيش أنيميشن حر، يعني لا حاجة بتلف ولا بتنبض. البديل: timeline بصور مختلفة.

No free animation: nothing spins or pulses. Workaround: a timeline of different images.

✕

مفيش تحديث كل ثانية لأي حاجة غير الساعة (NWClock) والعدّاد (NWTimer).

No per-second updates except for the clock (NWClock) and timer (NWTimer).

✕

مفيش كتابة نص (TextField)، ولا gestures غير الضغطة (سحب، ضغطة طويلة، pinch).

No text input and no gestures other than a tap (no drag, long-press or pinch).

✕

مفيش scroll على iOS خالص. على أندرويد في NWList بس (Android 12+).

No scrolling on iOS at all. On Android, only in NWList (Android 12+).

✕

مفيش فيديو ولا GIF، ولا Flutter بيشتغل جوه الـ widget نفسها.

No video or GIFs, and Flutter never runs inside the widget itself.

!

iOS بيدّي ميزانية تحديثات حوالي 40–70 مرة في اليوم. استخدم timeline بدل التحديثات الكتير. والتحميل على iOS لازم يكون https.

iOS has a refresh budget of about 40–70 updates a day. Prefer timelines over frequent updates. iOS downloads must use https.

!

عقرب الثواني بيظهر على Android 12+ بس. على iOS مش ممكن.

Second hand only on Android 12+. Not possible on iOS.

!

ظلال و blur وأشكال بـ path مش متاحين native على أندرويد. البديل: ارسمها بـ Flutter كصورة.

Shadows, blur and path shapes are not available natively on Android. Workaround: render them with Flutter as an image.

حدود الباكدج دلوقتيCurrent package limits

الحدLimitالسبب / البديلWhy / workaround
الـ layout لازم يبقى constThe layout must be constالـ generator بيقراه وقت الـ build. القيم المتغيرة بتيجي من الحقول و NWIf و timeline.The generator reads it at build time. Dynamic values come from fields, NWIf and timelines.
مينفعش تستخدم if و for العاديين، ولا functions بترجع nodesNo plain if/for or functions returning nodesاستخدم NWIf و NWRepeat، وحط الأجزاء اللي بتتكرر في const top-level.Use NWIf and NWRepeat, and share pieces as top-level consts.
أقصى عدد عناصر بيتحدد وقت الـ buildMaximum item count is fixed at build timeNWRepeat(count: 4) / NWList(count: 10)
القوايم اللي بتعمل scroll محتاجة Android 12+Scrolling lists need Android 12+على الإصدارات الأقدم بتظهر عمود عادي.Older versions show a plain column.
NWRefresh بيقرا JSON بسNWRefresh reads JSON onlyمسارات بالنقطة، من غير تحويلات.Dot paths, no transformations.
الأيقونات من Material Icons بسIcons come from Material Icons onlyعلى iOS بتتحول لـ SF Symbol من جدول فيه حوالي 100 أيقونة. لو أيقونتك مش فيه استخدم sfSymbol:.Mapped to SF Symbols on iOS (about 100 built in); pass sfSymbol: for others.
لو غيّرت اسم حقل بعد النشرRenaming a field after releaseالداتا القديمة اللي على أجهزة المستخدمين هتفضل متخزنة بالاسم القديم.Old data on users' devices stays under the old name.
مشروع فيه Widget Extension معمول يدوي، أو flavorsProjects with a hand-made Widget Extension or flavorsلسه ماتجرّبش. الـ init بيضيف FNWWidgets جنب الـ extension الموجود، مش بيدمج معاه.Not tested yet; init adds FNWWidgets next to it rather than merging.

أعمل الحتة دي إزاي؟How do I build this part?

الحتة دي بتتغير كل ثانية أو دقيقة؟ ├── أيوه، وهي ساعة أو تاريخ أو عدّاد ← NWClock / NWAnalogClock / NWDateText / NWTimer ├── أيوه، وهي حاجة تانية ← مش هينفع؛ استخدم timeline كل دقيقة └── لأ ├── بتتغير في مواعيد معروفة ← Timeline ├── بتتغير لما المستخدم يدوس ← NWAction (native) أو ‎@WidgetCallback (Dart) ├── جاية من النت ← NWRefresh └── بتتغير لما الأبليكيشن يفتح ← <Name>Widget.save(...) الـ DSL يقدر يرسمها؟ ├── أيوه ← native (أخف، وبتتظبط مع dark mode والحجم) └── لأ ← NWField.image + render<Field>() بأي Flutter widget
Does this part change every second or minute? ├── Yes, and it's a clock, date or timer → NWClock / NWAnalogClock / NWDateText / NWTimer ├── Yes, and it's something else → not possible; use a timeline every minute └── No ├── It changes at known times → Timeline ├── It changes when the user taps → NWAction (native) or @WidgetCallback (Dart) ├── It comes from the web → NWRefresh └── It changes when the app opens → <Name>Widget.save(...) Can the DSL draw it? ├── Yes → native (lighter, follows dark mode and size) └── No → NWField.image + render<Field>() with any Flutter widget

دعم المنصاتPlatform support

اتجرّب على Android 15 (emulator) و iOS 27 (simulator).

Tested on Android 15 (emulator) and iOS 27 (simulator).

AndroidiOS
أقل إصدارMinimumAPI 2316.0 للـ extension16.0 for the extension
ساعة / عدّاد حيLive clock / timerTextClock, Chronometertimeline كل دقيقة، Text(.timer)minute timeline, Text(.timer)
أزرار من غير فتح الأبليكيشنButtons without opening the app✓iOS 17+
Dart callbacksDart callbacks✓iOS 17+
قوايم بتعمل scrollScrolling listsAndroid 12+مش ممكنNot possible
اختيار لكل نسخةChoice per copyلما تضيفها، و Reconfigure على 12+When placed; Reconfigure on 12+iOS 17+
تحميل من النتDownloads✓✓ https بس✓ https only
Lock Screen / StandBy—✓
تحديث الصور في الـ backgroundUpdating images in the background✓والأبليكيشن شغّال بسOnly while the app runs
خطوط مخصصةCustom fontsAndroid 8+✓