1. 简介
什么是 widget?
对于 Flutter 开发者来说,widget 的常见定义是指使用 Flutter 框架创建的界面组件。在此 Codelab 中,微件是指应用的迷你版本,可在不打开应用的情况下提供应用信息的视图。在 Android 上,微件位于主屏幕上。在 iOS 设备上,它们可以添加到主屏幕、锁定屏幕或“今天”视图中。

Widget 可以有多复杂?
大多数主屏幕 widget 都很简单。它们可能包含基本文本、简单图形,或者在 Android 上包含基本控件。Android 和 iOS 都会限制您可以使用的界面组件和功能。

为 widget 创建界面
由于存在这些界面限制,您无法使用 Flutter 框架直接绘制主屏幕 widget 的界面。不过,您可以将使用 Jetpack Compose 或 SwiftUI 等平台框架创建的 widget 添加到 Flutter 应用中。此 Codelab 讨论了在应用和 widget 之间共享资源的示例,以避免重写复杂的界面。
构建内容
在此 Codelab 中,您将使用 home_widget 软件包为简单的 Flutter 应用在 Android 和 iOS 上构建主屏幕 widget,以便用户阅读文章。您的 widget 将:
- 显示来自 Flutter 应用的数据。
- 使用从 Flutter 应用共享的字体资源显示文本。
- 显示已渲染的 Flutter widget 的图片。

此 Flutter 应用包含两个界面(或路由):
- 第一个显示包含标题和广告内容描述的新闻文章列表。
- 第二张图片显示了包含使用
CustomPaint创建的图表的完整文章。
。

学习内容
- 如何在 iOS 和 Android 上创建主屏幕 widget。
- 如何使用 home_widget 软件包在主屏幕 widget 和 Flutter 应用之间共享数据。
- 如何减少需要重写的代码量。
- 如何从 Flutter 应用更新主屏幕 widget。
2. 设置您的开发环境
对于这两个平台,您都需要 Flutter SDK 和 IDE。您可以使用自己喜欢的 IDE 来处理 Flutter 项目。可以是包含 Dart Code 和 Flutter 扩展程序的 Visual Studio Code,也可以是安装了 Flutter 和 Dart 插件的 Android Studio 或 IntelliJ。
如需创建 iOS 主屏幕 widget,请执行以下操作:
- 您可以在 iOS 设备或 iOS 模拟器上运行此 Codelab。
- 您必须使用 Xcode IDE 配置 macOS 系统。此命令会安装构建应用的 iOS 版本所需的编译器。
如需创建 Android 主屏幕 widget,请执行以下操作:
- 您可以在实体 Android 设备或 Android 模拟器上运行此 Codelab。
- 您必须使用 Android Studio 配置开发系统。此命令会安装构建应用的 Android 版本所需的编译器。
获取起始代码
从 GitHub 下载项目的初始版本
在命令行中,将 GitHub 代码库克隆到 flutter-codelabs 目录:
$ git clone https://github.com/flutter/codelabs.git flutter-codelabs
克隆代码库后,您可以在 flutter-codelabs/homescreen_codelab 目录中找到此 Codelab 的代码。该目录包含此 Codelab 中每个步骤的完整项目代码。
打开起始应用
在您的首选 IDE 中打开 flutter-codelabs/homescreen_codelab/step_03 目录。
安装软件包
所有必需的软件包都已添加到项目的 pubspec.yaml 文件中。如需检索项目依赖项,请运行以下命令:
$ flutter pub get
3. 添加基本的主屏幕 widget
首先,使用原生平台工具添加主屏幕 widget。
创建基本的 iOS 主屏幕 widget
向 Flutter iOS 应用添加应用扩展服务与向 SwiftUI 或 UIKit 应用添加应用扩展服务类似:
- 在终端窗口中,从您的 Flutter 项目目录运行
open ios/Runner.xcworkspace。或者,您也可以在 VSCode 中右键点击 ios 文件夹,然后选择 Open in Xcode。这会在您的 Flutter 项目中打开默认的 Xcode 工作区。 - 从菜单中选择 File(文件)→ New(新建)→ Target(目标)。此操作会向项目添加新目标。
- 系统会显示模板列表。选择微件扩展程序。
- 在此 widget 的产品名称框中输入“NewsWidgets”。同时清除包含实时活动和包含配置 intent 复选框。
检查示例代码
添加新目标时,Xcode 会根据您选择的模板生成示例代码。如需详细了解生成的代码和 WidgetKit,请参阅 Apple 的应用扩展文档 。
调试并测试示例 widget
- 首先,更新 Flutter 应用的配置。当您在 Flutter 应用中添加新软件包并计划从 Xcode 运行项目中的目标时,必须执行此操作。如需更新应用的配置,请在 Flutter 应用目录中运行以下命令:
$ flutter build ios --config-only
- 点击 Runner 以显示目标列表。选择您刚刚创建的 widget 目标平台 NewsWidgets,然后点击 Run。更改 iOS widget 代码时,请从 Xcode 运行 widget 目标。

