הוספת ווידג'ט למסך הבית לאפליקציית Flutter

1. מבוא

מהם ווידג'טים?

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

f0027e8a7d0237e0.png b991e79ea72c8b65.png

עד כמה ווידג'ט יכול להיות מורכב?

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

819b9fffd700e571.png 92d62ccfd17d770d.png

יצירת ממשק המשתמש לווידג'טים

בגלל המגבלות האלה בממשק המשתמש, אי אפשר לשרטט ישירות את ממשק המשתמש של ווידג'ט במסך הבית באמצעות מסגרת Flutter. במקום זאת, אפשר להוסיף לאפליקציית Flutter ווידג'טים שנוצרו באמצעות frameworks של פלטפורמה, כמו 'Jetpack פיתוח נייטיב' או SwiftUI. ב-Codelab הזה מתוארים דוגמאות לשיתוף משאבים בין האפליקציה שלכם לבין הווידג'טים כדי להימנע משכתוב של ממשק משתמש מורכב.

מה תפַתחו

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

  • הצגת נתונים מאפליקציית Flutter.
  • הצגת טקסט באמצעות נכסי גופנים ששותפו מאפליקציית Flutter.
  • הצגת תמונה של ווידג'ט Flutter מעובד.

a36b7ba379151101.png

אפליקציית Flutter הזו כוללת שני מסכים (או מסלולים):

  • בעמודה הראשונה מוצגת רשימה של כתבות חדשותיות עם כותרות ותיאורים.
  • השנייה מציגה את הכתבה המלאה עם תרשים שנוצר באמצעות CustomPaint.

.

9c02f8b62c1faa3a.png d97d44051304cae4.png

מה תלמדו

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

2. הגדרת סביבת הפיתוח

בשתי הפלטפורמות צריך את Flutter SDK ואת סביבת הפיתוח המשולבת (IDE). אפשר להשתמש בסביבת הפיתוח המשולבת המועדפת עליך לעבודה עם Flutter. הוא יכול להיות Visual Studio Code עם התוספים Dart Code ו-Flutter, או Android Studio או IntelliJ שבהם מותקנים יישומי הפלאגיןFlutter ו-Dart.

