إضافة تطبيق مصغّر إلى "الشاشة الرئيسية" إلى تطبيق Flutter

1. مقدمة

ما هي "التطبيقات المصغّرة"؟

بالنسبة إلى مطوّري برامج Flutter، يشير التعريف الشائع للأدوات إلى مكوّنات واجهة المستخدم التي تم إنشاؤها باستخدام إطار عمل Flutter. في سياق هذا الدرس التطبيقي حول الترميز، يشير التطبيق المصغّر إلى إصدار مصغّر من تطبيق يوفّر عرضًا لمعلومات التطبيق بدون فتح التطبيق. على أجهزة Android، تظهر التطبيقات المصغّرة على الشاشة الرئيسية. وعلى أجهزة iOS، يمكن إضافتها إلى الشاشة الرئيسية أو شاشة القفل أو عرض اليوم.

f0027e8a7d0237e0.png b991e79ea72c8b65.png

إلى أي مدى يمكن أن يكون التطبيق المصغّر معقّدًا؟

تتميز معظم أدوات الشاشة الرئيسية بالبساطة. وقد تحتوي على نص أساسي أو رسومات بسيطة أو عناصر تحكُّم أساسية على Android. يحدّ كل من نظامي التشغيل Android وiOS من مكونات وميزات واجهة المستخدم التي يمكنك استخدامها.

819b9fffd700e571.png 92d62ccfd17d770d.png

إنشاء واجهة مستخدم "التطبيقات المصغّرة"

بسبب قيود واجهة المستخدم هذه، لا يمكنك رسم واجهة المستخدم مباشرةً لتطبيق مصغّر على الشاشة الرئيسية باستخدام إطار عمل Flutter. بدلاً من ذلك، يمكنك إضافة تطبيقات مصغّرة تم إنشاؤها باستخدام أُطر عمل الأنظمة الأساسية، مثل Jetpack Compose أو SwiftUI، إلى تطبيق Flutter. يناقش هذا الدرس التطبيقي حول الترميز أمثلة لمشاركة الموارد بين تطبيقك والتطبيقات المصغّرة لتجنُّب إعادة كتابة واجهة المستخدم المعقّدة.

ما الذي ستنشئه