- 模拟器或设备屏幕应显示一个基本的主屏幕 widget。如果您没有看到该图标,可以将其添加到屏幕上。在主屏幕上点击并按住,然后点击左上角的 +。

- 搜索应用的名称。在本 Codelab 中,搜索“主屏幕 widget”

- 添加主屏幕 widget 后,它应显示简单的文本,其中包含时间。
创建基本的 Android widget
- 如需在 Android 中添加主屏幕 widget,请在 Android Studio 中打开项目的 build 文件。您可以在 android/build.gradle 中找到此文件。或者,您也可以在 VSCode 中右键点击 android 文件夹,然后选择 Open in Android Studio。
- 项目构建完成后,找到左上角的应用目录。将新的主屏幕 widget 添加到此目录。右键点击该目录,然后依次选择 New -> Widget -> App Widget。

- Android Studio 会显示一个新表单。添加有关主屏幕 widget 的基本信息,包括其类名称、放置位置、大小和源语言
对于此 Codelab,请设置以下值:
- 类名称框中
- 将最小宽度(单元格)下拉菜单设置为 3
- 最小高度(单元格)下拉菜单中选择 3
检查示例代码
提交表单后,Android Studio 会创建并更新多个文件。下表列出了与此 Codelab 相关的更改
操作 | 目标文件 | 更改 |
更新 |
| 添加了一个用于注册 NewsWidget 的新接收器。 |
创建 |
| 定义主屏幕 widget 界面。 |
创建 |
| 定义主屏幕 widget 配置。您可以在此文件中调整 widget 的尺寸或名称。 |
创建 |
| 包含用于向主屏幕 widget 添加功能的 Kotlin 代码。 |
您可以在本 Codelab 中详细了解这些文件。
调试并测试示例 widget
现在,运行您的应用,并查看主屏幕 widget。构建应用后,前往 Android 设备的应用选择界面,然后长按相应 Flutter 项目的图标。从弹出式菜单中选择微件。

Android 设备或模拟器会显示 Android 的默认主屏幕 widget。
4. 从 Flutter 应用向主屏幕 widget 发送数据
您可以自定义创建的基本主屏幕 widget。更新主屏幕 widget 以显示新闻报道的标题和摘要。以下屏幕截图显示了主屏幕 widget 显示标题和摘要的示例。

如需在应用和主屏幕 widget 之间传递数据,您需要编写 Dart 和原生代码。本部分将此流程分为三个部分:
- 在 Flutter 应用中编写 Android 和 iOS 都能使用的 Dart 代码
- 添加原生 iOS 功能
- 添加原生 Android 功能
使用 iOS 应用群组
如需在 iOS 父应用和 widget 扩展程序之间共享数据,这两个目标必须属于同一应用群组。如需详细了解应用组,请参阅 Apple 的应用组文档。
更新您的软件包标识符:
在 Xcode 中,前往目标的设置。在 Signing & Capabilities(签名和功能)标签页中,检查您的团队和软件包标识符是否已设置。
在 Xcode 中,将应用组添加到 Runner 目标和 NewsWidgetExtension 目标两者中:
依次选择 + Capability -> App Groups,然后添加新的应用组。针对 Runner(父应用)目标和 widget 目标重复上述操作。

