1. 简介
什么是微件?
对于 Flutter 开发者,微件的常见定义是指使用 Flutter 框架创建的界面组件。在本 Codelab 中,微件是指迷你版应用,让用户无需打开应用即可查看应用的信息。在 Android 上,微件位于主屏幕上。在 iOS 设备上,用户可以将其添加到主屏幕、锁定屏幕或“今天”视图中。
widget 的复杂程度如何?
大多数主屏幕微件都很简单。这类控件可能包含基本文本、简单图形,在 Android 设备上则可能包含基本控件。Android 和 iOS 都会限制您可以使用的界面组件和功能。
为 widget 创建界面
由于这些界面限制,您无法使用 Flutter 框架直接绘制主屏幕 widget 的界面。相反,您可以将使用 Jetpack Compose 或 SwiftUI 等平台框架创建的 widget 添加到 Flutter 应用。此 Codelab 讨论了如何在应用和 widget 之间共享资源,以避免重写复杂的界面。
构建内容
在此 Codelab 中,您将使用 home_widget 软件包(可让用户阅读文章)在 Android 和 iOS 上为一个简单的 Flutter 应用构建主屏幕 widget。您的微件将:
- 显示来自 Flutter 应用的数据。
- 使用从 Flutter 应用共享的字体资源显示文本。
- 显示渲染的 Flutter widget 的图片。
此 Flutter 应用包含两个屏幕(或路由):
- 第一个页面会显示新闻报道列表,其中包含标题和说明。
- 第二个窗格会显示完整的报道,以及使用
CustomPaint
创建的图表。
.
学习内容
- 如何在 iOS 和 Android 上创建主屏幕微件。
- 如何使用 home_widget 软件包在主屏幕 widget 和 Flutter 应用之间共享数据。
- 如何减少需要重写的代码量。
- 如何从 Flutter 应用更新主屏幕 widget。
2. 设置您的开发环境
对于这两个平台,您都需要 Flutter SDK 和 IDE。您可以使用自己偏好的 IDE 来处理 Flutter。这可以是带有 Dart Code 和 Flutter 扩展程序的 Visual Studio Code,或者安装了 Flutter 和 Dart 插件的 Android Studio 或 IntelliJ。
如需创建 iOS 主屏幕微件,请执行以下操作:
- 您可以在实体 iOS 设备或 iOS 模拟器上运行此 Codelab。
- 您必须使用 Xcode IDE 配置 macOS 系统。这会安装构建 iOS 版本应用所需的编译器。
如需创建 Android 主屏幕微件,请执行以下操作:
- 您可以在实体 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 中每个步骤的完整项目代码。
打开起始应用
打开 flutter-codelabs/homescreen_codelab/step_03
目录并进入首选 IDE。
安装软件包
所有必需的软件包都已添加到项目的 pubspec.yaml 文件中。如需检索项目依赖项,请运行以下命令:
$ flutter pub get
3. 添加基本的主屏幕微件
首先,使用原生平台工具添加主屏幕微件。
创建基本的 iOS 主屏幕 widget
向 Flutter iOS 应用添加附加应用信息类似于向 SwiftUI 或 UIKit 应用添加附加应用信息:
- 从 Flutter 项目目录在终端窗口中运行
open ios/Runner.xcworkspace
。或者,右键点击 VSCode 中的 ios 文件夹,然后选择在 Xcode 中打开。这会在您的 Flutter 项目中打开默认的 Xcode 工作区。 - 从菜单中选择 File → New → Target。此操作会向项目中添加一个新目标。
- 系统会显示模板列表。选择 Widget Extension。
- 输入“NewsWidgets”输入此 widget 的 Product Name 框中。取消选中 Include Live Activity 和 Include Configuration Intent 复选框。
检查示例代码
当您添加新的目标时,Xcode 会根据您选择的模板生成示例代码。有关生成的代码和 WidgetKit 的详细信息,请参阅 Apple 的附加应用信息文档
调试和测试示例 widget
- 首先,更新您的 Flutter 应用的配置。当您在 Flutter 应用中添加新软件包,并计划从 Xcode 在项目中运行目标时,必须执行此操作。如需更新应用的配置,请在 Flutter 应用目录中运行以下命令:
$ flutter build ios --config-only
- 点击 Runner 以显示目标列表。选择您刚刚创建的 widget 目标 NewsWidgets,然后点击 Run。更改 iOS 微件代码时,从 Xcode 运行微件目标。
- 模拟器或设备屏幕应显示一个基本的主屏幕 widget。如果您没有看到此图标,可以将其添加到屏幕上。点击并按住主屏幕,然后点击左上角的 +。
- 搜索应用的名称。对于此 Codelab,请搜索“主屏幕微件”
- 添加主屏幕微件后,它应该会显示提供时间的简单文本。
创建基本的 Android widget
- 如需在 Android 中添加主屏幕微件,请在 Android Studio 中打开项目的 build 文件。您可以在 android/build.gradle 下找到此文件。或者,右键点击 VSCode 中的 android 文件夹,然后选择 Open in Android Studio。
- 项目构建完成后,在左上角找到应用目录。将新的主屏幕微件添加到此目录中。右键点击该目录,选择 New ->微件 ->应用微件。
- Android Studio 会显示一个新表单。添加有关主屏幕微件的基本信息,包括其类名称、位置、大小和源语言
对于此 Codelab,请设置以下值:
- Class Name 框添加到 NewsWidget
- 最小宽度(单元格)下拉菜单设置为 3
- 最小高度(单元格)下拉菜单设置为 3
检查示例代码
在您提交表单时,Android Studio 会创建并更新多个文件。下表列出了与此 Codelab 相关的更改
操作 | 目标文件 | 更改 |
更新 |
| 添加一个用于注册 NewsWidget 的新接收器。 |
创建 |
| 定义主屏幕 widget 界面。 |
创建 |
| 定义主屏幕 widget 配置。您可以在此文件中调整微件的尺寸或名称。 |
创建 |
| 包含用于向主屏幕 widget 添加功能的 Kotlin 代码。 |
您可以在此 Codelab 中找到关于这些文件的更多详细信息。
调试和测试示例 widget
现在,运行应用并查看主屏幕 widget。构建应用后,前往 Android 设备的应用选择界面,长按此 Flutter 项目的图标。从弹出式菜单中选择 Widgets。
Android 设备或模拟器会显示 Android 的默认主屏幕微件。
4. 将数据从 Flutter 应用发送到主屏幕 widget
您可以自定义您创建的基本主屏幕微件。更新主屏幕微件,以显示新闻报道的标题和摘要。以下屏幕截图显示了主屏幕 widget 的示例,其中显示了标题和摘要。
如需在应用与主屏幕微件之间传递数据,您需要编写 Dart 和原生代码。本部分将此过程分为三个部分:
- 在 Flutter 应用中编写 Android 和 iOS 都可以使用的 Dart 代码
- 添加原生 iOS 功能
- 添加原生 Android 功能
使用 iOS 应用组
如需在 iOS 父级应用和微件扩展程序之间共享数据,这两个目标必须属于同一应用组。如需详细了解应用组,请参阅 Apple 的应用组文档。
更新您的软件包标识符:
在 Xcode 中,前往目标的设置。在签署和功能标签页中,检查您的团队和软件包标识符是否已设置。
将应用组添加到 Xcode 中的 Runner 目标和 NewsWidgetExtension 目标:
选择 + 功能 ->应用组,然后添加新的应用组。对 Runner(父应用)目标和 widget 目标重复上述操作。
添加 Dart 代码
iOS 和 Android 应用都可以通过几种不同的方式与 Flutter 应用共享数据。如需与这些应用通信,请使用设备的本地 key/value
商店。iOS 将此商店称为 UserDefaults
,Android 将此商店称为 SharedPreferences
。home_widget 软件包会封装这些 API 以简化将数据保存到任一平台的过程,并使主屏幕 widget 能够提取更新后的数据。
标题和广告内容描述数据来自 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
的值。该函数还会通知原生平台可以检索和呈现主屏幕微件的新数据。
修改 floatActionButton
在按下 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 更新主屏幕微件,请使用 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)
}
}
}
修改提供程序,告知主屏幕 widget 何时以及如何更新
将现有 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 的结构和布局。选择右上角的 Code,然后将该文件的内容替换为以下代码:
android/app/res/layout/news_widget.xml
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/widget_container"
style="@style/Widget.Android.AppWidget.Container"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:background="@android:color/white"
android:theme="@style/Theme.Android.AppWidgetContainer">
<TextView
android:id="@+id/headline_title"
style="@style/Widget.Android.AppWidget.InnerView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentStart="true"
android:layout_alignParentLeft="true"
android:layout_marginStart="8dp"
android:layout_marginLeft="8dp"
android:background="@android:color/white"
android:text="Title"
android:textSize="20sp"
android:textStyle="bold" />
<TextView
android:id="@+id/headline_description"
style="@style/Widget.Android.AppWidget.InnerView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@+id/headline_title"
android:layout_alignParentStart="true"
android:layout_alignParentLeft="true"
android:layout_marginStart="8dp"
android:layout_marginLeft="8dp"
android:layout_marginTop="4dp"
android:background="@android:color/white"
android:text="Title"
android:textSize="16sp" />
</RelativeLayout>
此 XML 定义了两个文本视图,一个用于文章标题,另一个用于文章说明。这些文本视图还定义了样式。在本 Codelab 中,您将回到此文件。
更新 NewsWidget 功能
打开 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
。您的主屏幕微件应该会更新为文章标题。
5. 在 iOS 主屏幕 widget 中使用 Flutter 应用自定义字体
到目前为止,您已将主屏幕 widget 配置为读取 Flutter 应用提供的数据。Flutter 应用包含您可能想在主屏幕 widget 中使用的自定义字体。您可以在 iOS 主屏幕微件中使用自定义字体。Android 设备不支持在主屏幕微件中使用自定义字体。
更新 iOS 代码
Flutter 将其资源存储在 iOS 应用的 mainBundle 中。您可以通过主屏幕微件代码访问此 bundle 中的资源。
在 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)
}
}
...
}
现在,当您运行主屏幕微件时,它会使用自定义字体作为标题,如下图所示:
6. 将 Flutter widget 渲染为图像
在本部分中,您会将 Flutter 应用中的图表显示为主屏幕 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
的中心。
lib/article_screen.dart
class _ArticleScreenState extends State<ArticleScreen> {
...
Center(
// New: Add this key
key: _globalKey,
child: const LineChart(),
),
...
}
- 将 widget 保存为图片
当用户点击 floatingActionButton
时,系统会调用 renderFlutterWidget
方法。该方法将生成的 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
属性包含用户设备上的主屏幕 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) 目标和扩展程序目标。若要查看图片,请导航至应用中的某篇文章页面,然后按按钮更新主屏幕微件。
更新 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>
这个新代码会向主屏幕 widget 添加一张图片,该图片(目前)会显示一个通用星形图标。将此星形图标替换为您在 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 应用创建了主屏幕微件!
链接到 Flutter 应用中的内容
您可能希望根据用户点击的位置将用户定向到应用中的特定页面。例如,在此 Codelab 的新闻应用中,您可能希望用户看到显示标题的新闻报道。
此功能不在此 Codelab 的讨论范围内。您可以找到相关示例,了解如何使用 home_widget 软件包提供的流来识别应用从主屏幕 widget 启动,并通过网址从主屏幕 widget 发送消息。如需了解详情,请参阅 docs.flutter.dev 上的深层链接文档 。
在后台更新 widget
在此 Codelab 中,您使用按钮触发了主屏幕 widget 的更新。虽然这对于测试来说是合理的,但在正式版代码中,您可能希望应用在后台更新主屏幕 widget。您可以使用 workmanager 插件创建后台任务,以更新主屏幕 widget 所需的资源。如需了解详情,请参阅 home_widget 软件包中的后台更新部分。
对于 iOS,您还可以让主屏幕微件发出网络请求以更新其界面。如需控制该请求的条件或频率,请使用时间轴。要了解更多关于使用时间轴的信息,请参阅 Apple 的“保持小工具最新”文档。