在 Flutter 應用程式中加入主畫面小工具

1. 簡介

什麼是小工具?

對 Flutter 開發人員來說,「小工具」一詞通常是指使用 Flutter 架構建立的 UI 元件。在本程式碼實驗室中,主畫面小工具是指應用程式的精簡檢視畫面,不必開啟應用程式,即可顯示應用程式中的資訊。在 Android 裝置上,小工具會顯示在主畫面。在 iOS 裝置上,小工具可以新增到主畫面、螢幕鎖定畫面或「今日摘要」畫面。

iOS 主畫面顯示天氣小工具、日曆小工具和應用程式圖示

Android 主畫面,應用程式圖示上方顯示時鐘和天氣小工具

小工具可以多複雜?

大多數主畫面小工具都著重於一目瞭然的內容。可能包含簡潔的文字、圖片,或 Android 上的基本控制項。Android 和 iOS 都會限制主畫面小工具中可使用的 UI 元件和功能。

主畫面小工具範例,顯示健身活動圓環和電池電量

主畫面小工具範例,顯示音樂播放器和工作檢查清單

建立小工具 UI

由於這些平台 UI 限制,您無法直接使用 Flutter 架構繪製主畫面小工具的 UI。您必須改用平台架構建立小工具,例如 iOS 的 SwiftUI (WidgetKit) 或 Android 的 AppWidget XML 版面配置,然後將小工具新增至 Flutter 應用程式。本程式碼實驗室將示範如何在主應用程式和主畫面小工具之間共用資料、字型資產和轉譯的 Flutter 小工具,這樣您就不必在平台程式碼中重寫複雜的 UI。

建構項目

在本程式碼實驗室中,您將與 Google Antigravity 合作,建構 Flutter 新聞閱讀器應用程式,並使用 home_widget 套件,將應用程式連結至 Android 和 iOS 的主畫面小工具。主畫面小工具會:

  • 顯示 Flutter 應用程式的文章標題和說明資料。
  • 使用從 Flutter 應用程式共用的自訂字型素材資源 (Chewy) 顯示文字。
  • 顯示算繪 Flutter CustomPaint 圖表小工具的圖片。

主畫面小工具,在行動裝置主畫面上顯示新聞報導標題、摘要和折線圖

Flutter 新聞應用程式包含兩個畫面 (路徑):

  • 第一個畫面會顯示新聞報導清單,其中包含標題和說明。
  • 第二個畫面會顯示完整文章、使用 CustomPaint 建立的折線圖,以及更新主畫面小工具的按鈕。

Flutter 新聞應用程式的「焦點新聞」畫面,列出四篇含有標題和說明的報導

Flutter 新聞應用程式文章詳細資料畫面,顯示內文、折線圖和「Update Homescreen」按鈕

學習旅程

本程式碼實驗室會逐步引導您建構應用程式和主畫面小工具:

  1. 設定 Flutter 環境:設定 Flutter SDK、Google Antigravity,以及目標平台的 Xcode 或 Android Studio。
  2. 建立範例新聞應用程式:提示 Antigravity 使用 home_widget、Chewy 字型、文章清單和詳細資料畫面,建構 homescreen_widgets Flutter 應用程式。
  3. 新增基本主畫面小工具:在 Xcode 中建立 NewsWidgets iOS 小工具擴充功能,並在 Android Studio 中建立 NewsWidget Android 應用程式小工具。
  4. 將 Flutter 應用程式的資料傳送至主畫面小工具:設定 iOS 應用程式群組,提示 Antigravity 連結 HomeWidget.saveWidgetData 和 HomeWidget.updateWidget,並在 Swift 和 Kotlin 中讀取共用資料。
  5. 在 iOS 主畫面小工具中使用自訂字型:從 iOS 小工具擴充功能中的 Flutter 應用程式套件,載入並註冊隨附的 Chewy-Regular.ttf 字型資產。
  6. 將 Flutter 小工具轉譯為圖片:提示 Antigravity 使用 HomeWidget.renderFlutterWidget 將 LineChart 小工具擷取為 PNG,並在 iOS 和 Android 主畫面小工具中顯示轉譯的圖表。

