向 Flutter 应用添加主屏幕 widget

1. 简介

widget 是什么?

对于 Flutter 开发者,术语 widget 通常是指使用 Flutter 框架创建的界面组件。在本 Codelab 中,主屏幕微件是指应用的紧凑视图,无需打开应用即可显示应用中的信息。在 Android 设备上,微件位于主屏幕上。在 iOS 设备上,可将小组件添加到主屏幕、锁定屏幕或“今天”视图中。

iOS 主屏幕,其中显示了天气微件、日历微件和应用图标

Android 主屏幕,在应用图标上方显示时钟和天气微件

微件可以有多复杂?

大多数主屏幕微件都侧重于一目了然的内容。其中可能包含简明文本、图形,或者在 Android 设备上包含基本控件。Android 和 iOS 都会限制您在主屏幕微件中可以使用的界面组件和功能。

显示健身活动圆环和电池电量的主屏幕微件示例

主屏幕微件示例,显示了音乐播放器和任务清单

为微件创建界面

由于这些平台界面限制,您无法直接使用 Flutter 框架绘制主屏幕微件的界面。而是需要将使用平台框架(例如 iOS 上的 SwiftUI [WidgetKit] 或 Android 上的 AppWidget XML 布局)创建的微件添加到 Flutter 应用中。此 Codelab 演示了如何在主应用和主屏幕微件之间共享数据、字体资源和渲染的 Flutter 微件,这样您就不必在平台代码中重写复杂的界面。

构建内容

在此 Codelab 中,您将与 Google Antigravity 合作,构建一个 Flutter 新闻阅读器应用,并使用 home_widget 软件包将其连接到 Android 和 iOS 上的主屏幕微件。主屏幕微件将:

  • 显示 Flutter 应用中的文章标题和说明数据。
  • 使用从 Flutter 应用共享的自定义字体资源 (Chewy) 显示文本。
  • 显示渲染后的 Flutter CustomPaint 图表 widget 的图片。

手机主屏幕上的主屏幕微件,显示新闻报道标题、摘要和折线图

Flutter 新闻应用包含两个屏幕(路由):

  • 第一个界面显示新闻报道列表,其中包含标题和说明。
  • 第二个屏幕显示了完整文章、使用 CustomPaint 创建的折线图,以及用于更新主屏幕 widget 的按钮。

Flutter 新闻应用“焦点新闻”界面,其中列出了四篇报道,并附有标题和说明

Flutter 新闻应用文章详情界面,其中显示了正文、折线图和“Update Homescreen”(更新主屏幕)按钮

您的学习之旅

此 Codelab 将引导您逐步构建应用及其主屏幕 widget:

  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 应用发送到主屏幕 widget - 配置 iOS 应用组,提示 Antigravity 连接 HomeWidget.saveWidgetData 和 HomeWidget.updateWidget,并使用 Swift 和 Kotlin 读取共享数据。
  5. 在 iOS 主屏幕微件中使用自定义字体 - 从 iOS 微件扩展程序内的 Flutter 应用软件包中加载并注册捆绑的 Chewy-Regular.ttf 字体资源。
  6. 将 Flutter widget 渲染为图片 - 提示 Antigravity 使用 HomeWidget.renderFlutterWidget 将 LineChart widget 捕获为 PNG,并在 iOS 和 Android 主屏幕 widget 中显示渲染后的图表。

学习内容

  • 如何与 Google Antigravity 协作,以迭代方式构建 Flutter 应用并将其与 iOS 和 Android 主屏幕微件连接。
  • 如何在 iOS 和 Android 设备上创建主屏幕微件。
  • 如何使用 home_widget 软件包在主屏幕微件和 Flutter 应用之间共享数据。
  • 如何在主屏幕微件中重复使用 Flutter 字体和渲染的 Flutter 微件,以减少重复的界面代码。
  • 如何从 Flutter 应用触发主屏幕微件更新。

前提条件

  • 熟悉 Flutter widget 和 Dart 语法。

2. 设置您的 Flutter 环境

编辑者

此 Codelab 使用 Google Antigravity 作为 AI 优先的开发环境,并搭配 Xcode(适用于 iOS widget 目标)或 Android Studio(适用于 Android widget 目标)。在每个步骤中,您都需要提示 Antigravity 生成或更新 Dart 和平台 widget 文件,查看生成的代码中的关键概念,并可选择探索创意变体。