添加 Dart 代码
iOS 和 Android 应用可以通过多种不同的方式与 Flutter 应用共享数据。若要与这些应用通信,请利用设备的本地 key/value 存储区。iOS 将此存储区称为 UserDefaults,而 Android 将其称为 SharedPreferences。home_widget 软件包封装了这些 API,可简化将数据保存到任一平台的过程,并使主屏幕小组件能够提取更新后的数据。

标题和说明数据来自 news_data.dart 文件。此文件包含模拟数据和 NewsArticle 数据类。
lib/news_data.dart
class NewsArticle {
final String title;
final String description;
final String? articleText;
NewsArticle({
required this.title,
required this.description,
this.articleText = loremIpsum,
});
}
更新标题和说明值
如需添加从 Flutter 应用更新主屏幕 widget 的功能,请前往 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 的值。该函数还会通知原生平台可以检索并渲染主屏幕 widget 的新数据。
修改 floatingActionButton
在按下 floatingActionButton 时调用 updateHeadline 函数,如下所示:
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'),
),
...
在此次更改后,当用户在文章页面中按 Update Headline 按钮时,主屏幕 widget 详细信息会更新。
更新 iOS 代码以显示文章数据
如需更新 iOS 版主屏幕 widget,请使用 Xcode。
在 Xcode 中打开 NewsWidgets.swift 文件:
配置 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 结构体定义了在更新时要传递到主屏幕 widget 的传入数据。TimelineEntry 类型需要一个日期参数。如需详细了解 TimelineEntry 协议,请参阅 Apple 的 TimelineEntry 文档。
修改View (用于显示内容)
修改主屏幕微件,使其显示新闻报道的标题和说明,而不是日期。如需在 SwiftUI 中显示文本,请使用 Text 视图。如需在 SwiftUI 中将视图堆叠在一起,请使用 VStack 视图。
将生成的 NewsWidgetEntryView 视图替换为以下代码:
ios/NewsWidgets/NewsWidgets.swift
//View that holds the contents of the widget
struct NewsWidgetsEntryView : View {
var entry: Provider.Entry
var body: some View {
VStack {
Text(entry.title)
Text(entry.description)
}
}
}
修改提供程序以告知主屏幕微件何时以及如何更新
将现有的 Provider 替换为以下代码。然后,将 <YOUR APP GROUP> 替换为您的应用组标识符:
ios/NewsWidgets/NewsWidgets.swift
struct Provider: TimelineProvider {
// Placeholder is used as a placeholder when the widget is first displayed
func placeholder(in context: Context) -> NewsArticleEntry {
// Add some placeholder title and description, and get the current date
NewsArticleEntry(date: Date(), title: "Placeholder Title", description: "Placeholder description")
}
// Snapshot entry represents the current time and state
func getSnapshot(in context: Context, completion: @escaping (NewsArticleEntry) -> ()) {
let entry: NewsArticleEntry
if context.isPreview{
entry = placeholder(in: context)
}
else{
// Get the data from the user defaults to display
let userDefaults = UserDefaults(suiteName: <YOUR APP GROUP>)
let title = userDefaults?.string(forKey: "headline_title") ?? "No Title Set"
let description = userDefaults?.string(forKey: "headline_description") ?? "No Description Set"
entry = NewsArticleEntry(date: Date(), title: title, description: description)
}
completion(entry)
}
// getTimeline is called for the current and optionally future times to update the widget
func getTimeline(in context: Context, completion: @escaping (Timeline<Entry>) -> ()) {
// This just uses the snapshot function you defined earlier
getSnapshot(in: context) { (entry) in
// atEnd policy tells widgetkit to request a new entry after the date has passed
let timeline = Timeline(entries: [entry], policy: .atEnd)
completion(timeline)
}
}
}
上一个代码中的 Provider 符合 TimelineProvider。Provider 有三种不同的方法:
- 当用户首次预览主屏幕 widget 时,
placeholder方法会生成一个占位条目。