課程內容

  • 如何與 Google Antigravity 協作,反覆建構 Flutter 應用程式,並連結至 iOS 和 Android 主畫面小工具。
  • 如何在 iOS 和 Android 裝置上建立主畫面小工具。
  • 如何使用 home_widget 套件,在主畫面小工具和 Flutter 應用程式之間共用資料。
  • 如何在主畫面小工具中重複使用 Flutter 字型和轉譯的 Flutter 小工具,減少重複的 UI 程式碼。
  • 如何從 Flutter 應用程式觸發主畫面小工具更新。

必要條件

  • 熟悉 Flutter 小工具和 Dart 語法。

2. 設定 Flutter 環境

編輯者

本程式碼實驗室使用 Google Antigravity 做為 AI 優先開發環境,並搭配 Xcode (適用於 iOS 小工具目標) 或 Android Studio (適用於 Android 小工具目標)。在每個步驟中,您會提示 Antigravity 生成或更新 Dart 和平台小工具檔案、查看生成程式碼中的重要概念,並視需要探索創意變化版本。

選擇開發目標

建立 iOS 主畫面小工具

  • 您可以在實體 iOS 裝置或 iOS 模擬器上執行本程式碼實驗室。
  • 您必須使用已安裝 Xcode IDE 的 macOS 系統,才能新增小工具擴充功能目標、設定應用程式群組,以及建構 iOS 版應用程式。

建立 Android 主畫面小工具

  • 您可以在實體 Android 裝置或 Android Emulator 上執行本程式碼實驗室。
  • 您必須使用 Android Studio 設定開發系統,才能建構 Android 版應用程式,並使用應用程式小工具精靈。

安裝 Flutter

如需安裝 Flutter SDK 的最新操作說明,請前往 docs.flutter.dev。

請安裝下列軟體,以便進行本程式碼實驗室:

  1. Flutter SDK
  2. Google Antigravity,並啟用官方 Dart 和 Flutter 外掛程式 (可在外掛程式市集的「Build with Google」部分找到)。
  3. 所選行動目標的編譯器和平台工具 (指定 iOS 則使用 Xcode,指定 Android 則使用 Android Studio)。

3. 建立範例新聞應用程式

在這個步驟中,您會提示 Antigravity 建構 homescreen_widgets Flutter 專案、安裝 home_widget 和 material_ui 套件、繫結 Chewy 自訂字型,以及建立雙螢幕新聞閱讀器應用程式。

提示 Antigravity 建立專案

  1. 啟動 Google Antigravity,然後開啟要建構專案的空白目錄 (請選擇不需進階權限或不含空格的路徑,例如主目錄或 C:\src\ 中的資料夾)。
  2. 在 Antigravity 代理程式對話面板中,傳送下列提示詞,建立 Flutter 專案、新增依附元件和字型素材資源,並生成新聞應用程式範例:
Create a new empty Flutter project named `homescreen_widgets` targeting Android and iOS (`flutter create --empty homescreen_widgets --platforms=android,ios --org=com.mydomain`). Add the `cupertino_icons`, `shared_preferences`, `home_widget`, and `material_ui` packages. Download `Chewy-Regular.ttf` from `https://raw.githubusercontent.com/flutter/codelabs/main/homescreen_codelab/step_03/fonts/Chewy-Regular.ttf` into `fonts/Chewy-Regular.ttf` and register the `Chewy` font family in `pubspec.yaml`. Using `package:material_ui/material_ui.dart`, create the starter news reader app:
- `lib/news_data.dart` defining a `NewsArticle` class (`title`, `description`, `articleText = loremIpsum`, and optional `image`) and a `getNewsStories()` function returning sample Flutter news articles.
- `lib/article_screen.dart` defining an `ArticleScreen` stateful widget that displays the article's `description`, `articleText`, a centered 200x200 `LineChart` widget drawn with a `CustomPainter` (`LineChartPainter`), and a `FloatingActionButton.extended` labeled `'Update Homescreen'` that shows a `SnackBar` (`'Updating home screen widget...'`).
- `lib/home_screen.dart` defining a `MyHomePage` stateful widget with an `AppBar` titled `'Top Stories'` and a `ListView.separated` of `getNewsStories()` where tapping a `ListTile` navigates to `ArticleScreen(article: article)`.
- `lib/main.dart` configuring `MaterialApp` with a `Colors.deepPurple` seed color scheme, `titleMedium: TextStyle(fontFamily: 'Chewy', fontSize: 20)`, and `home: const MyHomePage()`.