选择目标开发平台

创建 iOS 主屏幕微件

  • 您可以在 iOS 设备或 iOS 模拟器上运行此 Codelab。
  • 您必须使用安装了 Xcode IDE 的 macOS 系统来添加 Widget Extension 目标、配置应用组,以及构建应用的 iOS 版本。

创建 Android 主屏幕微件

  • 您可以在实体 Android 设备或 Android 模拟器上运行此 Codelab。
  • 您必须使用 Android Studio 配置开发系统,才能构建应用的 Android 版本并使用应用 widget 向导。

安装 Flutter

如需获取关于如何安装 Flutter SDK 的最新说明,请随时访问 docs.flutter.dev。

对于此 Codelab,请安装以下软件:

  1. Flutter SDK
  2. 已启用官方 Dart 和 Flutter 插件的 Google Antigravity(可在插件市场的 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 widget,是一个 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 模拟器或连接的移动设备上运行起始应用,验证 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. 从模板列表中选择微件扩展程序,然后点击下一步。
  4. 在产品名称框中输入 NewsWidgets。清除包含实时活动和包含配置 intent(或包含配置应用 intent)复选框,然后点击完成。

检查生成的代码

添加目标后,Xcode 会在 ios/NewsWidgets/NewsWidgets.swift 和 ios/NewsWidgets/NewsWidgetsBundle.swift 中生成 WidgetKit 样板代码。如需详细了解 WidgetKit 扩展程序,请参阅 Apple 的应用扩展程序文档。

调试和测试示例微件

  1. 每当您在通过 Xcode 构建应用之前添加新的软件包或目标时,请更新 Flutter 应用的 iOS 配置。在 homescreen_widgets 目录中,运行以下命令:
flutter build ios --config-only
  1. 在 Xcode 的 scheme 选择器中(位于 Run 按钮旁边),点击 Runner,选择您刚刚创建的 NewsWidgetsExtension(或 NewsWidgets)目标,然后点击 Run。

Xcode scheme 选择器菜单,其中显示了 Runner 和 NewsWidgetsExtension 目标

  1. 模拟器或设备会显示一个占位主屏幕微件,其中显示当前时间。如果该应用未自动显示,请轻触并按住 iOS 主屏幕上的空白区域,然后点按左上角的 + 按钮:

iOS 主屏幕处于抖动模式,左上角显示“添加微件”加号按钮

  1. 在微件库中搜索主屏幕微件,然后将该微件添加到主屏幕:

iOS widget 库搜索结果,其中显示了主屏幕 widget 应用

创建基本的 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 对话框中,使用以下值配置主屏幕 widget,然后点击 Finish:
  • 类名称:NewsWidget
  • 最小宽度(单元格数):3
  • 最小高度(单元格数):3

检查生成的文件

Android Studio 会在您的 Android 项目中创建和更新以下文件(如果您不使用 Android Studio 向导,也可以让 Antigravity 直接搭建这些文件):

操作

目标文件

更改

更新

AndroidManifest.xml

为处理 APPWIDGET_UPDATE 的 .NewsWidget 注册 。

创建

res/layout/news_widget.xml

定义主屏幕 widget XML 布局。

创建

res/xml/news_widget_info.xml

配置 widget 尺寸、更新周期和预览布局。

创建

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 触发 widget 刷新。
  3. 更新 iOS (NewsWidgets.swift) 和 Android (news_widget.xml 和 NewsWidget.kt) widget 实现,以读取和呈现保存的字符串。

配置 iOS 应用群组

在 iOS 上,父级应用及其微件扩展程序在不同的沙盒中运行。如需在两个目标之间共享本地 UserDefaults 数据和文件,这两个目标必须属于同一个应用组。如需了解应用群组的背景信息,请参阅 Apple 的“配置应用群组”文档。

  1. 在 Xcode 中,打开 Runner 目标设置,然后选择 Signing & Capabilities(签名和功能)标签页。验证您的团队和软件包标识符是否已设置。
  2. 点击 + Capability(+ 功能),选择 App Groups(应用群组),然后点击 App Groups(应用群组)下的 + 按钮,以添加新的群组标识符(例如 group.com.mydomain.homescreenWidgets)。
  3. 在 Xcode 中选择 NewsWidgetsExtension 目标,打开 Signing & Capabilities,添加 App Groups 功能,然后选择完全相同的应用组标识符。

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 主屏幕 widget,使其显示 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 中配置的应用组标识符。

了解生成的代码

保存来自 Flutter 的 widget 数据(lib/home_screen.dart 和 lib/article_screen.dart)

在 lib/home_screen.dart 中,updateHeadline 会将 headline_title 和 headline_description 写入共享平台存储区,并调用 HomeWidget.updateWidget 以请求在 iOS 和 Android 上立即重新加载 widget:

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 中,按更新主屏幕 FloatingActionButton 会调用 updateHeadline(widget.article),将当前查看的新闻推送到主屏幕 widget。

在 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 widget 预览,显示占位标题和占位说明

  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:):使用 .atEnd 重新加载政策将快照条目封装在 Timeline 中,以便 WidgetKit 在收到请求时刷新 widget。