כדי ליצור את הווידג'ט של מסך הבית ב-iOS:

  • אפשר להריץ את Codelab זה במכשיר iOS פיזי או בסימולטור iOS.
  • צריך להגדיר מערכת macOS עם Xcode IDE. הפעולה הזו תתקין את המהדר שנדרש כדי לבנות את גרסת 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:

  1. מריצים את הפקודה open ios/Runner.xcworkspace בחלון טרמינל מספריית הפרויקט של Flutter. לחלופין, לוחצים לחיצה ימנית על תיקיית ios מ-VSCode ובוחרים באפשרות פתיחה ב-Xcode. פעולה זו תפתח את סביבת העבודה המוגדרת כברירת מחדל ב-Xcode בפרויקט Flutter.
  2. בתפריט, בוחרים קובץ ← חדש ← יעד. הפעולה הזו מוסיפה יעד חדש לפרויקט.
  3. תופיע רשימה של תבניות. בוחרים באפשרות Widget Extension (תוסף ווידג'ט).
  4. מקלידים "NewsWidgets" בתיבה שם המוצר של הווידג'ט הזה. מבטלים את הסימון של התיבות Include Live Activity ו-Include Configuration Intent (הכללה של פעילות בזמן אמת).

בדיקת הקוד לדוגמה

כשמוסיפים יעד חדש, Xcode יוצר קוד לדוגמה על סמך התבנית שבחרתם. מידע נוסף על הקוד שנוצר ועל WidgetKit זמין במסמכי התיעוד של Apple בנושא תוספי אפליקציות.

ניפוי באגים ובדיקה של הווידג'ט לדוגמה

  1. קודם כול, מעדכנים את ההגדרה של אפליקציית Flutter. עליכם לעשות זאת כשאתם מוסיפים חבילות חדשות לאפליקציית Flutter ומתכננים להריץ יעד בפרויקט מ-Xcode. כדי לעדכן את תצורת האפליקציה, מריצים את הפקודה הבאה בספריית האפליקציות Flutter:
$ flutter build ios --config-only
  1. לוחצים על Runner כדי להציג רשימה של יעדים. בוחרים את הווידג'ט החדש שיצרתם, NewsWidgets, ולוחצים על Run. מריצים את היעד של הווידג'ט מ-Xcode כשמשנים את הקוד של הווידג'ט ל-iOS.

bbb519df1782881d.png

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

18eff1cae152014d.png

  1. מחפשים את שם האפליקציה. ל-codelab הזה, צריך לחפש 'ווידג'טים של מסך הבית'

a0c00df87615493e.png

  1. אחרי שמוסיפים את הווידג'ט של מסך הבית, הוא אמור להציג טקסט פשוט שעוזר להגדיר את משך הזמן.

יצירת ווידג'ט בסיסי ל-Android

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

f19d8b7f95ab884e.png

  1. ב-Android Studio מוצג טופס חדש. הוספת מידע בסיסי על הווידג'ט של מסך הבית, כולל שם הכיתה, המיקום, הגודל ושפת המקור שלו

עבור ה-Codelab הזה, מגדירים את הערכים הבאים:

  • התיבה Class Name (שם הכיתה) ל-NewsWidget
  • התפריט הנפתח רוחב מינימלי (תאים) ל-3
  • התפריט הנפתח גובה מינימלי (תאים) ל-3

בדיקת הקוד לדוגמה

כששולחים את הטופס, מערכת Android Studio יוצרת ומעדכנת מספר קבצים. השינויים שרלוונטיים ל-Codelab הזה מפורטים בטבלה הבאה

פעולה

קובץ היעד

שינוי

עדכון

AndroidManifest.xml

הוספת מקלט חדש שרושם את NewsWidget.

יצירה

res/layout/news_widget.xml

מגדיר את ממשק המשתמש של הווידג'ט במסך הבית.

יצירה

res/xml/news_widget_info.xml

מגדיר את תצורת הווידג'ט של מסך הבית. אפשר לשנות את המימדים או את השם של הווידג'ט בקובץ הזה.

יצירה

java/com/example/homescreen_widgets/NewsWidget.kt

מכיל את קוד Kotlin כדי להוסיף פונקציונליות לווידג'ט של מסך הבית.

ב-Codelab הזה אפשר למצוא פרטים נוספים על הקבצים האלה.

ניפוי באגים ובדיקה של הווידג'ט לדוגמה

עכשיו, מריצים את האפליקציה ובודקים את הווידג'ט במסך הבית. לאחר בניית האפליקציה, מנווטים למסך בחירת האפליקציה במכשיר ה-Android ולוחצים לחיצה ארוכה על הסמל של פרויקט Flutter הזה. בתפריט הקופץ, בוחרים באפשרות Widgets [ווידג'טים].

dff7c9f9f85ef1c7.png

במכשיר Android או באמולטור יוצג ווידג'ט ברירת המחדל של מסך הבית ל-Android.

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

אתם יכולים להתאים אישית את הווידג'ט הבסיסי שיצרתם במסך הבית. מעדכנים את הווידג'ט של מסך הבית כדי להציג כותרת וסיכום לכתבה חדשותית. בצילום המסך הבא מוצגת דוגמה לווידג'ט של מסך הבית שמציג כותרת וסיכום.

acb90343a3e51b6d.png

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

  1. כתיבת קוד Dart באפליקציית Flutter, שאפשר להשתמש בו גם ב-Android וגם ב-iOS
  2. הוספת פונקציונליות מקורית של iOS
  3. הוספת פונקציונליות מקורית של Android

באמצעות קבוצות של אפליקציות ל-iOS

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

מעדכנים את מזהה החבילה:

ב-Xcode, עוברים להגדרות היעד. בקטע Signing & יכולות, מוודאים שהצוות ומזהה החבילה מוגדרים.

מוסיפים את קבוצת האפליקציות גם ליעד Runner וגם ליעד NewsWidgetExtension ב-Xcode:

בוחרים באפשרות + יכולת -> קבוצות של אפליקציות ומוסיפים קבוצת אפליקציות חדשה. חוזרים על הפעולה גם ליעד 'ריצה' (אפליקציית הורה) וגם ליעד הווידג'ט.

135e1a8c4652dac.png

מוסיפים את הקוד של Dart

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

707ae86f6650ac55.png

נתוני הכותרות והתיאורים מגיעים מהקובץ 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. הפונקציה גם מודיעה לפלטפורמה המקורית על כך שניתן לאחזר ולעבד נתונים חדשים של הווידג'טים של מסך הבית.