getSnapshot方法从用户默认设置中读取数据,并生成当前时间的条目。getTimeline方法会返回时间轴条目。如果您有可预测的时间点来更新内容,此功能会很有帮助。此 Codelab 使用 getSnapshot 函数来获取当前状态。.atEnd方法会告知主屏幕 widget 在当前时间过后刷新数据。
将 NewsWidgets_Previews 注释掉
使用预览版不在本 Codelab 的范围之内。如需详细了解如何预览 SwiftUI 主屏幕 widget,请参阅 Apple 关于调试 widget 的文档。
保存所有文件,然后重新运行应用和 widget 目标。
再次运行目标,以验证应用和主屏幕微件是否正常运行。
- 在 Xcode 中选择应用架构,以运行应用目标。
- 在 Xcode 中选择扩展程序架构,以运行扩展程序目标。
- 前往应用中的文章页面。
- 点击相应按钮即可更新标题。主屏幕 widget 也应更新标题。
更新 Android 代码
添加主屏幕 widget XML。
在 Android Studio 中,更新在上一步中生成的文件。打开 res/layout/news_widget.xml 文件。它定义了主屏幕 widget 的结构和布局。选择右上角的代码,然后将该文件的内容替换为以下代码:
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 功能
打开 NewsWidget.kt Kotlin 源代码文件。此文件包含一个名为 NewsWidget 的生成类,该类扩展了 AppWidgetProvider 类。
NewsWidget 类包含其父类中的三个方法。您将修改 onUpdate 方法。Android 会以固定的时间间隔为 widget 调用此方法。
将 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 来更改主屏幕 widget 上显示的文本。
测试更新
测试应用,确保主屏幕 widget 会随新数据更新。如需更新数据,请使用文章页面上的更新主屏幕 图标 FloatingActionButton。主屏幕 widget 应会更新为显示文章标题。

5. 在 iOS 主屏幕微件中使用 Flutter 应用自定义字体
到目前为止,您已将主屏幕 widget 配置为读取 Flutter 应用提供的数据。Flutter 应用包含您可能想在主屏幕 widget 中使用的自定义字体。您可以在 iOS 主屏幕 widget 中使用自定义字体。Android 设备不支持在主屏幕 widget 中使用自定义字体。
更新 iOS 代码
Flutter 将其资源存储在 iOS 应用的 mainBundle 中。您可以通过主屏幕 widget 代码访问此软件包中的资源。
在 NewsWidgets.swift 文件中的 NewsWidgetsEntryView 结构体中,进行以下更改
创建一个辅助函数来获取 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
}
...
}
使用自定义字体文件的网址注册字体。
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)
}
}
...
}
运行主屏幕 widget 时,它现在会使用自定义字体作为标题,如下所示:

6. 将 Flutter widget 渲染为图片
在本部分中,您将展示 Flutter 应用中的图表,并将其作为主屏幕 widget。
此 widget 提供的挑战比您在主屏幕上显示的文字更具挑战性。将 Flutter 图表显示为图片要比尝试使用原生界面组件重新创建它容易得多。
对主屏幕 widget 进行编码,以将 Flutter 图表渲染为 PNG 文件。主屏幕微件可以显示该图片。
编写 Dart 代码
在 Dart 端,添加来自 home_widget 软件包的 renderFlutterWidget 方法。此方法可获取 widget、文件名和密钥。它会返回 Flutter widget 的图片,并将其保存到共享容器中。在代码中提供映像名称,并确保主屏幕 widget 可以访问容器。key 将完整的文件路径作为字符串保存在设备的本地存储空间中。这样,即使名称在 Dart 代码中发生更改,主屏幕 widget 也能找到相应文件。
在此 Codelab 中,lib/article_screen.dart 文件中的 LineChart 类表示图表。其 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!),
],
),
);
}
}
此示例对 _ArticleScreenState 类进行了三项更改。
创建 GlobalKey
GlobalKey 可获取特定 widget 的上下文,这是获取该 widget 大小所必需的。
lib/article_screen.dart
class _ArticleScreenState extends State<ArticleScreen> {
// New: add this GlobalKey
final _globalKey = GlobalKey();
...
}
添加了 imagePath
imagePath 属性用于存储 Flutter widget 渲染的图片的位置。
lib/article_screen.dart
class _ArticleScreenState extends State<ArticleScreen> {
...
// New: add this imagePath
String? imagePath;
...
}
将键添加到要呈现的 widget
_globalKey 包含渲染到图片中的 Flutter widget。在本例中,Flutter widget 是包含 LineChart 的 Center。
lib/article_screen.dart
class _ArticleScreenState extends State<ArticleScreen> {
...
Center(
// New: Add this key
key: _globalKey,
child: const LineChart(),
),
...
}
- 将 widget 保存为图片
当用户点击 floatingActionButton 时,系统会调用 renderFlutterWidget 方法。该方法会将生成的 PNG 文件以“screenshot”为名保存到共享容器目录中。该方法还会将图片的完整路径保存为设备存储空间中的文件名键。
lib/article_screen.dart
class _ArticleScreenState extends State<ArticleScreen> {
...
floatingActionButton: FloatingActionButton.extended(
onPressed: () async {
if (_globalKey.currentContext != null) {
var path = await HomeWidget.renderFlutterWidget(
LineChart(),
fileName: 'screenshot',
key: 'filename',
logicalSize: _globalKey.currentContext!.size,
pixelRatio:
MediaQuery.of(_globalKey.currentContext!).devicePixelRatio,
);
setState(() {
imagePath = path as String?;
});
}
updateHeadline(widget.article);
},
...
}
更新 iOS 代码
对于 iOS,请更新代码以从存储空间获取文件路径,并使用 SwiftUI 将文件显示为图片。
打开 NewsWidgets.swift 文件,进行以下更改:
将 filename 和 displaySize 添加到 NewsArticleEntry 结构体
filename 属性包含表示图片文件路径的字符串。displaySize 属性用于保存用户设备上主屏幕 widget 的大小。主屏幕 widget 的大小来自 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)
}
从 getSnapshot 中的 userDefaults 获取文件名
这会在主屏幕 widget 更新时将 filename 变量设置为 userDefaults 存储空间中的 filename 值。
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())
}
...
}
在 NewsWidgetsEntryView 的正文中使用 ChartImage
将 ChartImage 视图添加到 NewsWidgetsEntryView 的正文中,以在主屏幕 widget 中显示 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
}
测试更改
如需测试更改,请在 Xcode 中重新运行 Flutter 应用 (Runner) 目标和扩展程序目标。如需查看图片,请前往应用中的某个文章页面,然后按相应按钮更新主屏幕 widget。