读取 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) 检索共享偏好设置,并在 widget 的 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 主屏幕 widget,显示“回顾 Flutter Forward”标题和摘要

6. 在 iOS 主屏幕微件中使用自定义字体

您的 Flutter 应用使用自定义 Chewy 字体系列 (fonts/Chewy-Regular.ttf) 作为文章子标题。在 iOS 上,Flutter 会将资源打包到主应用包内的 Frameworks/App.framework/flutter_assets 中。您的 iOS widget 扩展程序可以访问该共享 bundle 目录,并向 Core Text 注册字体,以便您的 SwiftUI widget 与 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 资源包

由于该 widget 在 .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 重新运行 widget 目标。iOS 主屏幕 widget 现在会以 Chewy 字体呈现文章标题:

iOS 主屏幕微件,其中文章标题采用自定义 Chewy 字体

7. 将 Flutter widget 渲染为图片

如果要在 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 widget 捕获为 PNG (lib/article_screen.dart)

在 _ArticleScreenState 中,_globalKey 附加到封装 const LineChart() 的 Center widget,以便应用可以读取其渲染的 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 应用和平台 widget 目标。在应用中打开一篇文章页面,然后点按更新主屏幕。主屏幕微件会更新,显示文章标题、摘要和渲染的 LineChart 图片:

iOS 模拟器,其中显示了文章详情屏幕,以及显示了标题、说明和折线图的主屏幕 widget

8. 恭喜!

恭喜!您已使用 Google Antigravity 和 home_widget 为 iOS 和 Android 构建了主屏幕 widget,并将其连接到 Flutter 应用。

您构建的内容

  • 一个包含两个屏幕的 Flutter 新闻应用,使用了 material_ui、自定义 Chewy 排版和 CustomPainter 折线图。
  • 原生 iOS(WidgetKit/SwiftUI)和 Android(AppWidgetProvider/RemoteViews)主屏幕微件。
  • Dart、UserDefaults(使用 iOS 应用组)和 SharedPreferences 之间的键值对数据同步。
  • iOS 微件扩展程序中的跨软件包自定义字体注册。
  • 在 iOS 和 Android 主屏幕 widget 中显示的屏幕外 Flutter widget 光栅化 (HomeWidget.renderFlutterWidget)。

后续步骤

链接到 Flutter 应用中的内容

您可能希望在用户点按主屏幕微件时,将其引导至 Flutter 应用中的特定屏幕,例如打开所显示标题的完整文章。您可以使用 home_widget 提供的 widgetClicked 流来检测 widget 启动情况,并将深层链接 URI 传递到您的应用中。如需了解详情,请参阅 docs.flutter.dev 上的深层链接文档。

正在后台更新微件

在此 Codelab 中,您通过应用内按钮触发了主屏幕 widget 更新。在生产环境中,您可以使用 workmanager 插件和 home_widget 安排定期后台更新。如需了解详情,请参阅 home_widget 的后台更新文档。

在 iOS 上,您还可以直接通过 WidgetKit 的 TimelineProvider 安排定期时间轴刷新或后台网络请求。请参阅 Apple 的“Keeping a widget up to date”文档。

资源