瞭解生成的程式碼

新聞報導模型 (lib/news_data.dart)

lib/news_data.dart 定義 NewsArticle 資料類別,並透過 getNewsStories() 提供模擬故事:

class NewsArticle {
  final String title;
  final String description;
  final String? articleText;
  final String? image;

  NewsArticle({
    required this.title,
    required this.description,
    this.articleText = loremIpsum,
    this.image,
  });
}

繪製文章圖表 (lib/article_screen.dart)

lib/article_screen.dart 會算繪所選的 NewsArticle,以及由 LineChartPainter 支援的 200x200 LineChart 小工具,是 CustomPainter,可將軸、門檻線和連結的資料點繪製到 Canvas 上:

class LineChart extends StatelessWidget {
  const LineChart({super.key});

  @override
  Widget build(BuildContext context) {
    return CustomPaint(
      painter: LineChartPainter(),
      child: const SizedBox(height: 200, width: 200),
    );
  }
}

執行及測試應用程式

在 iOS 模擬器、Android Emulator 或已連結的行動裝置上執行範例應用程式,確認「Top Stories」清單和「ArticleScreen」是否正常顯示:

flutter run

4. 新增基本主畫面小工具

連結 Dart 程式碼前,請先使用 iOS 和/或 Android 的平台工具,新增基本的主畫面小工具目標。

建立基本的 iOS 主畫面小工具

在 Flutter iOS 應用程式中新增小工具擴充功能,與在 SwiftUI 或 UIKit 應用程式中新增擴充功能的工作流程相同:

  1. 在 homescreen_widgets 專案目錄中執行 open ios/Runner.xcworkspace,在 Xcode 中開啟 ios/Runner.xcworkspace。
  2. 在 Xcode 選單中,依序選取「File」→「New」→「Target」,在專案中新增目標。
  3. 從範本清單中選取「Widget Extension」,然後按一下「Next」。
  4. 在「Product Name」方塊中輸入 NewsWidgets。清除「Include Live Activity」和「Include Configuration Intent」(或「Include Configuration App Intent」) 核取方塊,然後按一下「Finish」。

檢查生成的程式碼

新增目標後,Xcode 會在 ios/NewsWidgets/NewsWidgets.swift 和 ios/NewsWidgets/NewsWidgetsBundle.swift 中產生樣板 WidgetKit 程式碼。如要進一步瞭解 WidgetKit 擴充功能,請參閱 Apple 的應用程式擴充功能說明文件。

對範例小工具進行偵錯及測試

  1. 每次新增套件或目標時,請先更新 Flutter 應用程式的 iOS 設定,再透過 Xcode 建構。在 homescreen_widgets 目錄中執行:
flutter build ios --config-only
  1. 在 Xcode 的配置選取器中,按一下「Run」按鈕旁的「Runner」,選取剛建立的「NewsWidgetsExtension」(或「NewsWidgets」) 目標,然後按一下「Run」。

Xcode 配置選取器選單,顯示 Runner 和 NewsWidgetsExtension 目標

  1. 模擬器或裝置會顯示主畫面小工具預留位置,其中顯示目前時間。如果沒有自動顯示,請按住 iOS 主畫面的空白處,然後輕觸左上角的「+」按鈕:

iOS 主畫面處於晃動模式,左上角有新增小工具的加號按鈕

  1. 在小工具庫中搜尋「主畫面小工具」,然後將小工具新增至主畫面:

iOS 小工具庫搜尋結果,顯示「主畫面小工具」應用程式

建立基本的 Android 主畫面小工具

  1. 在 Android Studio 中開啟 homescreen_widgets/android 目錄。
  2. Gradle 同步處理完成後,請在左側的「Project」窗格中找出 app 模組。在「app」上按一下滑鼠右鍵,然後依序選取「New」→「Widget」→「App Widget」。

Android Studio 內容選單,顯示「New」、「Widget」和「App Widget」

  1. 在「New Android Component」對話方塊中,使用下列值設定主畫面小工具,然後按一下「Finish」:
  • Class Name:NewsWidget
  • 最小寬度 (儲存格):3
  • Minimum Height (cells):3

檢查生成的檔案

Android Studio 會在 Android 專案中建立及更新下列檔案 (如果您不使用 Android Studio 精靈,也可以要求 Antigravity 直接建構這些檔案):

動作

目標檔案

變更

更新

AndroidManifest.xml

註冊 ,以便 .NewsWidget 處理 APPWIDGET_UPDATE。

建立

res/layout/news_widget.xml

定義主畫面小工具 XML 版面配置。

建立

res/xml/news_widget_info.xml

設定小工具尺寸、更新週期和預覽版面配置。

建立

java/com/mydomain/homescreen_widgets/NewsWidget.kt

定義 NewsWidget : AppWidgetProvider() 類別。

對範例小工具進行偵錯及測試

在 Android 裝置或模擬器上建構及執行 Flutter 應用程式。安裝完成後,按住啟動器畫面上的應用程式圖示,然後從選單中選取「小工具」,即可將小工具放到主畫面。

Android 啟動器圖示內容選單,顯示「小工具」選項

5. 將資料從 Flutter 應用程式傳送至主畫面小工具

裝置上已有基本的主畫面小工具,現在要更新小工具,顯示 Flutter 應用程式中新聞報導的標題和說明。

iOS 主畫面小工具,顯示 Flutter DAU 標題和摘要文字

在 Flutter 應用程式和主畫面小工具之間共用資料,包含三個部分:

  1. 在 Xcode 中設定 iOS 應用程式群組,讓 Runner 應用程式和小工具擴充功能共用本機儲存空間容器。
  2. 更新 Dart 程式碼,寫入頭條新聞資料,並使用 package:home_widget/home_widget.dart 觸發小工具重新整理。
  3. 更新 iOS (NewsWidgets.swift) 和 Android (news_widget.xml 和 NewsWidget.kt) 小工具實作,以讀取及轉譯儲存的字串。

設定 iOS 應用程式群組

在 iOS 上,父項應用程式和其小工具擴充功能會在不同的沙箱中執行。如要在兩者之間共用本機 UserDefaults 資料和檔案,這兩個目標必須屬於同一個應用程式群組。如需應用程式群組的背景資訊,請參閱 Apple 的「設定應用程式群組」說明文件。

  1. 在 Xcode 中,開啟「Runner」目標設定,然後選取「Signing & Capabilities」分頁標籤。確認已設定「小組」和「軟體包 ID」。
  2. 按一下「+ Capability」,選取「App Groups」,然後按一下「App Groups」下方的「+」按鈕,新增群組 ID (例如 group.com.mydomain.homescreenWidgets)。
  3. 在 Xcode 中選取 NewsWidgetsExtension 目標,開啟「Signing & Capabilities」,新增「App Groups」功能,然後選取完全相同的應用程式群組 ID。

Xcode 的「Signing and Capabilities」分頁,顯示已啟用「App Groups」功能

「home_widget」如何分享資料

iOS 和 Android 都提供本機鍵/值儲存區:iOS 上的 UserDefaults 和 Android 上的 SharedPreferences。home_widget 套件會包裝這些平台 API,讓 Flutter 程式碼可以儲存鍵/值組合,並通知使用中的主畫面小工具重新載入檢視畫面。

