1. מבוא
מהם ווידג'טים?
למפתחים ב-Flutter, ההגדרה הנפוצה של ווידג'ט מתייחסת לרכיבי ממשק משתמש שנוצרו באמצעות מסגרת Flutter. במסגרת ה-codelab הזה, ווידג'ט הוא גרסה מוקטנת של אפליקציה שמציגה את המידע של האפליקציה בלי לפתוח אותה. ב-Android, הווידג'טים נמצאים במסך הבית. ב-iOS, אפשר להוסיף אותם למסך הבית, למסך הנעילה או לתצוגת היום.

מה יכולה להיות מידת המורכבות של הווידג'ט?
רוב הווידג'טים במסך הבית הם פשוטים. הם עשויים להכיל טקסט בסיסי, גרפיקה פשוטה או, ב-Android, אמצעי בקרה בסיסיים. ב-Android וב-iOS יש הגבלות על רכיבי ממשק המשתמש והתכונות שבהם אפשר להשתמש.

יצירת ממשק משתמש לווידג'טים
בגלל המגבלות האלה בממשק המשתמש, אי אפשר לצייר ישירות את ממשק המשתמש של הווידג'ט במסך הבית באמצעות ה-framework של Flutter. במקום זאת, אתם יכולים להוסיף לאפליקציית Flutter ווידג'טים שנוצרו באמצעות מסגרות פלטפורמה כמו Jetpack פיתוח נייטיב או SwiftUI. במעבדת הקוד הזו מוצגות דוגמאות לשיתוף משאבים בין האפליקציה לווידג'טים כדי להימנע משכתוב של ממשק משתמש מורכב.
מה תפַתחו
ב-codelab הזה תלמדו איך ליצור ווידג'טים למסך הבית ב-Android וב-iOS לאפליקציית Flutter פשוטה, באמצעות חבילת home_widget, שמאפשרת למשתמשים לקרוא מאמרים. הווידג'טים:
- הצגת נתונים מהאפליקציה שלכם ב-Flutter.
- הצגת טקסט באמצעות נכסי גופן ששותפו מאפליקציית Flutter.
- תמונה של ווידג'ט Flutter שעבר רינדור.

אפליקציית Flutter הזו כוללת שני מסכים (או נתיבים):
- הכרטיס הראשון מציג רשימה של מאמרים חדשותיים עם כותרות ותיאורים.
- בשני מוצג המאמר המלא עם תרשים שנוצר באמצעות
CustomPaint.
.

מה תלמדו
- איך יוצרים ווידג'טים במסך הבית ב-iOS וב-Android.
- איך משתמשים בחבילת home_widget כדי לשתף נתונים בין הווידג'ט במסך הבית לבין אפליקציית Flutter.
- איך לצמצם את כמות הקוד שצריך לכתוב מחדש.
- איך מעדכנים את הווידג'ט של מסך הבית מאפליקציית Flutter.
2. הגדרת סביבת הפיתוח
בשתי הפלטפורמות צריך Flutter SDK וסביבת פיתוח משולבת (IDE). אתם יכולים להשתמש בסביבת הפיתוח המשולבת (IDE) המועדפת עליכם כדי לעבוד עם Flutter. יכול להיות שזו סביבת Visual Studio Code עם התוספים Dart Code ו-Flutter, או Android Studio או IntelliJ עם הפלאגינים Flutter ו-Dart שהותקנו.
כדי ליצור את הווידג'ט של מסך הבית ב-iOS:
- אפשר להריץ את ה-codelab הזה במכשיר iOS פיזי או בסימולטור iOS.
- צריך להגדיר מערכת macOS עם סביבת הפיתוח המשולבת (IDE) של Xcode. הפעולה הזו מתקינה את הקומפיילר שנדרש כדי ליצור את גרסת ה-iOS של האפליקציה.
כדי ליצור את הווידג'ט של מסך הבית ב-Android:
- אפשר להריץ את ה-codelab הזה במכשיר Android פיזי או באמולטור Android.
- צריך להגדיר את מערכת הפיתוח באמצעות Android Studio. הפעולה הזו מתקינה את הקומפיילר שנדרש כדי ליצור את גרסת Android של האפליקציה.
קבלת קוד לתחילת הדרך
הורדת הגרסה הראשונית של הפרויקט מ-GitHub
משכפלים משורת הפקודה את מאגר GitHub לספרייה flutter-codelabs:
$ git clone https://github.com/flutter/codelabs.git flutter-codelabs
אחרי שיבוט המאגר, אפשר למצוא את הקוד של ה-Codelab הזה בספרייה flutter-codelabs/homescreen_codelab. הספרייה הזו מכילה קוד פרויקט שהושלם לכל שלב ב-Codelab.
פתיחת אפליקציה לתחילת הדרך
פותחים את הספרייה flutter-codelabs/homescreen_codelab/step_03 בסביבת הפיתוח המשולבת (IDE) המועדפת.
התקנת חבילות
כל החבילות הנדרשות נוספו לקובץ pubspec.yaml של הפרויקט. כדי לאחזר את התלות של הפרויקט, מריצים את הפקודה הבאה:
$ flutter pub get
3. הוספת ווידג'ט בסיסי למסך הבית
קודם צריך להוסיף את הווידג'ט למסך הבית באמצעות כלי הפלטפורמה המקוריים.
יצירת ווידג'ט בסיסי למסך הבית ב-iOS
הוספת תוסף אפליקציה לאפליקציית Flutter iOS דומה להוספת תוסף אפליקציה לאפליקציית SwiftUI או UIKit:
- מריצים את הפקודה
open ios/Runner.xcworkspaceבחלון טרמינל מתוך ספריית פרויקט Flutter. אפשר גם ללחוץ לחיצה ימנית על התיקייה ios מ-VSCode ולבחור באפשרות Open in Xcode (פתיחה ב-Xcode). סביבת העבודה של Xcode תיפתח בפרויקט Flutter. - בתפריט, בוחרים באפשרות קובץ ← חדש ← יעד. פעולה זו תוסיף יעד חדש לפרויקט.
- מופיעה רשימה של תבניות. בוחרים באפשרות תוסף ווידג'ט.
- מקלידים NewsWidgets בתיבה שם המוצר של הווידג'ט. מבטלים את הסימון בתיבות Include Live Activity ו-Include Configuration Intent.
בדיקת הקוד לדוגמה
כשמוסיפים יעד חדש, Xcode יוצר קוד לדוגמה על סמך התבנית שבחרתם. מידע נוסף על הקוד שנוצר ועל WidgetKit מופיע במאמרי העזרה של Apple בנושא תוספים לאפליקציות .
ניפוי באגים ובדיקה של הווידג'ט לדוגמה
- קודם צריך לעדכן את ההגדרות של אפליקציית Flutter. חובה לבצע את הפעולה הזו כשמוסיפים חבילות חדשות לאפליקציית Flutter ומתכננים להפעיל יעד בפרויקט מ-Xcode. כדי לעדכן את ההגדרות של האפליקציה, מריצים את הפקודה הבאה בספריית האפליקציה ב-Flutter:
$ flutter build ios --config-only
- לוחצים על Runner כדי להציג רשימה של יעדים. בוחרים את יעד הווידג'ט שיצרתם הרגע, NewsWidgets, ולוחצים על Run (הפעלה). מריצים את יעד הווידג'ט מ-Xcode כשמשנים את קוד הווידג'ט ל-iOS.

