1. Введение
Что такое виджеты?
Для разработчиков Flutter общепринятое определение виджета относится к компонентам пользовательского интерфейса, созданным с использованием фреймворка Flutter. В контексте этого практического занятия виджет — это мини-версия приложения, которая предоставляет доступ к информации приложения без его открытия . На Android виджеты размещаются на главном экране. На iOS их можно добавить на главный экран, экран блокировки или в раздел «Сегодня».


Насколько сложным может быть виджет?
Большинство виджетов на главном экране просты. Они могут содержать простой текст, несложную графику или, на Android, базовые элементы управления. И Android, и iOS ограничивают набор компонентов и функций пользовательского интерфейса, которые вы можете использовать.


Создайте пользовательский интерфейс для виджетов.
Из-за этих ограничений пользовательского интерфейса вы не можете напрямую отрисовывать интерфейс виджета главного экрана с помощью фреймворка Flutter. Вместо этого вы можете добавить в свое приложение Flutter виджеты, созданные с помощью таких платформенных фреймворков, как Jetpack Compose или SwiftUI. В этом практическом занятии рассматриваются примеры совместного использования ресурсов между вашим приложением и виджетами, чтобы избежать переписывания сложного пользовательского интерфейса.
Что вы построите
В этом практическом занятии вы создадите виджеты для главного экрана простого Flutter-приложения на Android и iOS, используя пакет home_widget, который позволит пользователям читать статьи. Ваши виджеты будут:
- Отображение данных из вашего Flutter-приложения.
- Отображение текста с использованием шрифтов, предоставленных из приложения Flutter.
- Отобразить изображение отрисованного виджета Flutter.

Это Flutter-приложение включает два экрана (или маршрута ):
- В первом разделе отображается список новостных статей с заголовками и описаниями.
- На втором изображении отображается полный текст статьи с диаграммой, созданной с помощью
CustomPaint.
.


Что вы узнаете
- Как создать виджеты для главного экрана на iOS и Android.
- Как использовать пакет home_widget для обмена данными между виджетом на главном экране и вашим приложением Flutter.
- Как уменьшить объем кода, который необходимо переписать.
- Как обновить виджет на главном экране из вашего Flutter-приложения.
2. Настройте среду разработки.
Для обеих платформ вам потребуется Flutter SDK и IDE . Вы можете использовать предпочитаемую вами IDE для работы с Flutter. Это может быть Visual Studio Code с расширением Dart Code и Flutter , или Android Studio или IntelliJ с установленными плагинами Flutter и Dart .
Чтобы создать виджет для главного экрана iOS:
- Этот практический урок можно выполнить на физическом устройстве iOS или в симуляторе iOS.
- Необходимо настроить систему macOS с интегрированной средой разработки Xcode. Это установит компилятор, необходимый для сборки iOS-версии вашего приложения.
Чтобы создать виджет для главного экрана Android:
- Этот практический урок можно выполнить как на физическом устройстве Android, так и в эмуляторе Android.
- Необходимо настроить систему разработки с помощью Android Studio. Это установит компилятор, необходимый для сборки Android-версии вашего приложения.
Получите стартовый код
Загрузите начальную версию своего проекта с GitHub.
В командной строке клонируйте репозиторий GitHub в директорию flutter-codelabs:
$ git clone https://github.com/flutter/codelabs.git flutter-codelabs
После клонирования репозитория вы найдете код для этого практического занятия в директории flutter-codelabs/homescreen_codelab. В этой директории содержится готовый код проекта для каждого шага практического занятия.
Откройте стартовое приложение
Откройте каталог flutter-codelabs/homescreen_codelab/step_03 в вашей любимой IDE.
Установить пакеты
Все необходимые пакеты были добавлены в файл pubspec.yaml проекта. Чтобы получить зависимости проекта, выполните следующую команду:
$ flutter pub get
3. Добавьте простой виджет на главный экран.
Сначала добавьте виджет «Главный экран», используя встроенные инструменты платформы.
Создание базового виджета для главного экрана iOS
Добавление расширения в ваше iOS-приложение на Flutter аналогично добавлению расширения в приложение на SwiftUI или UIKit:
- Откройте
open ios/Runner.xcworkspaceв окне терминала из каталога вашего проекта Flutter. В качестве альтернативы, щелкните правой кнопкой мыши на папке ios в VSCode и выберите «Открыть в Xcode» . Это откроет рабочую область Xcode по умолчанию в вашем проекте Flutter. - Выберите в меню «Файл» → «Создать» → «Цель» . Это добавит новую цель в проект.
- Появится список шаблонов. Выберите «Расширение виджета» .
- В поле « Название продукта » для этого виджета введите «NewsWidgets». Снимите флажки « Включить активность в реальном времени» и «Включить намерение конфигурации» .
Изучите пример кода.
При добавлении нового целевого объекта Xcode генерирует пример кода на основе выбранного вами шаблона. Более подробную информацию о сгенерированном коде и WidgetKit см. в документации Apple по расширениям приложений.
Отладьте и протестируйте свой пример виджета.
- Сначала обновите конфигурацию вашего Flutter-приложения. Это необходимо сделать при добавлении новых пакетов в ваше Flutter-приложение и при планировании запуска цели в проекте из Xcode. Чтобы обновить конфигурацию приложения, выполните следующую команду в каталоге вашего Flutter-приложения:
$ flutter build ios --config-only
- Нажмите «Runner» , чтобы отобразить список целей. Выберите только что созданную цель виджета, NewsWidgets, и нажмите «Run» . Запустите цель виджета из Xcode при изменении кода виджета iOS.