架構流程圖,顯示 Flutter 應用程式將鍵/值資料寫入 UserDefaults 和 SharedPreferences,以及主畫面小工具讀取資料

提示 Antigravity 傳送及顯示文章資料

在 Antigravity 代理程式對話面板中,傳送下列提示詞,在 Dart 中連結 home_widget,並更新 iOS 和 Android 主畫面小工具,顯示 headline_title 和 headline_description:

Connect the Flutter app to the iOS and Android Home Screen widgets using `package:home_widget/home_widget.dart`:
1. In `lib/home_screen.dart`, define constants `appGroupId = '<YOUR APP GROUP>'`, `iOSWidgetName = 'NewsWidgets'`, and `androidWidgetName = 'NewsWidget'`. Add a top-level `updateHeadline(NewsArticle newHeadline)` function that saves `'headline_title'` (`newHeadline.title`) and `'headline_description'` (`newHeadline.description`) using `HomeWidget.saveWidgetData<String>` and triggers `HomeWidget.updateWidget(iOSName: iOSWidgetName, androidName: androidWidgetName)`. In `_MyHomePageState.initState()`, call `HomeWidget.setAppGroupId(appGroupId)` and `updateHeadline(getNewsStories()[0])`.
2. In `lib/article_screen.dart`, import `home_screen.dart` and call `updateHeadline(widget.article)` inside the `FloatingActionButton.extended` `onPressed` callback.
3. Update `ios/NewsWidgets/NewsWidgets.swift` (creating it if needed) with a `NewsArticleEntry: TimelineEntry` struct (`date`, `title`, `description`), a `NewsWidgetsEntryView` displaying `Text(entry.title)` and `Text(entry.description)` in a `VStack`, and a `Provider: TimelineProvider` that reads `"headline_title"` and `"headline_description"` from `UserDefaults(suiteName: "<YOUR APP GROUP>")` in `getSnapshot`.
4. Update `android/app/src/main/res/layout/news_widget.xml` and `android/app/src/main/java/com/mydomain/homescreen_widgets/NewsWidget.kt` (creating them and registering `.NewsWidget` in `AndroidManifest.xml` if needed) so `onUpdate` reads `"headline_title"` and `"headline_description"` using `HomeWidgetPlugin.getData(context)` and updates `R.id.headline_title` and `R.id.headline_description` with `setTextViewText`.

Antigravity 套用這些變更後,請將 lib/home_screen.dart 和 ios/NewsWidgets/NewsWidgets.swift 中的 替換為您在 Xcode 中設定的應用程式群組 ID。

瞭解生成的程式碼

從 Flutter 儲存小工具資料 (lib/home_screen.dart 和 lib/article_screen.dart)

在 lib/home_screen.dart 中,updateHeadline 會將 headline_title 和 headline_description 寫入共用平台儲存區,並呼叫 HomeWidget.updateWidget,要求在 iOS 和 Android 上立即重新載入小工具:

const String appGroupId = '<YOUR APP GROUP>';
const String iOSWidgetName = 'NewsWidgets';
const String androidWidgetName = 'NewsWidget';

void updateHeadline(NewsArticle newHeadline) {
  HomeWidget.saveWidgetData<String>('headline_title', newHeadline.title);
  HomeWidget.saveWidgetData<String>(
    'headline_description',
    newHeadline.description,
  );
  HomeWidget.updateWidget(
    iOSName: iOSWidgetName,
    androidName: androidWidgetName,
  );
}

在 _MyHomePageState.initState() 中,應用程式會向 HomeWidget.setAppGroupId(appGroupId) 註冊 iOS 應用程式群組 ID,並在小工具中加入第一則報導。在 lib/article_screen.dart 中,按下「Update Homescreen」FloatingActionButton 會呼叫 updateHeadline(widget.article),將目前查看的報導推送至主畫面小工具。

在 iOS WidgetKit TimelineProvider (ios/NewsWidgets/NewsWidgets.swift) 中讀取資料