في هذا الدرس التطبيقي، ستعمل على إنشاء تطبيقات مصغّرة من "الشاشة الرئيسية" على كلّ من نظامَي التشغيل Android وiOS لتطبيق Flutter البسيط، وذلك باستخدام حزمة home_{@et التي تتيح للمستخدمين قراءة المقالات. ستنفّذ التطبيقات المصغّرة ما يلي:

  • عرض البيانات من تطبيق Flutter
  • عرض النص باستخدام مواد عرض الخطوط التي تمّت مشاركتها من تطبيق Flutter
  • يمكنك عرض صورة لتطبيق Flutter المصغّر المعروض.

a36b7ba379151101.png

يتضمّن تطبيق Flutter هذا شاشتَين (أو مسارات):

  • يعرض الأول قائمة بالمقالات الإخبارية مع عناوين وأوصاف.
  • ويعرض العنصر الثاني المقالة الكاملة من خلال رسم بياني تم إنشاؤه باستخدام السمة CustomPaint.

.

9c02f8b62c1faa3a.png d97d44051304cae4.png

المعلومات التي ستتعلّمها

  • كيفية إنشاء تطبيقات مصغّرة للشاشة الرئيسية على أجهزة iOS وAndroid
  • كيفية استخدام حزمة home_Widgetet لمشاركة البيانات بين تطبيق الشاشة الرئيسية وتطبيق Flutter
  • كيفية تقليل مقدار التعليمات البرمجية التي تحتاج إلى إعادة كتابتها.
  • كيفية تحديث تطبيق "الشاشة الرئيسية" المصغّر من تطبيق Flutter

2. إعداد بيئة التطوير

بالنسبة إلى كِلا النظامَين الأساسيَين، تحتاج إلى Flutter SDK وبيئة تطوير متكاملة (IDE). يمكنك استخدام بيئة تطوير البرامج (IDE) المفضّلة لديك للعمل مع Flutter. يمكن أن يكون هذا الرمز Visual Studio Code مع الإضافتَينDart Code وFlutter، أو "استوديو Android" أو IntelliJ مع تثبيت المكوّنَين الإضافيَين Flutter وDart.

لإنشاء تطبيق iOS المصغّر في الشاشة الرئيسية:

  • يمكنك تشغيل هذا الدرس التطبيقي حول الترميز على جهاز iOS فعلي أو محاكي iOS.
  • يجب ضبط نظام التشغيل macOS باستخدام Xcode IDE. يؤدي هذا إلى تثبيت برنامج التجميع اللازم لإنشاء إصدار iOS من تطبيقك.

لإنشاء تطبيق Android المصغّر في الشاشة الرئيسية:

  • يمكنك تشغيل هذا الدرس التطبيقي حول الترميز على جهاز Android أو محاكي Android.
  • يجب ضبط نظام التطوير باستخدام "استوديو Android". يؤدي هذا إلى تثبيت برنامج التجميع اللازم لإنشاء إصدار Android من تطبيقك.

الحصول على رمز إجراء التفعيل

تنزيل النسخة الأولية من مشروعك من GitHub

من سطر الأوامر، استنسِخ مستودع GitHub في دليل Flutter-codelabs:

$ git clone https://github.com/flutter/codelabs.git flutter-codelabs

بعد استنساخ المستودع، يمكنك العثور على رمز الدرس التطبيقي حول الترميز في الدليل flutter-codelabs/homescreen_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. ستظهر قائمة بالنماذج. اختَر إضافة الأداة.
  4. اكتب "NewsWidgets" في مربّع اسم المنتج لهذه الأداة امسح كلاً من مربعي الاختيار تضمين النشاط المباشر وتضمين نية الإعداد.

فحص نموذج الرمز

عند إضافة هدف جديد، ينشئ Xcode نموذج رمز استنادًا إلى النموذج الذي اخترته. للمزيد من المعلومات عن الرمز الذي تم إنشاؤه وWidgetKit، يمكنك الاطّلاع على مستندات إضافة تطبيق Apple.

تصحيح الأخطاء واختبار نموذج التطبيق المصغّر

  1. عليك أولاً تعديل إعدادات تطبيق Flutter عليك إجراء ذلك عند إضافة حِزم جديدة في تطبيق Flutter والتخطيط لتنفيذ هدف في المشروع من Xcode. لتعديل إعدادات تطبيقك، شغِّل الأمر التالي في دليل تطبيق Flutter:
$ flutter build ios --config-only
  1. انقر على Runner لعرض قائمة بالأهداف. اختَر استهداف الأداة الذي أنشأته للتوّ، وهو NewsWidgets، وانقر على تشغيل. شغِّل هدف الأداة من Xcode عند تغيير رمز التطبيق المصغّر لنظام التشغيل iOS.

bbb519df1782881d.png

  1. من المفترض أن يعرض المحاكي أو شاشة الجهاز التطبيق المصغّر الأساسي للشاشة الرئيسية. وإذا لم يظهر لك، يمكنك إضافته إلى الشاشة. انقر مع الاستمرار على الشاشة الرئيسية، ثم انقر على + في أعلى يمين الشاشة.

18eff1cae152014d.png

  1. ابحث عن اسم التطبيق. ابحث عن "التطبيقات المصغّرة للشاشة الرئيسية" في هذا الدرس التطبيقي.

a0c00df87615493e.png

  1. بعد إضافة تطبيق الشاشة الرئيسية المصغّر، من المفترض أن يعرض نصًا بسيطًا يوضّح الوقت.

إنشاء تطبيق Android المصغّر الأساسي

  1. لإضافة تطبيق مصغّر إلى الشاشة الرئيسية في نظام Android، افتح ملف إصدار المشروع في "استوديو Android". يمكنك العثور على هذا الملف على الرابط android/build.gradle. يمكنك بدلاً من ذلك النقر بزر الماوس الأيمن على مجلد android من VSCode واختيار فتح في "استوديو Android".
  2. بعد إنشاء المشروع، حدِّد موقع دليل التطبيق في أعلى يمين الشاشة. أضِف تطبيق الشاشة الرئيسية الجديد إلى هذا الدليل. انقر بزر الماوس الأيمن على الدليل، واختر جديد -> الأداة -> أداة التطبيق:

f19d8b7f95ab884e.png

  1. يعرض "استوديو Android" نموذجًا جديدًا. أضِف المعلومات الأساسية عن التطبيق المصغّر للشاشة الرئيسية، بما في ذلك اسم الفئة وموضعها وحجمها ولغة المصدر.

في هذا الدرس التطبيقي حول الترميز، اضبط القيم التالية:

  • المربع اسم الفئة في NewsWidget
  • القائمة المنسدلة الحد الأدنى للعرض (الخلايا) إلى 3
  • القائمة المنسدلة الحد الأدنى للارتفاع (الخلايا) إلى 3

فحص نموذج الرمز

عند إرسال النموذج، ينشئ "استوديو Android" عدة ملفات ويعدّلها. يتم إدراج التغييرات ذات الصلة بهذا الدرس التطبيقي في الجدول أدناه.

الإجراء

الملف المستهدف

تغيير

تعديل

AndroidManifest.xml

إضافة مستلم جديد يقوم بتسجيل NewsWidget.

إنشاء

res/layout/news_widget.xml

تحديد واجهة المستخدم المصغّر للشاشة الرئيسية.

إنشاء

res/xml/news_widget_info.xml

تحدد إعدادات التطبيق المصغّر للشاشة الرئيسية. يمكنك تعديل أبعاد الأداة أو اسمها في هذا الملف.

إنشاء

java/com/example/homescreen_widgets/NewsWidget.kt

يحتوي على رمز Kotlin لإضافة وظائف إلى تطبيق الشاشة الرئيسية المصغّر.

يمكنك العثور على مزيد من التفاصيل حول هذه الملفات في هذا الدرس التطبيقي حول الترميز.

تصحيح الأخطاء واختبار نموذج التطبيق المصغّر

الآن، شغِّل تطبيقك وشاهد تطبيق الشاشة الرئيسية المصغّر. بعد إنشاء التطبيق، انتقِل إلى شاشة اختيار التطبيقات على جهاز Android واضغط مع الاستمرار على رمز مشروع Flutter هذا. اختَر التطبيقات المصغّرة من القائمة المنبثقة.

dff7c9f9f85ef1c7.png

يعرض جهاز Android أو المحاكي تطبيق الشاشة الرئيسية التلقائي لنظام التشغيل Android.

4. إرسال البيانات من تطبيق Flutter إلى التطبيق المصغّر على الشاشة الرئيسية

يمكنك تخصيص تطبيق الشاشة الرئيسية المصغّر الذي أنشأته. يجب تحديث تطبيق الشاشة الرئيسية المصغّر لعرض عنوان رئيسي وملخص لمقالة إخبارية. تعرِض لقطة الشاشة التالية مثالاً على تطبيق "الشاشة الرئيسية" المصغّر الذي يعرض عنوانًا وملخّصًا.

acb90343a3e51b6d.png

لتمرير البيانات بين التطبيق وأداة الشاشة الرئيسية، ستحتاج إلى كتابة Dart ورمز أصلي. يقسم هذا القسم هذه العملية إلى ثلاثة أجزاء:

  1. كتابة رمز Dart في تطبيق Flutter يمكن استخدامه على كل من نظامَي التشغيل Android وiOS.
  2. إضافة وظائف iOS الأصلية
  3. إضافة وظائف Android الأصلية

استخدام مجموعات تطبيقات iOS

لمشاركة البيانات بين تطبيق رئيسي على iOS وإضافة أداة، يجب أن ينتمي كلا الهدفَين إلى مجموعة التطبيقات نفسها. لمزيد من المعلومات حول مجموعات التطبيقات، يمكنك الاطّلاع على مستندات مجموعة تطبيقات Apple.

تعديل معرِّف الحزمة:

في Xcode، انتقِل إلى إعدادات هدفك. في مستند توقيع الإمكانات، تحقَّق من ضبط معرِّف الفريق والحزمة.

أضِف مجموعة التطبيقات إلى كل من استهداف Runner وهدف NewsWidgetExtension في Xcode:

اختَر + القدرة -> مجموعات التطبيقات وإضافة مجموعة تطبيقات جديدة. كرِّر هذه الخطوات لكل من هدف Runner (التطبيق الرئيسي) وهدف الأداة.

135e1a8c4652dac.png

إضافة رمز Dart

يمكن لكل من تطبيقات iOS وAndroid مشاركة البيانات مع تطبيق Flutter بعدة طرق مختلفة.للتواصل مع هذه التطبيقات، يمكنك الاستفادة من متجر key/value المحلي للجهاز. يتصل iOS بهذا المتجر UserDefaults، ويتصل Android بهذا المتجر SharedPreferences. تضم home_Widgetet واجهات برمجة التطبيقات هذه لتبسيط حفظ البيانات على أي من النظامين الأساسيين وتمكين أدوات الشاشة الرئيسية من سحب البيانات المحدَّثة.

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. وتعمل هذه الدالة أيضًا على إعلام النظام الأساسي الأصلي بأنّه يمكن استرداد البيانات الجديدة المتعلّقة بالتطبيقات المصغّرة على الشاشة الرئيسية وعرضها.

تعديل الدالةfloatActionButton

يمكنك استدعاء الدالة 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، راجِع مستندات ScheduleEntry الخاصة بشركة 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 إدخال عنصر نائب عندما يعاين المستخدم تطبيق "الشاشة الرئيسية" المصغّر لأول مرة.

45a0f64240c12efe.png

  1. تقرأ الطريقة getSnapshot البيانات من الإعدادات التلقائية للمستخدم وتنشئ الإدخال للوقت الحالي.
  2. تعرض الطريقة getTimeline إدخالات المخطط الزمني. ويساعد ذلك في تحديد نقاط زمنية يمكن توقّعها لتعديل المحتوى. يستخدم هذا الدرس التطبيقي حول الترميز دالة getSnapshot لمعرفة الحالة الحالية. تطلب الطريقة .atEnd من التطبيق المصغّر للشاشة الرئيسية إعادة تحميل البيانات بعد مرور الوقت الحالي.

إضافة تعليق إلى NewsWidgets_Previews

ليس استخدام المعاينات خارج نطاق هذا الدرس التطبيقي حول الترميز. لمزيد من التفاصيل حول معاينة التطبيقات المصغّرة للشاشة الرئيسية في SwiftUI، اطّلِع على مستندات Apple حول تصحيح الأخطاء في التطبيقات المصغّرة.

احفظ كل الملفات وأعِد تشغيل التطبيق وهدف الأداة.

شغِّل الأهداف مرة أخرى للتحقّق من عمل التطبيق وأداة "الشاشة الرئيسية".

  1. اختَر مخطط التطبيق في Xcode لتشغيل استهداف التطبيق.
  2. اختَر مخطط الإضافة في Xcode لتشغيل استهداف الإضافة.
  3. انتقِل إلى صفحة مقالة في التطبيق.
  4. انقر على الزر لتعديل العنوان. من المفترض أن يتم أيضًا تعديل العنوان الرئيسي في تطبيق "الشاشة الرئيسية" المصغّر.

تحديث رمز Android

أضِف ملف XML المصغّر للشاشة الرئيسية.

في "استوديو Android"، عدِّل الملفات التي تم إنشاؤها في الخطوة السابقة.وافتح ملف 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 هذا طريقتي عرض للنص، أحدهما للعنوان الرئيسي للمقالة والآخر لوصف المقالة. تحدد طرق العرض النصية هذه أيضًا النمط. ستعود إلى هذا الملف خلال هذا الدرس التطبيقي حول الترميز.

تحديث وظيفة 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. يتضمّن تطبيق Flutter خطًا مخصّصًا قد ترغب في استخدامه في تطبيق "الشاشة الرئيسية". يمكنك استخدام الخط المخصّص في تطبيق الشاشة الرئيسية على جهاز iOS. لا تتوفّر ميزة استخدام الخطوط المخصّصة في التطبيقات المصغّرة على الشاشة الرئيسية على Android.

تعديل الرمز البرمجي لنظام التشغيل iOS

يخزِّن Flutter أصوله في الحزمة الرئيسية لتطبيقات 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

على جانب Dart، أضِف الطريقة renderFlutterWidget من الحزمة home_Widgetet. تستخدم هذه الطريقة أداة واسم ملف ومفتاحًا. ويعرض صورة من تطبيق Flutter المصغّر ويحفظها في حاوية مشترَكة. أدخِل اسم الصورة في الرمز الخاص بك وتأكَّد من إمكانية وصول تطبيق "الشاشة الرئيسية" المصغّر إلى الحاوية. يحفظ key مسار الملف بالكامل كسلسلة في مساحة التخزين المحلية على الجهاز. يسمح هذا لأداة الشاشة الرئيسية بالعثور على الملف في حالة تغيير الاسم في رمز Dart.

في هذا الدرس التطبيقي حول الترميز، تمثّل الفئة 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 المصغّر هو المركز الذي يحتوي على 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"

يمكنك تضمين العنصر النائب 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
}