- На экране симулятора или устройства должен отображаться базовый виджет главного экрана. Если вы его не видите, вы можете добавить его на экран. Нажмите и удерживайте главный экран, затем нажмите кнопку «+» в верхнем левом углу.

- Найдите название приложения. Для этого практического задания найдите "Виджеты главного экрана".

- После добавления виджета на главный экран должен отображаться простой текст, показывающий время.
Создание простого виджета для Android
- Чтобы добавить виджет на главный экран Android, откройте файл сборки проекта в Android Studio. Этот файл находится по адресу android/build.gradle. Также можно щелкнуть правой кнопкой мыши по папке android в VSCode и выбрать «Открыть в Android Studio» .
- После сборки проекта найдите папку приложения в верхнем левом углу. Добавьте свой новый виджет для главного экрана в эту папку. Щелкните правой кнопкой мыши по папке, выберите «Создать» -> «Виджет» -> «Виджет приложения» .

- В Android Studio отображается новая форма. Добавьте основную информацию о вашем виджете для главного экрана, включая имя класса, расположение, размер и язык программирования.
Для выполнения этого практического задания установите следующие значения:
- Поле «Название класса» для виджета новостей
- Минимальная ширина (ячеек) в выпадающем списке — 3.
- Минимальная высота (ячеек) в выпадающем списке — 3.
Изучите пример кода.
При отправке формы Android Studio создает и обновляет несколько файлов. Изменения, относящиеся к данному практическому заданию, перечислены в таблице ниже.
Действие | Целевой файл | Изменять |
Обновлять | | Добавляет новый приемник, который регистрирует NewsWidget. |
Создавать | | Определяет пользовательский интерфейс виджетов главного экрана. |
Создавать | | Определяет конфигурацию виджета на главном экране. В этом файле вы можете настроить размеры или название виджета. |
Создавать | | Содержит ваш код Kotlin для добавления функциональности к виджету на главном экране. |
Более подробную информацию об этих файлах вы найдете в этом практическом занятии.
Отладьте и протестируйте свой пример виджета.
Теперь запустите приложение и увидите виджет на главном экране. После сборки приложения перейдите на экран выбора приложений на вашем Android-устройстве и нажмите и удерживайте значок этого проекта Flutter. Выберите «Виджеты» в появившемся меню.