在 iOS 上,NewsArticleEntry 符合 TimelineEntry,並保留傳遞至 NewsWidgetsEntryView 的 date、title 和 description:

struct NewsArticleEntry: TimelineEntry {
    let date: Date
    let title: String
    let description: String
}

struct NewsWidgetsEntryView : View {
    var entry: Provider.Entry

    var body: some View {
      VStack {
        Text(entry.title)
        Text(entry.description)
      }
    }
}

Provider 透過三種方法實作 TimelineProvider:

  1. placeholder(in:):使用者首次在 iOS 小工具庫中預覽小工具時,傳回預留位置 NewsArticleEntry。

iOS 小工具預覽畫面,顯示預留位置標題和預留位置說明

  1. getSnapshot(in:completion:):從 UserDefaults(suiteName: "") 讀取 "headline_title" 和 "headline_description",並為目前狀態建構 NewsArticleEntry:
func getSnapshot(in context: Context, completion: @escaping (NewsArticleEntry) -> ()) {
  let entry: NewsArticleEntry
  if context.isPreview {
    entry = placeholder(in: context)
  } else {
    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)
}
  1. getTimeline(in:completion:):將快照項目包裝在 Timeline 中,並採用 .atEnd 重新載入政策,讓 WidgetKit 在收到要求時重新整理小工具。

在 Android AppWidgetProvider (NewsWidget.kt 和 news_widget.xml) 中讀取資料

在 Android 上,android/app/src/main/res/layout/news_widget.xml 會在 RelativeLayout 內定義兩個 TextView 元素 (@+id/headline_title 和 @+id/headline_description)。

在 android/app/src/main/java/com/mydomain/homescreen_widgets/NewsWidget.kt 中,onUpdate 會使用 HomeWidgetPlugin.getData(context) 擷取共用偏好設定,並在小工具的 RemoteViews 上填入兩個 TextView:

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

            appWidgetManager.updateAppWidget(appWidgetId, views)
        }
    }
}

執行及測試應用程式

重新建構並執行應用程式 (如果是在 iOS 上測試,則為 Xcode 中的 NewsWidgetsExtension 目標)。在應用程式中開啟任一文章,然後輕觸「更新主畫面」。返回裝置的主畫面,確認小工具顯示所選文章的標題和說明:

Android 主畫面小工具顯示「Remembering Flutter Forward」標題和摘要

6. 在 iOS 主畫面小工具中使用自訂字型

Flutter 應用程式會使用自訂 Chewy 字型系列 (fonts/Chewy-Regular.ttf) 做為文章副標題。在 iOS 上,Flutter 會將資產封裝在主要應用程式套件的 Frameworks/App.framework/flutter_assets 中。iOS 小工具擴充功能可以存取該共用套件目錄,並使用 Core Text 註冊字型,讓 SwiftUI 小工具與 Flutter 應用程式的字體排版相符。

提示 Antigravity 在 iOS 上註冊並套用自訂字型

在 Antigravity 代理程式對話面板中,傳送下列提示詞:

Update `NewsWidgetsEntryView` in `ios/NewsWidgets/NewsWidgets.swift` to use the Flutter app's bundled `Chewy` font for the headline title:
1. Add a computed `bundle: URL` property on `NewsWidgetsEntryView` that checks if `Bundle.main.bundleURL.pathExtension == "appex"` and, if so, navigates up two directory levels and appends `"Frameworks/App.framework/flutter_assets"`.
2. Add an `init(entry: Provider.Entry)` initializer that assigns `self.entry = entry` and registers `bundle.appending(path: "/fonts/Chewy-Regular.ttf")` using `CTFontManagerRegisterFontsForURL(..., CTFontManagerScope.process, nil)`.
3. Apply `.font(Font.custom("Chewy", size: 13))` to `Text(entry.title)` in `body`.

瞭解生成的程式碼

從 .appex 擴充功能找出 Flutter 資產套件

