Добавление виджета главного экрана в приложение Flutter

1. Введение

Что такое виджеты?

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

f0027e8a7d0237e0.pngb991e79ea72c8b65.png

Насколько сложным может быть виджет?

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

819b9fffd700e571.png92d62ccfd17d770d.png

Создайте пользовательский интерфейс для виджетов

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

Что ты построишь

В этой лаборатории кода вы создадите виджеты главного экрана на Android и iOS для простого приложения Flutter, используя пакет home_widget, который позволяет пользователям читать статьи. Ваши виджеты будут:

  • Покажите данные из вашего приложения Flutter.
  • Отображайте текст, используя ресурсы шрифтов, предоставленные из приложения Flutter.
  • Отобразите изображение визуализированного виджета Flutter.

а36b7ba379151101.png

Это приложение Flutter включает в себя два экрана (или маршрута ):

  • Первый отображает список новостных статей с заголовками и описаниями.
  • Второй отображает полную статью с диаграммой, созданной с помощью CustomPaint .

.

9c02f8b62c1faa3a.pngd97d44051304cae4.png

Что вы узнаете

  • Как создать виджеты главного экрана на 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:

  1. Запустите open ios/Runner.xcworkspace в окне терминала из каталога вашего проекта Flutter. Либо щелкните правой кнопкой мыши папку ios из VSCode и выберите «Открыть в Xcode» . Это откроет рабочую область Xcode по умолчанию в вашем проекте Flutter.
  2. В меню выберите Файл → Создать → Цель . Это добавляет новую цель в проект.
  3. Появится список шаблонов. Выберите Расширение виджета .
  4. Введите «NewsWidgets» в поле «Название продукта » для этого виджета. Снимите флажки «Включить динамическое действие» и «Включить намерение конфигурации» .

Проверьте пример кода

Когда вы добавляете новую цель, Xcode генерирует пример кода на основе выбранного вами шаблона. Дополнительные сведения о сгенерированном коде и WidgetKit см. в документации по расширениям приложений Apple.

Отладка и тестирование образца виджета

  1. Сначала обновите конфигурацию вашего приложения Flutter. Вы должны сделать это, когда добавляете новые пакеты в свое приложение Flutter и планируете запустить цель в проекте из Xcode. Чтобы обновить конфигурацию вашего приложения, выполните следующую команду в каталоге приложения Flutter:
$ flutter build ios --config-only
  1. Нажмите «Бегущий» , чтобы открыть список целей. Выберите цель виджета, которую вы только что создали, NewsWidgets, и нажмите «Выполнить» . Запустите цель виджета из Xcode при изменении кода виджета iOS.

bbb519df1782881d.png

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

18eff1cae152014d.png

  1. Найдите название приложения. Для этой кодовой лаборатории найдите «Виджеты главного экрана».

a0c00df87615493e.png

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

Создание базового виджета Android

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

f19d8b7f95ab884e.png

  1. Android Studio отображает новую форму. Добавьте базовую информацию о виджете главного экрана, включая имя его класса, размещение, размер и исходный язык.

Для этой лаборатории кода установите следующие значения:

  • Поле имени класса в NewsWidget
  • Раскрывающийся список «Минимальная ширина (ячейки)» до 3.
  • Минимальная высота (ячейки) раскрывается до 3.

Проверьте пример кода

Когда вы отправляете форму, Android Studio создает и обновляет несколько файлов. Изменения, относящиеся к этой кодовой лаборатории, перечислены в таблице ниже.

Действие

Целевой файл

Изменять

Обновлять

AndroidManifest.xml

Добавляет новый приемник, который регистрирует NewsWidget.

Создавать

res/layout/news_widget.xml

Определяет пользовательский интерфейс виджета главного экрана.

Создавать

res/xml/news_widget_info.xml

Определяет конфигурацию виджета главного экрана. В этом файле вы можете настроить размеры или имя вашего виджета.

Создавать

java/com/example/homescreen_widgets/NewsWidget.kt

Содержит код Kotlin для расширения функциональности виджета главного экрана.

Более подробную информацию об этих файлах можно найти в этой лаборатории кода.

Отладка и тестирование образца виджета

Теперь запустите приложение и увидите виджет «Главный экран». После создания приложения перейдите к экрану выбора приложения на вашем устройстве Android и нажмите и удерживайте значок этого проекта Flutter. Выберите «Виджеты» во всплывающем меню.

dff7c9f9f85ef1c7.png

На устройстве Android или в эмуляторе отображается виджет главного экрана Android по умолчанию.

4. Отправьте данные из приложения Flutter на виджет главного экрана.

Вы можете настроить созданный вами базовый виджет главного экрана. Обновите виджет «Главный экран», чтобы отображать заголовок и краткое содержание новостной статьи. На следующем снимке экрана показан пример виджета «Главный экран», отображающего заголовок и сводку.

acb90343a3e51b6d.png

Чтобы передавать данные между вашим приложением и виджетом главного экрана, вам нужно написать Dart и собственный код. В этом разделе этот процесс разделен на три части:

  1. Написание кода Dart в вашем приложении Flutter, который могут использовать как Android, так и iOS.
  2. Добавление встроенной функциональности iOS
  3. Добавление встроенной функциональности Android