Устройство Android или эмулятор отображает ваш стандартный виджет главного экрана Android.
4. Отправляйте данные из вашего Flutter-приложения на виджет главного экрана.
Вы можете настроить базовый виджет главного экрана, который вы создали. Обновите виджет главного экрана, чтобы он отображал заголовок и краткое содержание новостной статьи. На следующем снимке экрана показан пример виджета главного экрана, отображающего заголовок и краткое содержание.

Для передачи данных между вашим приложением и виджетом на главном экране вам потребуется написать код на Dart и нативный код. В этом разделе этот процесс разделен на три части:
- Напишите код на Dart для вашего Flutter-приложения, который смогут использовать как Android, так и iOS.
- Добавление нативной функциональности iOS
- Добавление нативной функциональности Android.
Использование групп приложений iOS
Для обмена данными между родительским приложением iOS и расширением виджета оба целевых приложения должны принадлежать к одной и той же группе приложений. Более подробную информацию о группах приложений см. в документации Apple по группам приложений .
Обновите идентификатор пакета:
В Xcode перейдите в настройки целевого объекта. На вкладке «Подписание и возможности» убедитесь, что указаны ваша команда и идентификатор пакета.
Добавьте группу приложений (App Group) как к целевому объекту Runner, так и к целевому объекту NewsWidgetExtension в Xcode:
Выберите + Возможность -> Группы приложений и добавьте новую группу приложений. Повторите эти действия для целевого объекта Runner (родительского приложения) и целевого объекта виджета.

Добавьте код 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-приложения, перейдите к файлу 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 . Функция также уведомляет собственную платформу о возможности получения и отображения новых данных для виджетов главного экрана.
Измените плавающую кнопку действия (floatingActionButton).
Вызовите функцию updateHeadline при нажатии кнопки floatingActionButton , как показано ниже:
lib/article_screen.dart
// New: import the updateHeadline function
import 'home_screen.dart';
...
floatingActionButton: FloatingActionButton.extended(
onPressed: () {
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(
content: Text('Updating home screen widget...'),
));
// New: call updateHeadline
updateHeadline(widget.article);
},
label: const Text('Update Homescreen'),
),
...
Благодаря этому изменению, при нажатии пользователем кнопки «Обновить заголовок» на странице статьи обновляются и сведения о виджете на главном экране.
Обновите код iOS, чтобы отображать данные статьи.
Для обновления виджета «Домашний экран» для iOS используйте Xcode.
Откройте файл NewsWidgets.swift в Xcode:
Настройте элемент TimelineEntry .
Замените структуру SimpleEntry следующим кодом:
ios/NewsWidgets/NewsWidgets.swift
// The date and any data you want to pass into your app must conform to TimelineEntry
struct NewsArticleEntry: TimelineEntry {
let date: Date
let title: String
let description:String
}
Структура NewsArticleEntry определяет входящие данные, которые будут передаваться в виджет на главном экране при обновлении. Тип TimelineEntry требует параметра даты. Чтобы узнать больше о протоколе TimelineEntry , ознакомьтесь с документацией 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 следующим кодом. Затем замените идентификатор вашей группы приложений на <ВАША ГРУППА ПРИЛОЖЕНИЙ>:
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 есть три разных метода:
- Метод
placeholderгенерирует запись-заполнитель при первом предварительном просмотре виджета на главном экране.