שינוי הלחצן הצף (floatAction)

קוראים לפונקציה 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 זמין במסמכי התיעוד של TimesEntry של Apple.

עריכת 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 יש שלוש שיטות שונות:

  1. השיטה placeholder יוצרת רשומת placeholder בפעם הראשונה שהמשתמש מציג תצוגה מקדימה של הווידג'ט במסך הבית.

45a0f64240c12efe.png

  1. השיטה getSnapshot קוראת את הנתונים מברירות המחדל של המשתמש ויוצרת את הרשומה לזמן הנוכחי.
  2. השיטה getTimeline מחזירה רשומות בציר הזמן. האפשרות הזו עוזרת כשיהיו לכם נקודות זמן צפויות לעדכון התוכן. ה-Codelab הזה משתמש בפונקציה getSnapshot כדי לקבל את המצב הנוכחי. השיטה .atEnd מנחה את הווידג'ט של מסך הבית לרענן את הנתונים אחרי שהזמן הנוכחי יחלוף.

הוספת תגובה לNewsWidgets_Previews

ב-Codelab הזה אין אפשרות להשתמש בתצוגות מקדימות. פרטים נוספים על תצוגה מקדימה של הווידג'טים של SwiftUI במסך הבית זמינים במסמכי התיעוד של Apple בנושא ווידג'טים של ניפוי באגים.

שומרים את כל הקבצים ומפעילים מחדש את יעד האפליקציה והווידג'ט.

מריצים שוב את היעדים כדי לוודא שהאפליקציה והווידג'ט במסך הבית פועלים.

  1. צריך לבחור את סכימת האפליקציה ב-Xcode כדי להריץ את היעד של האפליקציה.
  2. צריך לבחור את סכימת התוספים ב-Xcode כדי להריץ את יעד התוסף.
  3. עוברים אל דף מאמר באפליקציה.
  4. לוחצים על הלחצן כדי לעדכן את הכותרת. גם הווידג'ט של מסך הבית אמור לעדכן את הכותרת.

עדכון הקוד של Android

מוסיפים את ה-XML של הווידג'ט למסך הבית.

ב-Android Studio, מעדכנים את הקבצים שנוצרו בשלב הקודם.פותחים את הקובץ res/layout/news_widget.xml. הוא מגדיר את המבנה והפריסה של הווידג'ט של מסך הבית. בוחרים באפשרות Code (קוד) בפינה הימנית העליונה ומחליפים את תוכן הקובץ בקוד הבא:

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 NewsWidget.kt. הקובץ מכיל מחלקה שנוצרה בשם NewsWidget ומרחיבים את המחלקה AppWidgetProvider.

המחלקה NewsWidget מכילה שלוש שיטות מסיווג-העל שלה. הפעולה הזו תגרום לשינוי של השיטה 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 מקבלת את הערכים החדשים ביותר מהאחסון המקומי באמצעות השיטה the widgetData.getString(), ואז שולחת קריאה אל setTextViewText כדי לשנות את הטקסט שמוצג בווידג'ט של מסך הבית.

בדיקת העדכונים

מומלץ לבדוק את האפליקציה כדי לוודא שהווידג'טים של מסך הבית מתעדכנים בנתונים חדשים. כדי לעדכן את הנתונים, משתמשים באפשרות עדכון מסך הבית FloatingActionButton בדפי המאמרים. הווידג'ט של מסך הבית אמור להתעדכן עם כותרת הכתבה.

5ce1c9914b43ad79.png

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)
    }
   }
   ...
}

כשמפעילים את הווידג'ט במסך הבית, הוא משתמש עכשיו בגופן המותאם אישית לכותרת, כפי שמוצג בתמונה הבאה:

93f8b9d767aacfb2.png

6. עיבוד ווידג'טים של Flutter כתמונה

בקטע הזה, יוצג גרף מאפליקציית Flutter כווידג'ט במסך הבית.

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

לקודד את הווידג'ט של מסך הבית כדי לעבד את תרשים Flutter כקובץ PNG. בווידג'ט של מסך הבית אפשר להציג את התמונה.

כותבים את הקוד של Dart