Использование групп приложений iOS

Чтобы обмениваться данными между родительским приложением iOS и расширением виджета, обе цели должны принадлежать к одной и той же группе приложений. Дополнительную информацию о группах приложений см. в документации по группам приложений Apple .

Обновите идентификатор пакета:

В Xcode перейдите к настройкам вашей цели. На вкладке «Подписание и возможности» убедитесь, что установлены идентификатор вашей команды и пакета.

Добавьте группу приложений как в цель Runner, так и в цель NewsWidgetExtension в Xcode:

Выберите + Возможности -> Группы приложений и добавьте новую группу приложений. Повторите эти действия как для цели Runner (родительское приложение), так и для цели виджета.

135e1a8c4652dac.png

Добавьте дартс-код

Приложения iOS и Android могут обмениваться данными с приложением Flutter несколькими разными способами. Для связи с этими приложениями используйте локальное хранилище key/value устройства. iOS называет это хранилище UserDefaults , а Android называет это хранилище SharedPreferences . Пакет home_widget объединяет эти API, чтобы упростить сохранение данных на любую платформу, и позволяет виджетам главного экрана получать обновленные данные.

707ae86f6650ac55.png

Данные заголовка и описания берутся из файла 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 есть три разных метода:

  1. Метод placeholder создает запись-заполнитель, когда пользователь впервые просматривает виджет «Главный экран».

45a0f64240c12efe.png

  1. Метод getSnapshot считывает данные из пользовательских настроек по умолчанию и генерирует запись для текущего времени.
  2. Метод getTimeline возвращает записи временной шкалы. Это помогает, когда у вас есть предсказуемые моменты времени для обновления контента. В этой кодовой лаборатории используется функция getSnapshot для получения текущего состояния. Метод .atEnd сообщает виджету «Главный экран» обновить данные по истечении текущего времени.

Прокомментируйте NewsWidgets_Previews

Использование предварительного просмотра выходит за рамки этой лаборатории. Дополнительные сведения о предварительном просмотре виджетов главного экрана SwiftUI см. в документации Apple по отладке виджетов .

Сохраните все файлы и повторно запустите приложение и виджет.

Запустите цели еще раз, чтобы убедиться, что приложение и виджет главного экрана работают.

  1. Выберите схему приложения в Xcode для запуска целевого приложения.
  2. Выберите схему расширения в Xcode для запуска цели расширения.
  3. Перейдите на страницу статьи в приложении.
  4. Нажмите кнопку, чтобы обновить заголовок. Виджет «Главный экран» также должен обновить заголовок.

Обновите код 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 «Обновить главный экран» на страницах статей. Виджет вашего главного экрана должен обновиться с названием статьи.

5ce1c9914b43ad79.png

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)
    }
   }
   ...
}

Когда вы запускаете виджет «Домой экран», он теперь использует собственный шрифт для заголовка, как показано на следующем изображении:

93f8b9d767aacfb2.png

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(),
   ),
   ...
}
  1. Сохраняет виджет как изображение.

Метод 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. Чтобы увидеть изображение, перейдите на одну из страниц статьи в приложении и нажмите кнопку, чтобы обновить виджет главного экрана.

33bdfe2cce908c48.png

Обновите код Android

Код Android функционирует так же, как код iOS.

  1. Откройте файл 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.

  1. Откройте файл 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 заменяет изображение в виджете «Главный экран» новым изображением.

  1. Перезагрузите приложение и перейдите на экран статьи. Нажмите «Обновить главный экран» . Виджет «На главном экране» отображает диаграмму.

7. Следующие шаги

Поздравляем!

Поздравляем, вам удалось создать виджеты главного экрана для ваших приложений Flutter для iOS и Android!

Ссылка на контент в вашем приложении Flutter

Возможно, вы захотите направить пользователя на определенную страницу вашего приложения, в зависимости от того, куда он нажимает. Например, в новостном приложении из этой кодовой лаборатории вы можете захотеть, чтобы пользователь видел новостную статью по отображаемому заголовку.

Эта функция выходит за рамки данной лаборатории кода. Вы можете найти примеры использования потока, предоставляемого пакетом home_widget, для идентификации запуска приложений из виджетов главного экрана и отправки сообщений из виджета главного экрана через URL-адрес. Чтобы узнать больше, см. документацию по глубоким ссылкам на docs.flutter.dev.

Обновление виджета в фоновом режиме

В этой кодовой лаборатории вы активировали обновление виджета главного экрана с помощью кнопки. Хотя это приемлемо для тестирования, в рабочем коде вы можете захотеть, чтобы ваше приложение обновляло виджет главного экрана в фоновом режиме. Вы можете использовать плагин Workmanager для создания фоновых задач для обновления ресурсов, необходимых виджету главного экрана. Чтобы узнать больше, посетите раздел «Фоновое обновление» в пакете home_widget.

В iOS вы также можете сделать так, чтобы виджет «Домой экран» отправлял сетевой запрос на обновление своего пользовательского интерфейса. Чтобы контролировать условия или частоту этого запроса, используйте временную шкалу. Дополнительные сведения об использовании временной шкалы см. в документации Apple «Обновление виджета».

Дальнейшее чтение