更新 Android 代码
Android 代码的功能与 iOS 代码相同。
- 打开
android/app/res/layout/news_widget.xml文件。它包含主屏幕 widget 的界面元素。将其内容替换为以下代码:
android/app/res/layout/news_widget.xml
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/widget_container"
style="@style/Widget.Android.AppWidget.Container"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:background="@android:color/white"
android:theme="@style/Theme.Android.AppWidgetContainer">
<TextView
android:id="@+id/headline_title"
style="@style/Widget.Android.AppWidget.InnerView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentStart="true"
android:layout_alignParentLeft="true"
android:layout_marginStart="8dp"
android:layout_marginLeft="8dp"
android:background="@android:color/white"
android:text="Title"
android:textSize="20sp"
android:textStyle="bold" />
<TextView
android:id="@+id/headline_description"
style="@style/Widget.Android.AppWidget.InnerView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@+id/headline_title"
android:layout_alignParentStart="true"
android:layout_alignParentLeft="true"
android:layout_marginStart="8dp"
android:layout_marginLeft="8dp"
android:layout_marginTop="4dp"
android:background="@android:color/white"
android:text="Title"
android:textSize="16sp" />
<!--New: add this image view -->
<ImageView
android:id="@+id/widget_image"
android:layout_width="200dp"
android:layout_height="200dp"
android:layout_below="@+id/headline_description"
android:layout_alignBottom="@+id/headline_title"
android:layout_alignParentStart="true"
android:layout_alignParentLeft="true"
android:layout_marginStart="8dp"
android:layout_marginLeft="8dp"
android:layout_marginTop="6dp"
android:layout_marginBottom="-134dp"
android:layout_weight="1"
android:adjustViewBounds="true"
android:background="@android:color/white"
android:scaleType="fitCenter"
android:src="@android:drawable/star_big_on"
android:visibility="visible"
tools:visibility="visible" />
</RelativeLayout>
此新代码会向主屏幕小组件添加图片,该图片(目前)会显示一个通用星形图标。将此星形图标替换为在 Dart 代码中保存的图片。
- 打开
NewsWidget.kt文件。将其内容替换为以下代码:
android/app/java/com.mydomain.homescreen_widgets/NewsWidget.kt
// Import will depend on App ID.
package com.mydomain.homescreen_widgets
import android.appwidget.AppWidgetManager
import android.appwidget.AppWidgetProvider
import android.content.Context
import android.graphics.Bitmap
import android.graphics.BitmapFactory
import android.widget.RemoteViews
import java.io.File
import es.antonborri.home_widget.HomeWidgetPlugin
/**
* Implementation of App Widget functionality.
*/
class NewsWidget : AppWidgetProvider() {
override fun onUpdate(
context: Context,
appWidgetManager: AppWidgetManager,
appWidgetIds: IntArray,
) {
for (appWidgetId in appWidgetIds) {
val widgetData = HomeWidgetPlugin.getData(context)
val views = RemoteViews(context.packageName, R.layout.news_widget).apply {
val title = widgetData.getString("headline_title", null)
setTextViewText(R.id.headline_title, title ?: "No title set")
val description = widgetData.getString("headline_description", null)
setTextViewText(R.id.headline_description, description ?: "No description set")
// New: Add the section below
// Get chart image and put it in the widget, if it exists
val imageName = widgetData.getString("filename", null)
val imageFile = File(imageName)
val imageExists = imageFile.exists()
if (imageExists) {
val myBitmap: Bitmap = BitmapFactory.decodeFile(imageFile.absolutePath)
setImageViewBitmap(R.id.widget_image, myBitmap)
} else {
println("image not found!, looked @: ${imageName}")
}
// End new code
}
appWidgetManager.updateAppWidget(appWidgetId, views)
}
}
}
此 Dart 代码使用 filename 键将屏幕截图保存到本地存储空间。它还会获取图片的完整路径,并从中创建一个 File 对象。如果图片存在,Dart 代码会将主屏幕 widget 中的图片替换为新图片。
- 重新加载应用,然后前往文章界面。按更新主屏幕。主屏幕 widget 会显示图表。
7. 后续步骤
恭喜!
恭喜,您已成功为 Flutter iOS 和 Android 应用创建了主屏幕 widget!
链接到 Flutter 应用中的内容
您可能希望根据用户点击的位置将用户引导至应用中的特定页面。例如,在此 Codelab 的新闻应用中,您可能希望用户看到所显示标题对应的新闻报道。
此功能不在本 Codelab 的范围内。您可以找到使用 home_widget 软件包提供的 stream 来识别从主屏幕 widget 启动的应用并通过网址从主屏幕 widget 发送消息的示例。如需了解详情,请参阅 docs.flutter.dev 上的深层链接文档 。
在后台更新 widget
在此 Codelab 中,您使用按钮触发了主屏幕 widget 的更新。虽然这对于测试来说是合理的,但在生产代码中,您可能希望应用在后台更新主屏幕 widget。您可以使用 workmanager 插件创建后台任务,以更新主屏幕 widget 所需的资源。如需了解详情,请参阅 home_widget 软件包中的后台更新部分。
对于 iOS,您还可以让主屏幕 widget 发出网络请求来更新其界面。如需控制该请求的条件或频次,请使用时间轴。如需详细了解如何使用时间轴,请参阅 Apple 的“保持 widget 最新状态”文档。