- בסימולטור או במסך המכשיר אמור להופיע ווידג'ט בסיסי של מסך הבית. אם היא לא מופיעה, אפשר להוסיף אותה למסך. לוחצים לחיצה ארוכה על מסך הבית ואז לוחצים על + בפינה הימנית העליונה.

- חפשו את שם האפליקציה. בסדנת הקוד הזו, חפשו את האפליקציה Homescreen Widgets (ווידג'טים למסך הבית).

- אחרי שמוסיפים את הווידג'ט למסך הבית, אמור להופיע טקסט פשוט עם השעה.
יצירת ווידג'ט בסיסי ל-Android
- כדי להוסיף ווידג'ט למסך הבית ב-Android, פותחים את קובץ ה-build של הפרויקט ב-Android Studio. הקובץ הזה נמצא בנתיב android/build.gradle. אפשר גם ללחוץ לחיצה ימנית על התיקייה android מ-VSCode ולבחור באפשרות Open in Android Studio (פתיחה ב-Android Studio).
- אחרי שהפרויקט נוצר, מאתרים את ספריית האפליקציה בפינה הימנית העליונה. מוסיפים את הווידג'ט החדש למסך הבית לספרייה הזו. לוחצים לחיצה ימנית על הספרייה, בוחרים באפשרות New -> Widget -> App Widget (חדש -> ווידג'ט -> ווידג'ט של אפליקציה).

- מוצג טופס חדש ב-Android Studio. הוספת מידע בסיסי על הווידג'ט במסך הבית, כולל שם המחלקה, המיקום, הגודל ושפת המקור
ב-codelab הזה, מגדירים את הערכים הבאים:
- התיבה שם הכיתה ל-NewsWidget
- בתפריט הנפתח רוחב מינימלי (תאים) בוחרים באפשרות 3.
- בתפריט הנפתח גובה מינימלי (תאים), בוחרים באפשרות 3.
בדיקת הקוד לדוגמה
כששולחים את הטופס, Android Studio יוצר ומעדכן כמה קבצים. בטבלה הבאה מפורטים השינויים שרלוונטיים ל-codelab הזה
פעולה | קובץ היעד | שינוי |
עדכון |
| מוסיף מקלט חדש שרושם את NewsWidget. |
יצירה |
| הגדרת ממשק המשתמש של הווידג'ט במסך הבית. |
יצירה |
| הגדרה של הווידג'ט במסך הבית. אפשר לשנות את המידות או השם של הווידג'ט בקובץ הזה. |
יצירה |
| מכיל את קוד Kotlin שמוסיף פונקציונליות לווידג'ט של מסך הבית. |
פרטים נוספים על הקבצים האלה מופיעים לאורך ה-codelab הזה.
ניפוי באגים ובדיקה של הווידג'ט לדוגמה
עכשיו מריצים את האפליקציה ורואים את הווידג'ט של מסך הבית. אחרי שיוצרים את האפליקציה, עוברים למסך בחירת האפליקציה במכשיר Android ולוחצים לחיצה ארוכה על הסמל של פרויקט Flutter. בתפריט הקופץ, בוחרים באפשרות ווידג'טים.

במכשיר Android או באמולטור מוצג הווידג'ט של מסך הבית שמוגדר כברירת מחדל ל-Android.
4. שליחת נתונים מאפליקציית Flutter לווידג'ט במסך הבית
אתם יכולים להתאים אישית את הווידג'ט הבסיסי של מסך הבית שיצרתם. עדכון הווידג'ט של מסך הבית כך שיוצגו בו כותרת וסיכום של כתבת חדשות. בצילום המסך הבא מוצגת דוגמה לווידג'ט של מסך הבית שבו מוצגים כותרת וסיכום.

כדי להעביר נתונים בין האפליקציה לווידג'ט במסך הבית, צריך לכתוב קוד Dart וקוד Native. התהליך הזה מחולק לשלושה חלקים:
- כתיבת קוד Dart באפליקציית Flutter שגם Android וגם iOS יכולות להשתמש בו
- הוספת פונקציונליות מובנית ל-iOS
- הוספת פונקציונליות מקורית של Android
שימוש בקבוצות אפליקציות ל-iOS
כדי לשתף נתונים בין אפליקציית הורה ל-iOS לבין תוסף ווידג'ט, שני היעדים צריכים להיות שייכים לאותה קבוצת אפליקציות. מידע נוסף על קבוצות אפליקציות זמין במאמרי העזרה של Apple בנושא קבוצות אפליקציות.
כדי לעדכן את מזהה החבילה:
ב-Xcode, עוברים להגדרות של היעד. בכרטיסייה Signing & Capabilities (חתימה ויכולות), בודקים שהוגדרו הצוות ומזהה החבילה.
מוסיפים את קבוצת האפליקציות גם ליעד Runner וגם ליעד NewsWidgetExtension ב-Xcode:
בוחרים באפשרות + Capability -> App Groups ומוסיפים קבוצת אפליקציות חדשה. חוזרים על הפעולה גם ליעד Runner (אפליקציית האב) וגם ליעד הווידג'ט.

הוספת קוד Dart
אפליקציות ל-iOS ולאנדרואיד יכולות לשתף נתונים עם אפליקציית Flutter בכמה דרכים שונות.כדי לתקשר עם האפליקציות האלה, צריך להשתמש בחנות המקומית של המכשיר key/value. ב-iOS החנות הזו נקראת UserDefaults, וב-Android היא נקראת SharedPreferences. home_widget package עוטף את ממשקי ה-API האלה כדי לפשט את שמירת הנתונים בכל אחת מהפלטפורמות, ומאפשר לווידג'טים במסך הבית לשלוף נתונים מעודכנים.

הנתונים של הכותרת והתיאור מגיעים מהקובץ news_data.dart. הקובץ הזה מכיל נתונים לדוגמה ומחלקת נתונים NewsArticle.
lib/news_data.dart
class NewsArticle {
final String title;
final String description;
final String? articleText;
NewsArticle({
required this.title,
required this.description,
this.articleText = loremIpsum,
});
}
עדכון הערכים של הכותרת והתיאור
כדי להוסיף את הפונקציונליות לעדכון הווידג'ט במסך הבית מאפליקציית Flutter, עוברים אל הקובץ lib/home_screen.dart. מחליפים את התוכן של הקובץ בקוד הבא. לאחר מכן, מחליפים את <YOUR APP GROUP> במזהה של קבוצת האפליקציות.
lib/home_screen.dart
import 'package:flutter/material.dart';
import 'package:home_widget/home_widget.dart'; // Add this import
import 'article_screen.dart';
import 'news_data.dart';
// TODO: Replace with your App Group ID
const String appGroupId = '<YOUR APP GROUP>'; // Add from here
const String iOSWidgetName = 'NewsWidgets';
const String androidWidgetName = 'NewsWidget'; // To here.
class MyHomePage extends StatefulWidget {
const MyHomePage({super.key});
@override
State<MyHomePage> createState() => _MyHomePageState();
}
void updateHeadline(NewsArticle newHeadline) { // Add from here
// Save the headline data to the widget
HomeWidget.saveWidgetData<String>('headline_title', newHeadline.title);
HomeWidget.saveWidgetData<String>(
'headline_description', newHeadline.description);
HomeWidget.updateWidget(
iOSName: iOSWidgetName,
androidName: androidWidgetName,
);
} // To here.
class _MyHomePageState extends State<MyHomePage> {
@override // Add from here
void initState() {
super.initState();
HomeWidget.setAppGroupId(appGroupId);
// Mock read in some data and update the headline
final newHeadline = getNewsStories()[0];
updateHeadline(newHeadline);
} // To here.
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Top Stories'),
centerTitle: false,
titleTextStyle: const TextStyle(
fontSize: 30,
fontWeight: FontWeight.bold,
color: Colors.black)),
body: ListView.separated(
separatorBuilder: (context, idx) {
return const Divider();
},
itemCount: getNewsStories().length,
itemBuilder: (context, idx) {
final article = getNewsStories()[idx];
return ListTile(
key: Key('$idx ${article.hashCode}'),
title: Text(article.title!),
subtitle: Text(article.description!),
onTap: () {
Navigator.of(context).push(
MaterialPageRoute(
builder: (context) {
return ArticleScreen(article: article);
},
),
);
},
);
},
));
}
}
הפונקציה updateHeadline שומרת את זוגות המפתח/ערך באחסון המקומי של המכשיר. המפתח headline_title מכיל את הערך newHeadline.title. המפתח headline_description מכיל את הערך של newHeadline.description. הפונקציה גם מודיעה לפלטפורמה המקורית שאפשר לאחזר ולעבד נתונים חדשים לווידג'טים במסך הבית.
שינוי של floatingActionButton
קוראים לפונקציה updateHeadline כשלוחצים על floatingActionButton, כמו שמוצג כאן:
lib/article_screen.dart
// New: import the updateHeadline function
import 'home_screen.dart';
...
floatingActionButton: FloatingActionButton.extended(
onPressed: () {
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(
content: Text('Updating home screen widget...'),
));
// New: call updateHeadline
updateHeadline(widget.article);
},
label: const Text('Update Homescreen'),
),
...
בעקבות השינוי הזה, כשמשתמש לוחץ על הלחצן עדכון הכותרת בדף מאמר, הפרטים בווידג'ט של מסך הבית מתעדכנים.
עדכון הקוד של iOS כדי להציג את נתוני המאמר
כדי לעדכן את הווידג'ט של מסך הבית ב-iOS, צריך להשתמש ב-Xcode.
פותחים את הקובץ NewsWidgets.swift ב-Xcode:
מגדירים את TimelineEntry.
מחליפים את המבנה SimpleEntry בקוד הבא:
ios/NewsWidgets/NewsWidgets.swift
// The date and any data you want to pass into your app must conform to TimelineEntry
struct NewsArticleEntry: TimelineEntry {
let date: Date
let title: String
let description:String
}
המבנה NewsArticleEntry הזה מגדיר את הנתונים הנכנסים שיועברו לווידג'ט של מסך הבית כשהוא יתעדכן. הסוג TimelineEntry מחייב פרמטר של תאריך.מידע נוסף על הפרוטוקול TimelineEntry זמין בתיעוד של Apple בנושא TimelineEntry.
עריכה של View שמציג את התוכן
אפשר לשנות את הווידג'ט במסך הבית כך שיוצגו בו הכותרת והתיאור של כתבת החדשות במקום התאריך. כדי להציג טקסט ב-SwiftUI, משתמשים בתצוגה Text. כדי להציג תצוגות אחת על גבי השנייה ב-SwiftUI, משתמשים בתצוגה VStack.
מחליפים את התצוגה NewsWidgetEntryView שנוצרה בקוד הבא:
ios/NewsWidgets/NewsWidgets.swift
//View that holds the contents of the widget
struct NewsWidgetsEntryView : View {
var entry: Provider.Entry
var body: some View {
VStack {
Text(entry.title)
Text(entry.description)
}
}
}
עריכת הספק כדי להגדיר לו מתי ואיך לעדכן את הווידג'ט של מסך הבית
מחליפים את הקוד הקיים Provider בקוד הבא. לאחר מכן, מחליפים את המחרוזת <YOUR APP GROUP> במזהה קבוצת האפליקציות:
ios/NewsWidgets/NewsWidgets.swift
struct Provider: TimelineProvider {
// Placeholder is used as a placeholder when the widget is first displayed
func placeholder(in context: Context) -> NewsArticleEntry {
// Add some placeholder title and description, and get the current date
NewsArticleEntry(date: Date(), title: "Placeholder Title", description: "Placeholder description")
}
// Snapshot entry represents the current time and state
func getSnapshot(in context: Context, completion: @escaping (NewsArticleEntry) -> ()) {
let entry: NewsArticleEntry
if context.isPreview{
entry = placeholder(in: context)
}
else{
// Get the data from the user defaults to display
let userDefaults = UserDefaults(suiteName: <YOUR APP GROUP>)
let title = userDefaults?.string(forKey: "headline_title") ?? "No Title Set"
let description = userDefaults?.string(forKey: "headline_description") ?? "No Description Set"
entry = NewsArticleEntry(date: Date(), title: title, description: description)
}
completion(entry)
}
// getTimeline is called for the current and optionally future times to update the widget
func getTimeline(in context: Context, completion: @escaping (Timeline<Entry>) -> ()) {
// This just uses the snapshot function you defined earlier
getSnapshot(in: context) { (entry) in
// atEnd policy tells widgetkit to request a new entry after the date has passed
let timeline = Timeline(entries: [entry], policy: .atEnd)
completion(timeline)
}
}
}
הקוד Provider בדוגמה הקודמת תואם ל-TimelineProvider. Provider יש שלוש שיטות שונות:
- השיטה של
placeholderיוצרת רשומה של placeholder כשהמשתמש צופה בתצוגה מקדימה של הווידג'ט של מסך הבית בפעם הראשונה.