- Метод
getSnapshotсчитывает данные из пользовательских настроек и генерирует запись для текущего времени. - Метод
getTimelineвозвращает записи временной шкалы. Это полезно, когда у вас есть предсказуемые моменты времени для обновления контента. В этом практическом задании используется функция `getSnapshot` для получения текущего состояния. Метод.atEndуказывает виджету на главном экране обновить данные по истечении текущего времени.
Закомментируйте строку NewsWidgets_Previews
Использование предварительного просмотра выходит за рамки данного практического занятия. Более подробную информацию о предварительном просмотре виджетов SwiftUI Home Screen см. в документации Apple по отладке виджетов .
Сохраните все файлы и повторно запустите приложение и целевой объект виджета.
Повторите выполнение заданий, чтобы убедиться в работоспособности приложения и виджета на главном экране.
- Выберите схему приложения в Xcode, чтобы запустить целевое приложение.
- Выберите схему расширения в Xcode, чтобы запустить целевое приложение расширения.
- Перейдите на страницу статьи в приложении.
- Нажмите кнопку, чтобы обновить заголовок. Виджет на главном экране также должен обновить заголовок.
Обновите код Android.
Добавьте XML-файл виджета «Главный экран».
В Android Studio обновите файлы, созданные на предыдущем шаге. Откройте файл res/layout/news_widget.xml . Он определяет структуру и расположение виджета на главном экране. Выберите «Код» в правом верхнем углу и замените содержимое этого файла следующим кодом:
android/app/res/layout/news_widget.xml
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/widget_container"
style="@style/Widget.Android.AppWidget.Container"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:background="@android:color/white"
android:theme="@style/Theme.Android.AppWidgetContainer">
<TextView
android:id="@+id/headline_title"
style="@style/Widget.Android.AppWidget.InnerView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentStart="true"
android:layout_alignParentLeft="true"
android:layout_marginStart="8dp"
android:layout_marginLeft="8dp"
android:background="@android:color/white"
android:text="Title"
android:textSize="20sp"
android:textStyle="bold" />
<TextView
android:id="@+id/headline_description"
style="@style/Widget.Android.AppWidget.InnerView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@+id/headline_title"
android:layout_alignParentStart="true"
android:layout_alignParentLeft="true"
android:layout_marginStart="8dp"
android:layout_marginLeft="8dp"
android:layout_marginTop="4dp"
android:background="@android:color/white"
android:text="Title"
android:textSize="16sp" />
</RelativeLayout>
Этот XML-файл определяет два текстовых поля: одно для заголовка статьи, другое для описания статьи. Эти текстовые поля также определяют стиль оформления. Вы будете возвращаться к этому файлу на протяжении всего этого практического занятия.
Обновить функциональность виджета новостей.
Откройте файл исходного кода Kotlin NewsWidget.kt . Этот файл содержит сгенерированный класс NewsWidget , который наследует класс AppWidgetProvider .
Класс NewsWidget содержит три метода из своего суперкласса. Вам нужно будет изменить метод onUpdate . Android вызывает этот метод для виджетов через фиксированные интервалы времени.
Замените содержимое файла NewsWidget.kt следующим кодом:
android/app/java/com.mydomain.homescreen_widgets/NewsWidget.kt
// Import will depend on App ID.
package com.mydomain.homescreen_widgets
import android.appwidget.AppWidgetManager
import android.appwidget.AppWidgetProvider
import android.content.Context
import android.widget.RemoteViews
// New import.
import es.antonborri.home_widget.HomeWidgetPlugin
/**
* Implementation of App Widget functionality.
*/
class NewsWidget : AppWidgetProvider() {
override fun onUpdate(
context: Context,
appWidgetManager: AppWidgetManager,
appWidgetIds: IntArray,
) {
for (appWidgetId in appWidgetIds) {
// Get reference to SharedPreferences
val widgetData = HomeWidgetPlugin.getData(context)
val views = RemoteViews(context.packageName, R.layout.news_widget).apply {
val title = widgetData.getString("headline_title", null)
setTextViewText(R.id.headline_title, title ?: "No title set")
val description = widgetData.getString("headline_description", null)
setTextViewText(R.id.headline_description, description ?: "No description set")
}
appWidgetManager.updateAppWidget(appWidgetId, views)
}
}
}
Теперь, когда вызывается onUpdate , Android получает самые новые значения из локального хранилища с помощью метода the widgetData.getString() , а затем вызывает setTextViewText для изменения текста, отображаемого на виджете главного экрана.
Проверьте обновления
Проверьте приложение, чтобы убедиться, что виджеты на главном экране обновляются новыми данными. Для обновления данных используйте FloatingActionButton Обновить главный экран» на страницах статей. Ваш виджет на главном экране должен обновиться, отображая заголовок статьи.