由於小工具是在 .appex 套件 (MY_APP.app/PlugIns/MY_APP_EXTENSION.appex) 內執行,因此計算出的 bundle 屬性會剝離兩個目錄層級,以到達父項應用程式套件並附加 Frameworks/App.framework/flutter_assets:

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
}

註冊及套用字型

在 NewsWidgetsEntryView.init(entry:) 中,CTFontManagerRegisterFontsForURL 會為目前程序註冊 /fonts/Chewy-Regular.ttf,以便 SwiftUI 的 Font.custom("Chewy", size: 13) 顯示標題:

init(entry: Provider.Entry) {
    self.entry = entry
    CTFontManagerRegisterFontsForURL(
        bundle.appending(path: "/fonts/Chewy-Regular.ttf") as CFURL,
        CTFontManagerScope.process,
        nil
    )
}

var body: some View {
    VStack {
        Text(entry.title).font(Font.custom("Chewy", size: 13))
        Text(entry.description)
    }
}

執行及測試應用程式

從 Xcode 重新執行小工具目標。iOS 主畫面小工具現在會以 Chewy 字型顯示文章標題:

iOS 主畫面小工具,以自訂 Chewy 字型顯示文章標題

7. 將 Flutter 小工具算繪為圖片

如果要在 SwiftUI 和 Android XML 中重新建立自訂 Flutter 視覺效果 (例如使用 CustomPainter 繪製的 LineChart),就必須維護三種語言的重複繪圖程式碼。HomeWidget.renderFlutterWidget 可讓您將任何 Flutter 小工具點陣化為共用容器中的 PNG 檔案,並將其路徑傳遞至 iOS 和 Android 主畫面小工具。

提示 Antigravity 算繪 Flutter 圖表,並顯示在小工具中

在 Antigravity 代理程式對話面板中,傳送下列提示詞:

Render the `LineChart` Flutter widget to an image and display it in both the iOS and Android Home Screen widgets:
1. In `lib/article_screen.dart`, import `package:home_widget/home_widget.dart` and add `final _globalKey = GlobalKey();` and `String? imagePath;` to `_ArticleScreenState`. Attach `key: _globalKey` to the `Center` widget wrapping `const LineChart()`. Make the `FloatingActionButton.extended` `onPressed` callback `async`: when `_globalKey.currentContext != null`, call `await HomeWidget.renderFlutterWidget(const LineChart(), key: 'filename', logicalSize: _globalKey.currentContext!.size!, pixelRatio: MediaQuery.of(_globalKey.currentContext!).devicePixelRatio)`, store the returned path in `imagePath` with `setState()`, and then call `updateHeadline(widget.article)`.
2. In `ios/NewsWidgets/NewsWidgets.swift`, add `let filename: String` and `let displaySize: CGSize` to `NewsArticleEntry`, pass placeholder values in `placeholder(in:)`, read `userDefaults?.string(forKey: "filename") ?? "No screenshot available"` and `context.displaySize` in `getSnapshot`, and add a `ChartImage` computed view on `NewsWidgetsEntryView` that loads `UIImage(contentsOfFile: entry.filename)` into a resizable `Image` framed at `entry.displaySize.height * 0.5` and renders `ChartImage` below the description in `VStack`.
3. In `android/app/src/main/res/layout/news_widget.xml`, add an `<ImageView android:id="@+id/widget_image" ... />` below `@+id/headline_description`. In `android/app/src/main/java/com/mydomain/homescreen_widgets/NewsWidget.kt`, read `widgetData.getString("filename", null)` in `onUpdate`, check if `File(imageName).exists()`, decode the file with `BitmapFactory.decodeFile(imageFile.absolutePath)`, and call `setImageViewBitmap(R.id.widget_image, myBitmap)`.

更新 getSnapshot 時,請在 ios/NewsWidgets/NewsWidgets.swift 中保留實際的應用程式群組 ID。

瞭解生成的程式碼

將 Flutter 小工具擷取為 PNG (lib/article_screen.dart)