- השיטה
getSnapshotקוראת את הנתונים מהגדרות ברירת המחדל של המשתמש ויוצרת את הרשומה לשעה הנוכחית. - השיטה
getTimelineמחזירה רשומות בציר הזמן. האפשרות הזו שימושית אם יש לכם נקודות זמן צפויות שבהן אתם רוצים לעדכן את התוכן. ב-Codelab הזה נעשה שימוש בפונקציה getSnapshot כדי לקבל את המצב הנוכחי. השיטה.atEndאומרת לווידג'ט של מסך הבית לרענן את הנתונים אחרי שהשעה הנוכחית חולפת.
מוסיפים הערה ל-NewsWidgets_Previews
השימוש בתצוגות מקדימות הוא מעבר להיקף של ה-codelab הזה. פרטים נוספים על תצוגה מקדימה של ווידג'טים של SwiftUI במסך הבית זמינים במסמכי התיעוד של Apple בנושא ניפוי באגים בווידג'טים.
שומרים את כל הקבצים ומריצים מחדש את האפליקציה ואת יעד הווידג'ט.
מריצים שוב את היעדים כדי לוודא שהאפליקציה והווידג'ט של מסך הבית פועלים.
- בוחרים את סכימת האפליקציה ב-Xcode כדי להפעיל את יעד האפליקציה.
- בוחרים את סכימת התוסף ב-Xcode כדי להריץ את יעד התוסף.
- עוברים לדף מאמר באפליקציה.
- לוחצים על הלחצן כדי לעדכן את הכותרת. הכותרת אמורה להתעדכן גם בווידג'ט של מסך הבית.
עדכון קוד Android
מוסיפים את ה-XML של הווידג'ט למסך הבית.
ב-Android Studio, מעדכנים את הקבצים שנוצרו בשלב הקודם.פותחים את הקובץ res/layout/news_widget.xml. הוא מגדיר את המבנה והפריסה של הווידג'ט במסך הבית. בפינה השמאלית העליונה, בוחרים באפשרות קוד ומחליפים את התוכן של הקובץ בקוד הבא:
android/app/res/layout/news_widget.xml
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/widget_container"
style="@style/Widget.Android.AppWidget.Container"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:background="@android:color/white"
android:theme="@style/Theme.Android.AppWidgetContainer">
<TextView
android:id="@+id/headline_title"
style="@style/Widget.Android.AppWidget.InnerView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentStart="true"
android:layout_alignParentLeft="true"
android:layout_marginStart="8dp"
android:layout_marginLeft="8dp"
android:background="@android:color/white"
android:text="Title"
android:textSize="20sp"
android:textStyle="bold" />
<TextView
android:id="@+id/headline_description"
style="@style/Widget.Android.AppWidget.InnerView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@+id/headline_title"
android:layout_alignParentStart="true"
android:layout_alignParentLeft="true"
android:layout_marginStart="8dp"
android:layout_marginLeft="8dp"
android:layout_marginTop="4dp"
android:background="@android:color/white"
android:text="Title"
android:textSize="16sp" />
</RelativeLayout>
קובץ ה-XML הזה מגדיר שני תצוגות טקסט, אחת לכותרת המאמר ואחת לתיאור המאמר. תצוגות הטקסט האלה מגדירות גם את הסגנון. נחזור לקובץ הזה במהלך ה-Codelab.
עדכון הפונקציונליות של NewsWidget
פותחים את קובץ קוד המקור של Kotlin (.kt).NewsWidget.kt הקובץ הזה מכיל מחלקה שנוצרה בשם NewsWidget, שהיא הרחבה של המחלקה AppWidgetProvider.
המחלקה NewsWidget מכילה שלושה methods ממחלקת האב שלה. תשנו את השיטה onUpdate. מערכת Android קוראת לשיטה הזו לווידג'טים במרווחי זמן קבועים.
מחליפים את התוכן של הקובץ NewsWidget.kt בקוד הבא:
android/app/java/com.mydomain.homescreen_widgets/NewsWidget.kt
// Import will depend on App ID.
package com.mydomain.homescreen_widgets
import android.appwidget.AppWidgetManager
import android.appwidget.AppWidgetProvider
import android.content.Context
import android.widget.RemoteViews
// New import.
import es.antonborri.home_widget.HomeWidgetPlugin
/**
* Implementation of App Widget functionality.
*/
class NewsWidget : AppWidgetProvider() {
override fun onUpdate(
context: Context,
appWidgetManager: AppWidgetManager,
appWidgetIds: IntArray,
) {
for (appWidgetId in appWidgetIds) {
// Get reference to SharedPreferences
val widgetData = HomeWidgetPlugin.getData(context)
val views = RemoteViews(context.packageName, R.layout.news_widget).apply {
val title = widgetData.getString("headline_title", null)
setTextViewText(R.id.headline_title, title ?: "No title set")
val description = widgetData.getString("headline_description", null)
setTextViewText(R.id.headline_description, description ?: "No description set")
}
appWidgetManager.updateAppWidget(appWidgetId, views)
}
}
}
עכשיו, כשמתבצעת קריאה ל-onUpdate, מערכת Android מקבלת את הערכים העדכניים ביותר מהאחסון המקומי באמצעות ה-method the widgetData.getString(), ואז מתבצעת קריאה ל-setTextViewText כדי לשנות את הטקסט שמוצג בווידג'ט של מסך הבית.
בדיקת העדכונים
בודקים את האפליקציה כדי לוודא שהווידג'טים במסך הבית מתעדכנים עם נתונים חדשים. כדי לעדכן את הנתונים, משתמשים באפשרות עדכון מסך הבית FloatingActionButton בדפי המאמרים. הווידג'ט של מסך הבית אמור להתעדכן עם שם המאמר.