5. Использование пользовательских шрифтов приложения Flutter в виджете на главном экране iOS.
На данный момент вы настроили виджет главного экрана для чтения данных, предоставляемых приложением Flutter. Приложение Flutter включает в себя пользовательский шрифт, который вы, возможно, захотите использовать в виджете главного экрана. Вы можете использовать этот пользовательский шрифт в виджете главного экрана iOS. Использование пользовательских шрифтов в виджетах главного экрана недоступно на Android.
Обновите код iOS.
Flutter хранит свои ресурсы в mainBundle iOS-приложений. Вы можете получить доступ к ресурсам из этого пакета из кода виджета на главном экране.
В структуре NewsWidgetsEntryView в файле NewsWidgets.swift внесите следующие изменения.
Создайте вспомогательную функцию для получения пути к каталогу ресурсов Flutter:
ios/NewsWidgets/NewsWidgets.swift
struct NewsWidgetsEntryView : View {
...
// New: Add the helper function.
var bundle: URL {
let bundle = Bundle.main
if bundle.bundleURL.pathExtension == "appex" {
// Peel off two directory levels - MY_APP.app/PlugIns/MY_APP_EXTENSION.appex
var url = bundle.bundleURL.deletingLastPathComponent().deletingLastPathComponent()
url.append(component: "Frameworks/App.framework/flutter_assets")
return url
}
return bundle.bundleURL
}
...
}
Зарегистрируйте шрифт, используя URL-адрес вашего файла пользовательского шрифта.
ios/NewsWidgets/NewsWidgets.swift
struct NewsWidgetsEntryView : View {
...
// New: Register the font.
init(entry: Provider.Entry){
self.entry = entry
CTFontManagerRegisterFontsForURL(bundle.appending(path: "/fonts/Chewy-Regular.ttf") as CFURL, CTFontManagerScope.process, nil)
}
...
}
Обновите текст заголовка, чтобы использовать свой собственный шрифт.
ios/NewsWidgets/NewsWidgets.swift
struct NewsWidgetsEntryView : View {
...
var body: some View {
VStack {
// Update the following line.
Text(entry.title).font(Font.custom("Chewy", size: 13))
Text(entry.description)
}
}
...
}
При запуске виджета на главном экране для заголовка теперь используется пользовательский шрифт, как показано на следующем изображении:

6. Отображение виджетов Flutter в виде изображения.
В этом разделе вы отобразите график из вашего Flutter-приложения в виде виджета на главном экране.
Этот виджет представляет собой более сложную задачу, чем текст, отображаемый на главном экране. Отобразить диаграмму Flutter в виде изображения гораздо проще, чем пытаться воссоздать её с помощью нативных компонентов пользовательского интерфейса.
Настройте виджет на главном экране так, чтобы он отображал ваш Flutter-диаграмму в формате PNG. Виджет на главном экране сможет отображать это изображение.
Напишите код на Dart.
На стороне Dart добавьте метод renderFlutterWidget из пакета home_widget. Этот метод принимает виджет, имя файла и ключ. Он возвращает изображение виджета Flutter и сохраняет его в общий контейнер. Укажите имя изображения в своем коде и убедитесь, что виджет «Главный экран» имеет доступ к контейнеру. key сохраняет полный путь к файлу в виде строки в локальном хранилище устройства. Это позволяет виджету «Главный экран» найти файл, если имя файла изменится в коде Dart.
В этом практическом задании класс LineChart из файла lib/article_screen.dart представляет собой диаграмму. Его метод 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 получает контекст конкретного виджета, что необходимо для определения его размера.
lib/article_screen.dart
class _ArticleScreenState extends State<ArticleScreen> {
// New: add this GlobalKey
final _globalKey = GlobalKey();
...
}
Добавляет imagePath
Свойство imagePath хранит местоположение изображения, где отображается виджет Flutter.
lib/article_screen.dart
class _ArticleScreenState extends State<ArticleScreen> {
...
// New: add this imagePath
String? imagePath;
...
}
Добавляет ключ к виджету для отображения.
Объект _globalKey содержит виджет Flutter, который отображается на изображении. В данном случае виджетом Flutter является объект Center, содержащий LineChart .
lib/article_screen.dart
class _ArticleScreenState extends State<ArticleScreen> {
...
Center(
// New: Add this key
key: _globalKey,
child: const LineChart(),
),
...
}
- Сохраняет виджет как изображение.
Метод renderFlutterWidget вызывается при нажатии пользователем кнопки floatingActionButton . Метод сохраняет полученный PNG-файл как "скриншот" в общую директорию контейнера. Метод также сохраняет полный путь к изображению в качестве ключа filename в памяти устройства.
lib/article_screen.dart
class _ArticleScreenState extends State<ArticleScreen> {
...
floatingActionButton: FloatingActionButton.extended(
onPressed: () async {
if (_globalKey.currentContext != null) {
var path = await HomeWidget.renderFlutterWidget(
LineChart(),
fileName: 'screenshot',
key: 'filename',
logicalSize: _globalKey.currentContext!.size,
pixelRatio:
MediaQuery.of(_globalKey.currentContext!).devicePixelRatio,
);
setState(() {
imagePath = path as String?;
});
}
updateHeadline(widget.article);
},
...
}
Обновите код iOS.
Для iOS обновите код, чтобы получать путь к файлу из хранилища и отображать файл в виде изображения с помощью SwiftUI.
Откройте файл NewsWidgets.swift и внесите следующие изменения:
Добавьте filename и displaySize в структуру NewsArticleEntry
Свойство filename содержит строку, представляющую путь к файлу изображения. Свойство displaySize содержит размер виджета главного экрана на устройстве пользователя. Размер виджета главного экрана определяется context .
ios/NewsWidgets/NewsWidgets.swift
struct NewsArticleEntry: TimelineEntry {
...
// New: add the filename and displaySize.
let filename: String
let displaySize: CGSize
}
Обновите функцию placeholder .
Укажите filename заполнитель и displaySize .
ios/NewsWidgets/NewsWidgets.swift
func placeholder(in context: Context) -> NewsArticleEntry {
NewsArticleEntry(date: Date(), title: "Placeholder Title", description: "Placeholder description", filename: "No screenshot available", displaySize: context.displaySize)
}
Получите имя файла из userDefaults в функции getSnapshot.
Это устанавливает значение переменной filename равным значению filename из хранилища userDefaults при обновлении виджета на главном экране.
ios/NewsWidgets/NewsWidgets.swift
func getSnapshot(
...
let title = userDefaults?.string(forKey: "headline_title") ?? "No Title Set"
let description = userDefaults?.string(forKey: "headline_description") ?? "No Description Set"
// New: get fileName from key/value store
let filename = userDefaults?.string(forKey: "filename") ?? "No screenshot available"
...
)
Создайте объект ChartImage , который отображает изображение по заданному пути.
Компонент ChartImage View создает изображение из содержимого файла, сгенерированного на стороне Dart. Здесь вы устанавливаете размер изображения равным 50% от размера рамки.
ios/NewsWidgets/NewsWidgets.swift
struct NewsWidgetsEntryView : View {
...
// New: create the ChartImage view
var ChartImage: some View {
if let uiImage = UIImage(contentsOfFile: entry.filename) {
let image = Image(uiImage: uiImage)
.resizable()
.frame(width: entry.displaySize.height*0.5, height: entry.displaySize.height*0.5, alignment: .center)
return AnyView(image)
}
print("The image file could not be loaded")
return AnyView(EmptyView())
}
...
}
Используйте ChartImage в теле NewsWidgetsEntryView.
Добавьте элемент ChartImage в тело элемента NewsWidgetsEntryView, чтобы отобразить ChartImage в виджете на главном экране.
ios/NewsWidgets/NewsWidgets.swift
VStack {
Text(entry.title).font(Font.custom("Chewy", size: 13))
Text(entry.description).font(.system(size: 12)).padding(10)
// New: add the ChartImage to the NewsWidgetEntryView
ChartImage
}
Проверьте изменения.
Чтобы проверить изменения, повторно запустите как целевое приложение Flutter (Runner), так и целевое расширение из Xcode. Чтобы увидеть изображение, перейдите на одну из страниц статьи в приложении и нажмите кнопку обновления виджета на главном экране.

Обновите код Android.
Код для Android функционирует так же, как и код для iOS.
- Откройте файл
android/app/res/layout/news_widget.xml. Он содержит элементы пользовательского интерфейса вашего виджета на главном экране. Замените его содержимое следующим кодом:
android/app/res/layout/news_widget.xml
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/widget_container"
style="@style/Widget.Android.AppWidget.Container"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:background="@android:color/white"
android:theme="@style/Theme.Android.AppWidgetContainer">
<TextView
android:id="@+id/headline_title"
style="@style/Widget.Android.AppWidget.InnerView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentStart="true"
android:layout_alignParentLeft="true"
android:layout_marginStart="8dp"
android:layout_marginLeft="8dp"
android:background="@android:color/white"
android:text="Title"
android:textSize="20sp"
android:textStyle="bold" />
<TextView
android:id="@+id/headline_description"
style="@style/Widget.Android.AppWidget.InnerView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@+id/headline_title"
android:layout_alignParentStart="true"
android:layout_alignParentLeft="true"
android:layout_marginStart="8dp"
android:layout_marginLeft="8dp"
android:layout_marginTop="4dp"
android:background="@android:color/white"
android:text="Title"
android:textSize="16sp" />
<!--New: add this image view -->
<ImageView
android:id="@+id/widget_image"
android:layout_width="200dp"
android:layout_height="200dp"
android:layout_below="@+id/headline_description"
android:layout_alignBottom="@+id/headline_title"
android:layout_alignParentStart="true"
android:layout_alignParentLeft="true"
android:layout_marginStart="8dp"
android:layout_marginLeft="8dp"
android:layout_marginTop="6dp"
android:layout_marginBottom="-134dp"
android:layout_weight="1"
android:adjustViewBounds="true"
android:background="@android:color/white"
android:scaleType="fitCenter"
android:src="@android:drawable/star_big_on"
android:visibility="visible"
tools:visibility="visible" />
</RelativeLayout>
Этот новый код добавляет изображение в виджет главного экрана, который (пока) отображает обычную иконку звезды. Замените эту иконку звезды изображением, которое вы сохранили в коде Dart.
- Откройте файл
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 заменяет изображение в виджете на главном экране новым изображением.
- Перезагрузите приложение и перейдите на экран статьи. Нажмите «Обновить главный экран» . На главном экране отобразится диаграмма.
7. Дальнейшие шаги
Поздравляем!
Поздравляем, вам удалось создать виджеты для главного экрана ваших приложений Flutter для iOS и Android!
Создание ссылок на контент в вашем Flutter-приложении
В зависимости от того, куда пользователь кликнет, вы можете захотеть перенаправить пользователя на определенную страницу в вашем приложении. Например, в новостном приложении из этого практического занятия вы можете захотеть, чтобы пользователь увидел новостную статью с соответствующим заголовком.
Эта функция выходит за рамки данного практического занятия. Примеры использования потока, предоставляемого пакетом home_widget , для идентификации запуска приложений из виджетов главного экрана и отправки сообщений из виджета главного экрана через URL-адрес можно найти здесь. Для получения дополнительной информации см. документацию по глубоким ссылкам на docs.flutter.dev.
Обновление виджета в фоновом режиме.
В этом практическом задании вы запустили обновление виджета «Главный экран» с помощью кнопки. Хотя это вполне приемлемо для тестирования, в рабочем коде вам может потребоваться, чтобы ваше приложение обновляло виджет «Главный экран» в фоновом режиме. Вы можете использовать плагин workmanager для создания фоновых задач по обновлению ресурсов, необходимых виджету «Главный экран». Чтобы узнать больше, ознакомьтесь с разделом «Фоновое обновление» в пакете home_widget.
В iOS вы также можете настроить виджет на главном экране на отправку сетевого запроса для обновления пользовательского интерфейса. Чтобы контролировать условия или частоту этого запроса, используйте временную шкалу. Подробнее об использовании временной шкалы см. в документации Apple «Поддержание виджета в актуальном состоянии».