اختبار التغييرات

لاختبار التغييرات، أعِد تشغيل كلٍّ من تطبيق 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>

يضيف هذا الرمز الجديد صورة إلى تطبيق الشاشة الرئيسية، والتي تعرض (في الوقت الحالي) رمز نجمة عامًا. استبدل رمز النجمة هذا بالصورة التي حفظتها في رمز Dart.

  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

قد تحتاج إلى توجيه المستخدم إلى صفحة معيّنة في تطبيقك، حسب الموضع الذي ينقر عليه المستخدم. على سبيل المثال، في تطبيق الأخبار من هذا الدرس التطبيقي حول الترميز، قد ترغب في أن يرى المستخدم المقالة الإخبارية للعنوان الرئيسي المعروض.

لا تندرج هذه الميزة ضمن نطاق هذا الدرس التطبيقي حول الترميز. يمكنك العثور على أمثلة على استخدام ساحة المشاركات التي توفّرها حزمة Home_Widget من أجل تحديد عمليات تشغيل التطبيقات من التطبيقات المصغّرة على الشاشة الرئيسية وإرسال الرسائل من تطبيق الشاشة الرئيسية من خلال عنوان URL. لمزيد من المعلومات، يُرجى الاطّلاع على مستندات الربط بصفحة معيّنة على docs.flutter.dev.

تحديث التطبيق المصغّر في الخلفية

لقد شغّلت تحديثًا لتطبيق "الشاشة الرئيسية" المصغّر باستخدام زر في هذا الدرس التطبيقي حول الترميز. على الرغم من أنّ هذا الأمر معقول للاختبار، إلا أنك قد ترغب في تحديث تطبيق الشاشة الرئيسية المصغّر في الخلفية في رمز الإنتاج. يمكنك استخدام المكوّن الإضافي Workmanager لإنشاء مهام في الخلفية لتحديث الموارد التي يحتاجها تطبيق "الشاشة الرئيسية" المصغّر. لمزيد من المعلومات، راجع قسم تحديث الخلفية في حزمة home_Widgetet.

بالنسبة إلى نظام التشغيل iOS، يمكنك أيضًا جعل أداة الشاشة الرئيسية تطلب الشبكة لتحديث واجهة المستخدم الخاصة بها. للتحكّم في شروط هذا الطلب أو معدّل تكراره، استخدِم "المخطّط الزمني". لمزيد من المعلومات عن استخدام "المخطّط الزمني"، اطّلِع على المقالة تحديث الأداة باستمرار" من Apple. التوثيق.

محتوى إضافي للقراءة