5. שימוש בגופנים מותאמים אישית באפליקציית Flutter בווידג'ט של מסך הבית ב-iOS
עד עכשיו הגדרתם את הווידג'ט של מסך הבית לקריאת נתונים שהאפליקציה של Flutter מספקת. אפליקציית Flutter כוללת גופן מותאם אישית שאולי תרצו להשתמש בו בווידג'ט של מסך הבית. אפשר להשתמש בגופן המותאם אישית בווידג'ט של מסך הבית ב-iOS. אי אפשר להשתמש בגופנים בהתאמה אישית בווידג'טים במסך הבית ב-Android.
עדכון הקוד ל-iOS
Flutter מאחסן את הנכסים שלו ב-mainBundle של אפליקציות ל-iOS. אפשר לגשת לנכסים בחבילה הזו מקוד הווידג'ט במסך הבית.
במבנה NewsWidgetsEntryView בקובץ NewsWidgets.swift, מבצעים את השינויים הבאים
יוצרים פונקציית עזר כדי לקבל את הנתיב לספריית הנכסים של Flutter:
ios/NewsWidgets/NewsWidgets.swift
struct NewsWidgetsEntryView : View {
...
// New: Add the helper function.
var bundle: URL {
let bundle = Bundle.main
if bundle.bundleURL.pathExtension == "appex" {
// Peel off two directory levels - MY_APP.app/PlugIns/MY_APP_EXTENSION.appex
var url = bundle.bundleURL.deletingLastPathComponent().deletingLastPathComponent()
url.append(component: "Frameworks/App.framework/flutter_assets")
return url
}
return bundle.bundleURL
}
...
}
רושמים את הגופן באמצעות כתובת ה-URL של קובץ הגופן המותאם אישית.
ios/NewsWidgets/NewsWidgets.swift
struct NewsWidgetsEntryView : View {
...
// New: Register the font.
init(entry: Provider.Entry){
self.entry = entry
CTFontManagerRegisterFontsForURL(bundle.appending(path: "/fonts/Chewy-Regular.ttf") as CFURL, CTFontManagerScope.process, nil)
}
...
}
מעדכנים את הכותרת בתצוגת הטקסט כדי להשתמש בגופן המותאם אישית.
ios/NewsWidgets/NewsWidgets.swift
struct NewsWidgetsEntryView : View {
...
var body: some View {
VStack {
// Update the following line.
Text(entry.title).font(Font.custom("Chewy", size: 13))
Text(entry.description)
}
}
...
}
כשמפעילים את הווידג'ט במסך הבית, הכותרת משתמשת עכשיו בגופן המותאם אישית, כמו שמוצג בתמונה הבאה:

6. עיבוד ווידג'טים של Flutter כתמונה
בקטע הזה נראה איך להציג תרשים מאפליקציית Flutter כווידג'ט במסך הבית.
הווידג'ט הזה מאתגר יותר מהטקסט שמוצג במסך הבית. הרבה יותר קל להציג את התרשים של Flutter כתמונה מאשר לנסות ליצור אותו מחדש באמצעות רכיבים מקוריים של ממשק המשתמש.
כותבים קוד לווידג'ט של מסך הבית כדי לעבד את תרשים Flutter כקובץ PNG. אפשר להציג את התמונה הזו בווידג'ט של מסך הבית.
כתיבת קוד Dart
בצד Dart, מוסיפים את השיטה renderFlutterWidget מהחבילה home_widget. השיטה הזו מקבלת ווידג'ט, שם קובץ ומפתח. הפונקציה מחזירה תמונה של הווידג'ט של Flutter ושומרת אותה בקונטיינר משותף. מזינים את שם התמונה בקוד ומוודאים שלווידג'ט במסך הבית יש גישה לקונטיינר. key שומר את הנתיב המלא של הקובץ כמחרוזת באחסון המקומי של המכשיר. כך הווידג'ט במסך הבית יכול למצוא את הקובץ אם השם משתנה בקוד Dart.
ב-codelab הזה, המחלקה LineChart בקובץ lib/article_screen.dart מייצגת את התרשים. שיטת הבנייה שלו מחזירה CustomPainter שמצייר את התרשים הזה על המסך.
כדי להטמיע את התכונה הזו, פותחים את הקובץ lib/article_screen.dart. מייבאים את חבילת home_widget. לאחר מכן, מחליפים את הקוד בכיתה _ArticleScreenState בקוד הבא:
lib/article_screen.dart
import 'package:flutter/material.dart';
// New: import the home_widget package.
import 'package:home_widget/home_widget.dart';
import 'home_screen.dart';
import 'news_data.dart';
...
class _ArticleScreenState extends State<ArticleScreen> {
// New: add this GlobalKey
final _globalKey = GlobalKey();
String? imagePath;
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.article.title!),
),
// New: add this FloatingActionButton
floatingActionButton: FloatingActionButton.extended(
onPressed: () async {
if (_globalKey.currentContext != null) {
var path = await HomeWidget.renderFlutterWidget(
const LineChart(),
fileName: 'screenshot',
key: 'filename',
logicalSize: _globalKey.currentContext!.size,
pixelRatio:
MediaQuery.of(_globalKey.currentContext!).devicePixelRatio,
);
setState(() {
imagePath = path as String?;
});
}
updateHeadline(widget.article);
},
label: const Text('Update Homescreen'),
),
body: ListView(
padding: const EdgeInsets.all(16.0),
children: [
Text(
widget.article.description!,
style: Theme.of(context).textTheme.titleMedium,
),
const SizedBox(height: 20.0),
Text(widget.article.articleText!),
const SizedBox(height: 20.0),
Center(
// New: Add this key
key: _globalKey,
child: const LineChart(),
),
const SizedBox(height: 20.0),
Text(widget.article.articleText!),
],
),
);
}
}
בדוגמה הזו מתבצעים שלושה שינויים במחלקה _ArticleScreenState.
יצירת GlobalKey
הפונקציה GlobalKey מקבלת את ההקשר של הווידג'ט הספציפי, שנדרש כדי לקבל את הגודל של הווידג'ט הזה .
lib/article_screen.dart
class _ArticleScreenState extends State<ArticleScreen> {
// New: add this GlobalKey
final _globalKey = GlobalKey();
...
}
הוספת imagePath
המאפיין imagePath מאחסן את המיקום של התמונה שבה הווידג'ט של Flutter מעובד.
lib/article_screen.dart
class _ArticleScreenState extends State<ArticleScreen> {
...
// New: add this imagePath
String? imagePath;
...
}
הוספת המפתח לווידג'ט לצורך עיבוד
התג _globalKey מכיל את הווידג'ט של Flutter שמעובד לתמונה. במקרה הזה, הווידג'ט של Flutter הוא Center שמכיל את LineChart.
lib/article_screen.dart
class _ArticleScreenState extends State<ArticleScreen> {
...
Center(
// New: Add this key
key: _globalKey,
child: const LineChart(),
),
...
}
- שמירת הווידג'ט כתמונה
השיטה renderFlutterWidget מופעלת כשהמשתמש לוחץ על floatingActionButton. השיטה שומרת את קובץ ה-PNG שנוצר כ-screenshot בספריית המאגר המשותפת. בנוסף, השיטה שומרת את הנתיב המלא לתמונה כמפתח שם הקובץ באחסון המכשיר.
lib/article_screen.dart
class _ArticleScreenState extends State<ArticleScreen> {
...
floatingActionButton: FloatingActionButton.extended(
onPressed: () async {
if (_globalKey.currentContext != null) {
var path = await HomeWidget.renderFlutterWidget(
LineChart(),
fileName: 'screenshot',
key: 'filename',
logicalSize: _globalKey.currentContext!.size,
pixelRatio:
MediaQuery.of(_globalKey.currentContext!).devicePixelRatio,
);
setState(() {
imagePath = path as String?;
});
}
updateHeadline(widget.article);
},
...
}
עדכון הקוד ב-iOS
ב-iOS, מעדכנים את הקוד כדי לקבל את נתיב הקובץ מהאחסון ולהציג את הקובץ כתמונה באמצעות SwiftUI.
פותחים את הקובץ NewsWidgets.swift ומבצעים את השינויים הבאים:
הוספה של filename ושל displaySize אל NewsArticleEntry struct
המאפיין filename מכיל את המחרוזת שמייצגת את הנתיב לקובץ התמונה. המאפיין displaySize מכיל את הגודל של הווידג'ט במסך הבית במכשיר של המשתמש. הגודל של הווידג'ט במסך הבית נקבע על ידי context.
ios/NewsWidgets/NewsWidgets.swift
struct NewsArticleEntry: TimelineEntry {
...
// New: add the filename and displaySize.
let filename: String
let displaySize: CGSize
}
עדכון הפונקציה של placeholder
כולל פלייסהולדרים filename ו-displaySize.
ios/NewsWidgets/NewsWidgets.swift
func placeholder(in context: Context) -> NewsArticleEntry {
NewsArticleEntry(date: Date(), title: "Placeholder Title", description: "Placeholder description", filename: "No screenshot available", displaySize: context.displaySize)
}
קבלת שם הקובץ מ-userDefaults ב-getSnapshot
הפעולה הזו מגדירה את המשתנה filename לערך filename באחסון userDefaults כשהווידג'ט של מסך הבית מתעדכן.
ios/NewsWidgets/NewsWidgets.swift
func getSnapshot(
...
let title = userDefaults?.string(forKey: "headline_title") ?? "No Title Set"
let description = userDefaults?.string(forKey: "headline_description") ?? "No Description Set"
// New: get fileName from key/value store
let filename = userDefaults?.string(forKey: "filename") ?? "No screenshot available"
...
)
יצירת ChartImage שמציג את התמונה מנתיב
התצוגה ChartImage יוצרת תמונה מתוכן הקובץ שנוצר בצד Dart. כאן מגדירים את הגודל ל-50% מהפריים.
ios/NewsWidgets/NewsWidgets.swift
struct NewsWidgetsEntryView : View {
...
// New: create the ChartImage view
var ChartImage: some View {
if let uiImage = UIImage(contentsOfFile: entry.filename) {
let image = Image(uiImage: uiImage)
.resizable()
.frame(width: entry.displaySize.height*0.5, height: entry.displaySize.height*0.5, alignment: .center)
return AnyView(image)
}
print("The image file could not be loaded")
return AnyView(EmptyView())
}
...
}
שימוש ב-ChartImage בגוף של NewsWidgetsEntryView
מוסיפים את התצוגה ChartImage לגוף של NewsWidgetsEntryView כדי להציג את ChartImage בווידג'ט של מסך הבית.
ios/NewsWidgets/NewsWidgets.swift
VStack {
Text(entry.title).font(Font.custom("Chewy", size: 13))
Text(entry.description).font(.system(size: 12)).padding(10)
// New: add the ChartImage to the NewsWidgetEntryView
ChartImage
}
בדיקת השינויים
כדי לבדוק את השינויים, מריצים מחדש את יעד האפליקציה (Runner) ב-Flutter ואת יעד התוסף מ-Xcode. כדי לראות את התמונה, עוברים לאחד מדפי המאמרים באפליקציה ולוחצים על הכפתור לעדכון הווידג'ט במסך הבית.

עדכון הקוד ל-Android
הקוד ל-Android פועל כמו הקוד ל-iOS.
- פותחים את הקובץ
android/app/res/layout/news_widget.xml. הוא מכיל את רכיבי ממשק המשתמש של הווידג'ט במסך הבית. מחליפים את התוכן שלו בקוד הבא:
android/app/res/layout/news_widget.xml
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/widget_container"
style="@style/Widget.Android.AppWidget.Container"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:background="@android:color/white"
android:theme="@style/Theme.Android.AppWidgetContainer">
<TextView
android:id="@+id/headline_title"
style="@style/Widget.Android.AppWidget.InnerView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentStart="true"
android:layout_alignParentLeft="true"
android:layout_marginStart="8dp"
android:layout_marginLeft="8dp"
android:background="@android:color/white"
android:text="Title"
android:textSize="20sp"
android:textStyle="bold" />
<TextView
android:id="@+id/headline_description"
style="@style/Widget.Android.AppWidget.InnerView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@+id/headline_title"
android:layout_alignParentStart="true"
android:layout_alignParentLeft="true"
android:layout_marginStart="8dp"
android:layout_marginLeft="8dp"
android:layout_marginTop="4dp"
android:background="@android:color/white"
android:text="Title"
android:textSize="16sp" />
<!--New: add this image view -->
<ImageView
android:id="@+id/widget_image"
android:layout_width="200dp"
android:layout_height="200dp"
android:layout_below="@+id/headline_description"
android:layout_alignBottom="@+id/headline_title"
android:layout_alignParentStart="true"
android:layout_alignParentLeft="true"
android:layout_marginStart="8dp"
android:layout_marginLeft="8dp"
android:layout_marginTop="6dp"
android:layout_marginBottom="-134dp"
android:layout_weight="1"
android:adjustViewBounds="true"
android:background="@android:color/white"
android:scaleType="fitCenter"
android:src="@android:drawable/star_big_on"
android:visibility="visible"
tools:visibility="visible" />
</RelativeLayout>
הקוד החדש הזה מוסיף תמונה לווידג'ט של מסך הבית, שמוצג בו (לעת עתה) סמל כוכב כללי. מחליפים את סמל הכוכב בתמונה ששמרתם בקוד Dart.
- פותחים את הקובץ
NewsWidget.kt. מחליפים את התוכן שלו בקוד הבא:
android/app/java/com.mydomain.homescreen_widgets/NewsWidget.kt
// Import will depend on App ID.
package com.mydomain.homescreen_widgets
import android.appwidget.AppWidgetManager
import android.appwidget.AppWidgetProvider
import android.content.Context
import android.graphics.Bitmap
import android.graphics.BitmapFactory
import android.widget.RemoteViews
import java.io.File
import es.antonborri.home_widget.HomeWidgetPlugin
/**
* Implementation of App Widget functionality.
*/
class NewsWidget : AppWidgetProvider() {
override fun onUpdate(
context: Context,
appWidgetManager: AppWidgetManager,
appWidgetIds: IntArray,
) {
for (appWidgetId in appWidgetIds) {
val widgetData = HomeWidgetPlugin.getData(context)
val views = RemoteViews(context.packageName, R.layout.news_widget).apply {
val title = widgetData.getString("headline_title", null)
setTextViewText(R.id.headline_title, title ?: "No title set")
val description = widgetData.getString("headline_description", null)
setTextViewText(R.id.headline_description, description ?: "No description set")
// New: Add the section below
// Get chart image and put it in the widget, if it exists
val imageName = widgetData.getString("filename", null)
val imageFile = File(imageName)
val imageExists = imageFile.exists()
if (imageExists) {
val myBitmap: Bitmap = BitmapFactory.decodeFile(imageFile.absolutePath)
setImageViewBitmap(R.id.widget_image, myBitmap)
} else {
println("image not found!, looked @: ${imageName}")
}
// End new code
}
appWidgetManager.updateAppWidget(appWidgetId, views)
}
}
}
קוד ה-Dart הזה שומר צילום מסך באחסון המקומי עם המפתח filename. הוא גם מקבל את הנתיב המלא של התמונה ויוצר ממנו אובייקט File. אם התמונה קיימת, קוד ה-Dart מחליף את התמונה בווידג'ט של מסך הבית בתמונה החדשה.
- טוענים מחדש את האפליקציה ועוברים למסך של מאמר. לוחצים על עדכון מסך הבית. התרשים מוצג בווידג'ט של מסך הבית.
7. השלבים הבאים
מעולה!
הצלחתם ליצור ווידג'טים למסך הבית באפליקציות Flutter ל-iOS ול-Android.
קישור לתוכן באפליקציית Flutter
יכול להיות שתרצו להפנות את המשתמש לדף ספציפי באפליקציה, בהתאם למקום שבו המשתמש לוחץ. לדוגמה, באפליקציית החדשות מ-Codelab זה, יכול להיות שתרצו שהמשתמש יראה את הכתבה שמתאימה לכותרת שמוצגת.
התכונה הזו לא נכללת ב-codelab הזה. אפשר למצוא דוגמאות לשימוש בstream שחבילת home_widget מספקת כדי לזהות הפעלות של אפליקציות מווידג'טים במסך הבית ולשלוח הודעות מהווידג'ט במסך הבית דרך כתובת ה-URL. מידע נוסף זמין במאמרי העזרה בנושא קישורי עומק באתר docs.flutter.dev.
עדכון הווידג'ט ברקע
ב-codelab הזה הפעלתם עדכון של הווידג'ט של מסך הבית באמצעות לחצן. אמנם זה הגיוני לבדיקות, אבל בקוד של מוצר פעיל יכול להיות שתרצו שהאפליקציה תעדכן את הווידג'ט במסך הבית ברקע. אתם יכולים להשתמש בתוסף workmanager כדי ליצור משימות ברקע לעדכון משאבים שהווידג'ט של מסך הבית צריך. מידע נוסף זמין בקטע Background update בחבילה home_widget.
ב-iOS, אפשר גם להגדיר שהווידג'ט של מסך הבית ישלח בקשה לרשת כדי לעדכן את ממשק המשתמש שלו. כדי לשלוט בתנאים או בתדירות של הבקשה הזו, משתמשים בציר הזמן. מידע נוסף על השימוש בציר הזמן זמין במאמר 'שמירה על עדכניות הווידג'ט' של Apple .