1. Введение
Что такое виджеты?
Для разработчиков Flutter общее определение виджета относится к компонентам пользовательского интерфейса, созданным с использованием платформы Flutter. В контексте этой лаборатории виджет относится к мини-версии приложения, которая обеспечивает просмотр информации о приложении , не открывая его. На Android виджеты размещаются на главном экране. В iOS их можно добавить на главный экран, экран блокировки или в представление «Сегодня».
Насколько сложным может быть виджет?
Большинство виджетов главного экрана просты. Они могут содержать простой текст, простую графику или, на Android, базовые элементы управления. И Android, и iOS ограничивают количество компонентов и функций пользовательского интерфейса, которые вы можете использовать.
Создайте пользовательский интерфейс для виджетов
Из-за этих ограничений пользовательского интерфейса вы не можете напрямую рисовать пользовательский интерфейс виджета главного экрана с помощью платформы Flutter. Вместо этого вы можете добавить виджеты, созданные с помощью платформ, таких как Jetpack Compose или SwiftUI, в свое приложение Flutter. В этой кодовой лаборатории обсуждаются примеры совместного использования ресурсов между вашим приложением и виджетами, чтобы избежать переписывания сложного пользовательского интерфейса.
Что ты построишь
В этой лаборатории кода вы создадите виджеты главного экрана на Android и iOS для простого приложения Flutter, используя пакет 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 IDE. При этом будет установлен компилятор, необходимый для сборки версии вашего приложения для 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
Добавление расширения приложения в приложение Flutter для iOS аналогично добавлению расширения приложения в приложение 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
- Нажмите «Бегущий» , чтобы открыть список целей. Выберите цель виджета, которую вы только что создали, NewsWidgets, и нажмите «Выполнить» . Запустите цель виджета из Xcode при изменении кода виджета iOS.
- На экране симулятора или устройства должен отображаться базовый виджет главного экрана. Если вы его не видите, вы можете добавить его на экран. Нажмите и удерживайте главный экран, затем нажмите + в верхнем левом углу.
- Найдите название приложения. Для этой кодовой лаборатории найдите «Виджеты главного экрана».
- После того, как вы добавите виджет «На главный экран», он должен отображать простой текст с указанием времени.
Создание базового виджета Android
- Чтобы добавить виджет «Главный экран» в Android, откройте файл сборки проекта в Android Studio. Вы можете найти этот файл по адресу android/build.gradle. Либо щелкните правой кнопкой мыши папку Android из VSCode и выберите «Открыть в Android Studio» .
- После сборки проекта найдите каталог приложения в верхнем левом углу. Добавьте в этот каталог новый виджет главного экрана. Щелкните правой кнопкой мыши каталог и выберите «Создать» -> «Виджет» -> «Виджет приложения» .
- Android Studio отображает новую форму. Добавьте базовую информацию о виджете главного экрана, включая имя его класса, размещение, размер и исходный язык.
Для этой лаборатории кода установите следующие значения:
- Поле имени класса в NewsWidget
- Раскрывающийся список «Минимальная ширина (ячейки)» до 3.
- Минимальная высота (ячейки) раскрывается до 3.
Проверьте пример кода
Когда вы отправляете форму, Android Studio создает и обновляет несколько файлов. Изменения, относящиеся к этой кодовой лаборатории, перечислены в таблице ниже.
Действие | Целевой файл | Изменять |
Обновлять | | Добавляет новый приемник, который регистрирует NewsWidget. |
Создавать | | Определяет пользовательский интерфейс виджета главного экрана. |
Создавать | | Определяет конфигурацию виджета главного экрана. В этом файле вы можете настроить размеры или имя вашего виджета. |
Создавать | | Содержит код Kotlin для расширения функциональности виджета главного экрана. |
Более подробную информацию об этих файлах можно найти в этой лаборатории кода.
Отладка и тестирование образца виджета
Теперь запустите приложение и увидите виджет «Главный экран». После создания приложения перейдите к экрану выбора приложения на вашем устройстве Android и нажмите и удерживайте значок этого проекта Flutter. Выберите «Виджеты» во всплывающем меню.
На устройстве Android или в эмуляторе отображается виджет главного экрана Android по умолчанию.
4. Отправьте данные из приложения Flutter на виджет главного экрана.
Вы можете настроить созданный вами базовый виджет главного экрана. Обновите виджет «Главный экран», чтобы отображать заголовок и краткое содержание новостной статьи. На следующем снимке экрана показан пример виджета «Главный экран», отображающего заголовок и сводку.
Чтобы передавать данные между вашим приложением и виджетом главного экрана, вам нужно написать Dart и собственный код. В этом разделе этот процесс разделен на три части:
- Написание кода Dart в вашем приложении Flutter, который могут использовать как Android, так и iOS.
- Добавление встроенной функциональности iOS
- Добавление встроенной функциональности Android
Использование групп приложений iOS
Чтобы обмениваться данными между родительским приложением iOS и расширением виджета, обе цели должны принадлежать к одной и той же группе приложений. Дополнительную информацию о группах приложений см. в документации по группам приложений Apple .
Обновите идентификатор пакета:
В Xcode перейдите к настройкам вашей цели. На вкладке «Подписание и возможности» убедитесь, что установлены идентификатор вашей команды и пакета.
Добавьте группу приложений как в цель Runner, так и в цель NewsWidgetExtension в Xcode:
Выберите + Возможности -> Группы приложений и добавьте новую группу приложений. Повторите эти действия как для цели Runner (родительское приложение), так и для цели виджета.
Добавьте дартс-код
Приложения 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
. Функция также уведомляет собственную платформу о том, что новые данные для виджетов главного экрана могут быть получены и отображены.
Измените floatActionButton.
Вызовите функцию 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 см. в документации Apple по отладке виджетов .
Сохраните все файлы и повторно запустите приложение и виджет.
Запустите цели еще раз, чтобы убедиться, что приложение и виджет главного экрана работают.
- Выберите схему приложения в Xcode для запуска целевого приложения.
- Выберите схему расширения в Xcode для запуска цели расширения.
- Перейдите на страницу статьи в приложении.
- Нажмите кнопку, чтобы обновить заголовок. Виджет «Главный экран» также должен обновить заголовок.
Обновите код Android
Добавьте XML-код виджета главного экрана.
В Android Studio обновите файлы, созданные на предыдущем шаге. Откройте файл res/layout/news_widget.xml
. Он определяет структуру и расположение виджета вашего главного экрана. Выберите «Код» в правом верхнем углу и замените содержимое этого файла следующим кодом:
Android/приложение/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 определяет два текстовых представления: одно для заголовка статьи, а другое для описания статьи. Эти текстовые представления также определяют стиль. Вы будете возвращаться к этому файлу на протяжении всей этой лаборатории.
Обновить функциональность NewsWidget
Откройте файл исходного кода NewsWidget.kt
Kotlin. Этот файл содержит сгенерированный класс 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 хранит свои ресурсы в основном пакете приложений 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 добавьте метод renderFlutterWidget
из пакета home_widget. Этот метод принимает виджет, имя файла и ключ. Он возвращает изображение виджета Flutter и сохраняет его в общий контейнер. Укажите имя изображения в своем коде и убедитесь, что виджет «Главный экран» имеет доступ к контейнеру. key
сохраняет полный путь к файлу в виде строки в локальном хранилище устройства. Это позволяет виджету «Главный экран» найти файл, если имя изменится в коде Dart.
В этой кодовой лаборатории класс LineChart
в файле lib/article_screen.dart
представляет диаграмму. Его метод сборки возвращает 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
получает контекст конкретного виджета, который необходим для получения размера этого виджета.
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 — это центр, содержащий LineChart
.
lib/article_screen.dart
class _ArticleScreenState extends State<ArticleScreen> {
...
Center(
// New: Add this key
key: _globalKey,
child: const LineChart(),
),
...
}
- Сохраняет виджет как изображение.
Метод renderFlutterWidget
вызывается, когда пользователь нажимает floatingActionButton
. Метод сохраняет полученный файл 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
содержит размер виджета главного экрана на устройстве пользователя. Размер виджета главного экрана зависит от 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
создает изображение из содержимого файла, созданного на стороне 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/приложение/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 «Обновление виджета».