בצד של החץ, מוסיפים את השיטה renderFlutterWidget מחבילת home_widget. השיטה הזו לוקחת את הווידג'ט, שם הקובץ ומפתח. היא מחזירה תמונה של הווידג'ט Flutter ושומרת אותו בקונטיינר משותף. מזינים את שם התמונה בקוד ומוודאים שהווידג'ט של מסך הבית יכול לגשת למאגר. הקובץ key שומר את הנתיב המלא של הקובץ כמחרוזת באחסון המקומי של המכשיר. הפעולה הזו מאפשרת לווידג'ט של מסך הבית למצוא את הקובץ אם השם משתנה בקוד של Dart.

ב-Codelab הזה, המחלקה LineChart בקובץ lib/article_screen.dart מייצגת את התרשים. שיטת ה-build שלה מחזירה '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!),
        ],
      ),
    );
  }
}

בדוגמה הזו בוצעו 3 שינויים במחלקה _ArticleScreenState.

יצירה של מפתח גלובלי

הרכיב 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 הוא המרכז שמכיל את LineChart.

lib/article_screen.dart

class _ArticleScreenState extends State<ArticleScreen> {
   ...
   Center(
      // New: Add this key
 key: _globalKey,
 child: const LineChart(),
   ),
   ...
}
  1. שמירת הווידג'ט כתמונה

מתבצעת קריאה לשיטה renderFlutterWidget כשהמשתמש לוחץ על floatingActionButton. השיטה שומרת את קובץ ה-PNG שמתקבל כ'צילום מסך' לספריית המאגר המשותפת. השיטה גם שומרת את הנתיב המלא לתמונה כמפתח שם הקובץ באחסון של המכשיר.

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

המאפיין filename מכיל את המחרוזת שמייצגת את הנתיב לקובץ התמונה. המאפיין displaySize כולל את גודל הווידג'ט של מסך הבית במכשיר של המשתמש. גודל הווידג'ט של מסך הבית מגיע מ-context.

ios/NewsWidgets/NewsWidgets.swift

struct NewsArticleEntry: TimelineEntry {
   ...

   // New: add the filename and displaySize.
   let filename: String
   let displaySize: CGSize
}

עדכון הפונקציה של placeholder

לכלול ערכי 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 נוצרת תמונה מהתוכן של הקובץ שנוצר בצד החץ. כאן מגדירים את הגודל ל-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 בווידג'ט של מסך הבית, מוסיפים את התצוגה ChartImage לגוף של NewsWidgetsEntryView.

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
}

בדיקת השינויים

כדי לבדוק את השינויים, צריך להפעיל מחדש את היעד של אפליקציית Flutter (Runner) ואת יעד התוסף מ-Xcode. כדי לראות את התמונה, מנווטים לאחד מדפי הכתבה באפליקציה ולוחצים על הלחצן כדי לעדכן את הווידג'ט של מסך הבית.

33bdfe2cce908c48.png

עדכון הקוד של Android

הקוד של Android פועל כמו קוד iOS.

  1. פותחים את הקובץ 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>

הקוד החדש מוסיף תמונה לווידג'ט של מסך הבית, שמציג (נכון לעכשיו) סמל כוכב גנרי. מחליפים את סמל הכוכב הזה בתמונה ששמרתם בקוד Gart.

  1. פותחים את הקובץ 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 מחליף את התמונה בווידג'ט של מסך הבית בתמונה החדשה.

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

7. השלבים הבאים

מעולה!

מזל טוב, הצלחת ליצור ווידג'טים של מסך הבית לאפליקציות Flutter ל-iOS ול-Android!

קישור לתוכן באפליקציית Flutter

ייתכן שתרצו להפנות את המשתמשים לדף ספציפי באפליקציה, בהתאם למיקום שבו המשתמש לוחץ. לדוגמה, באפליקציית החדשות מ-Codelab זה, ייתכן שתרצו שהמשתמש יראה את הכתבה עבור הכותרת המוצגת.

התכונה הזו לא נכללת בהיקף של Codelab זה. תוכלו למצוא דוגמאות לשימוש בזרם שחבילת home_widget מספקת כדי לזהות הפעלות של אפליקציה מווידג'טים של מסך הבית ולשלוח הודעות מהווידג'ט של מסך הבית דרך כתובת ה-URL. מידע נוסף זמין במסמכי התיעוד בנושא קישורי עומק בכתובת docs.flutter.dev.

עדכון הווידג'ט ברקע

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

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

קריאה נוספת