在 _ArticleScreenState 中,_globalKey 會附加至包裝 const LineChart() 的 Center 小工具,讓應用程式讀取轉譯的 size 和 devicePixelRatio。當使用者輕觸「Update Homescreen」時,HomeWidget.renderFlutterWidget 會將 const LineChart() 轉譯為共用容器中的 PNG 檔案,並將其路徑儲存在本機儲存空間的 'filename' 鍵下:

floatingActionButton: FloatingActionButton.extended(
  onPressed: () async {
    if (_globalKey.currentContext != null) {
      var path = await HomeWidget.renderFlutterWidget(
        const LineChart(),
        key: 'filename',
        logicalSize: _globalKey.currentContext!.size!,
        pixelRatio:
            MediaQuery.of(_globalKey.currentContext!).devicePixelRatio,
      );
      setState(() {
        imagePath = path;
      });
    }
    updateHeadline(widget.article);
  },
  label: const Text('Update Homescreen'),
),

在 iOS 上顯示算繪圖片 (ios/NewsWidgets/NewsWidgets.swift)

在 NewsWidgets.swift 中,getSnapshot 會從 UserDefaults 讀取 "filename" 路徑,並將其與 context.displaySize 一起傳遞至 NewsArticleEntry。NewsWidgetsEntryView 定義 ChartImage 運算屬性,使用 UIImage(contentsOfFile: entry.filename) 載入 PNG 檔案,並縮放至小工具高度的 50%:

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

在 Android 上顯示算繪的圖片 (news_widget.xml 和 NewsWidget.kt)

在 android/app/src/main/res/layout/news_widget.xml 中, 位於 @+id/headline_description 下方。在 NewsWidget.kt 中,onUpdate 會從 widgetData 讀取 "filename" 路徑,使用 BitmapFactory.decodeFile 將 PNG 檔案解碼為 Bitmap,並使用 setImageViewBitmap 更新 ImageView:

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

執行及測試應用程式

重新執行 Flutter 應用程式和平台小工具目標。在應用程式中開啟文章頁面,然後輕觸「更新主畫面」。主畫面小工具會更新,顯示文章標題、摘要和算繪的 LineChart 圖片:

iOS 模擬器顯示文章詳細資料畫面,以及顯示標題、說明和折線圖的主畫面小工具

8. 恭喜!

恭喜!您已使用 Google Antigravity 和 home_widget,為 iOS 和 Android 建構主畫面小工具,並連結至 Flutter 應用程式。

建構內容

  • 使用 material_ui、自訂 Chewy 字體排版和 CustomPainter 折線圖的雙螢幕 Flutter 新聞應用程式。
  • 原生 iOS (WidgetKit/SwiftUI) 和 Android (AppWidgetProvider/RemoteViews) 主畫面小工具。
  • 在 Dart、UserDefaults (使用 iOS 應用程式群組) 和 SharedPreferences 之間同步處理鍵/值資料。
  • 在 iOS 小工具擴充功能中註冊跨套件自訂字型。
  • 在 iOS 和 Android 主畫面小工具中,顯示螢幕外 Flutter 小工具光柵化 (HomeWidget.renderFlutterWidget)。

後續步驟

連結至 Flutter 應用程式中的內容

您可能希望使用者輕觸主畫面小工具時,能前往 Flutter 應用程式中的特定畫面,例如開啟所顯示標題的完整文章。您可以使用 home_widget 提供的 widgetClicked 串流偵測小工具啟動,並將深層連結 URI 傳遞至應用程式。詳情請參閱 docs.flutter.dev 上的深層連結說明文件。

正在背景更新小工具

在本程式碼實驗室中,您已透過應用程式內按鈕觸發主畫面小工具更新。在正式版中,您可以使用 workmanager 外掛程式搭配 home_widget,安排定期背景更新。詳情請參閱 home_widget 的背景更新說明文件。

在 iOS 上,您也可以直接透過 WidgetKit 的 TimelineProvider,排定定期重新整理時間軸或背景網路要求。請參閱 Apple 的「Keeping a widget up to date」說明文件。

資源