Cómo agregar un widget de la pantalla principal a tu app creada con Flutter

1. Introducción

¿Qué son los widgets?

Para los desarrolladores de Flutter, la definición común de widget se refiere a los componentes de la IU creados con el marco de trabajo de Flutter. En el contexto de este codelab, un widget hace referencia a una versión en miniatura de una app que proporciona una vista de la información de la app sin abrir la app. En Android, los widgets se encuentran en la pantalla principal. En iOS, se pueden agregar a la pantalla principal, a la pantalla de bloqueo o a la vista de hoy.

f0027e8a7d0237e0.png b991e79ea72c8b65.png

¿Qué tan complejo puede ser un widget?

La mayoría de los widgets de la pantalla principal son simples. Pueden contener texto básico, gráficos simples o, en Android, controles básicos. Tanto Android como iOS limitan los componentes y las funciones de la IU que puedes usar.

819b9fffd700e571.png 92d62ccfd17d770d.png

Cómo crear la IU para widgets

Debido a estas limitaciones de IU, no puedes dibujar directamente la IU de un widget de la pantalla principal con el framework de Flutter. En su lugar, puedes agregar widgets creados con frameworks de plataformas como Jetpack Compose o SwiftUI a tu app de Flutter. En este codelab, se analizan ejemplos para compartir recursos entre tu app y los widgets para evitar reescribir una IU compleja.

Qué compilarás

En este codelab, compilarás widgets de la pantalla principal en Android y en iOS para una app simple de Flutter, mediante el paquete home_widget, que permite a los usuarios leer artículos. Tus widgets harán lo siguiente:

  • Muestra datos de tu app de Flutter.
  • Muestra texto con recursos de fuente compartidos desde la app de Flutter.
  • Muestra una imagen de un widget de Flutter renderizado.

a36b7ba379151101.png

Esta app de Flutter incluye dos pantallas (o rutas):

  • La primera muestra una lista de artículos de noticias con títulos y descripciones.
  • En la segunda, se muestra el artículo completo con un gráfico creado con CustomPaint.

.

9c02f8b62c1faa3a.png d97d44051304cae4.png

Qué aprenderá

  • Cómo crear widgets para la pantalla de inicio en iOS y Android
  • Cómo usar el paquete home_widget para compartir datos entre el widget de la pantalla principal y tu app de Flutter
  • Cómo reducir la cantidad de código que debes reescribir
  • Cómo actualizar el widget de la pantalla principal desde tu app de Flutter

2. Configura tu entorno de desarrollo

Para ambas plataformas, necesitas el SDK de Flutter y un IDE. Puedes usar tu IDE preferido para trabajar con Flutter. Podría ser Visual Studio Code con las extensiones de Flutter y Dart Code, o Android Studio o IntelliJ con los complementos de Flutter y Dart instalados.

Sigue estos pasos para crear el widget de la pantalla principal de iOS:

  • Puedes ejecutar este codelab en un dispositivo iOS físico o en el simulador de la plataforma.
  • Debes configurar un sistema macOS con el IDE de Xcode. De esta manera, se instala el compilador necesario para compilar la versión de iOS de tu app.

Para crear el widget de la pantalla principal de Android, haz lo siguiente:

  • Puedes ejecutar este codelab en un dispositivo Android físico o en el emulador de Android.
  • Debes configurar tu sistema de desarrollo con Android Studio. De esta manera, se instala el compilador necesario para compilar la versión de Android de tu app.

Obtén el código de inicio

Descarga la versión inicial de tu proyecto desde GitHub

Desde la línea de comandos, clona el repositorio de GitHub en un directorio de flutter-codelabs:

$ git clone https://github.com/flutter/codelabs.git flutter-codelabs

Después de clonar el repositorio, podrás encontrar el código para este codelab en el directorio flutter-codelabs/homescreen_codelab. Este directorio contiene el código completo del proyecto para cada paso del codelab.

Abre la app de partida

Abre el directorio flutter-codelabs/homescreen_codelab/step_03 en tu IDE preferido.

Cómo instalar paquetes

Se agregaron todos los paquetes necesarios al archivo pubspec.yaml del proyecto. Para recuperar las dependencias del proyecto, ejecuta el siguiente comando:

$ flutter pub get

3. Cómo agregar un widget básico a la pantalla principal

Primero, agrega el widget de la pantalla de inicio usando las herramientas nativas de la plataforma.

Cómo crear un widget básico para la pantalla principal de iOS

Agregar una extensión de aplicación a tu app de Flutter para iOS es similar a agregar una extensión a una app de SwiftUI o UIKit:

  1. Ejecuta open ios/Runner.xcworkspace en una ventana de la terminal desde el directorio de tu proyecto de Flutter. Como alternativa, haz clic con el botón derecho en la carpeta ios de VSCode y selecciona Abrir en Xcode. Se abrirá el espacio de trabajo predeterminado de Xcode en tu proyecto de Flutter.
  2. Selecciona File → New → Target en el menú. Esto agrega un destino nuevo al proyecto.
  3. Aparecerá una lista de plantillas. Selecciona Extensión de widgets.
  4. Escribe "NewsWidgets". en el cuadro Product Name de este widget. Desmarca las casillas de verificación Incluir actividad en vivo e Incluir intent de configuración.

Cómo inspeccionar el código de muestra

Cuando agregas un destino nuevo, Xcode genera un código de muestra basado en la plantilla que seleccionaste. Para obtener más información sobre el código generado y WidgetKit, consulta la documentación sobre extensiones de aplicación de Apple.

Depura y prueba el widget de muestra

  1. Primero, actualiza la configuración de tu app de Flutter. Debes hacerlo cuando agregues nuevos paquetes en tu app de Flutter y planeas ejecutar un destino en el proyecto desde Xcode. Para actualizar la configuración de tu app, ejecuta el siguiente comando en el directorio de tu app de Flutter:
$ flutter build ios --config-only
  1. Haz clic en Runner para que aparezca una lista de objetivos. Selecciona el widget de destino que acabas de crear, NewsWidgets, y haz clic en Ejecutar. Ejecuta el destino del widget desde Xcode cuando cambies el código del widget de iOS.

bbb519df1782881d.png

  1. La pantalla del simulador o del dispositivo debe mostrar un widget básico de la pantalla principal. Si no ves la opción, puedes agregarla a la pantalla. Mantén presionada la pantalla principal y haz clic en el signo + que aparece en la esquina superior izquierda.

18eff1cae152014d.png

  1. Busca el nombre de la app. Para este codelab, busca "Widgets de la pantalla principal".

a0c00df87615493e.png

  1. Una vez que agregues el widget de la pantalla de inicio, debería mostrar texto simple que indique la hora.

Cómo crear un widget básico de Android

  1. Para agregar un widget de la pantalla principal en Android, abre el archivo de compilación del proyecto en Android Studio. Puedes encontrar este archivo en android/build.gradle. Como alternativa, haz clic con el botón derecho en la carpeta android de VSCode y selecciona Open in Android Studio.
  2. Cuando el proyecto se compile, ubica el directorio de la app en la esquina superior izquierda. Agrega el nuevo widget para la pantalla principal a este directorio. Haz clic con el botón derecho en el directorio y selecciona New ->. Widget -> Widget de la app

f19d8b7f95ab884e.png

  1. Android Studio mostrará un formulario nuevo. Agrega información básica sobre el widget de la Pantalla principal, incluidos el nombre de la clase, la posición, el tamaño y el idioma de origen

En este codelab, establece los siguientes valores:

  • Class Name en NewsWidget.
  • Menú desplegable Ancho mínimo (celdas): 3
  • Menú desplegable Altura mínima (celdas): 3

Cómo inspeccionar el código de muestra

Cuando envías el formulario, Android Studio crea y actualiza varios archivos. Los cambios relevantes para este codelab se enumeran en la siguiente tabla

Acción

Archivo de destino

Cambiar

Actualizar

AndroidManifest.xml

Agrega un nuevo receptor que registra el NewsWidget.

Crear

res/layout/news_widget.xml

Define la IU del widget de la pantalla principal.

Crear

res/xml/news_widget_info.xml

Define la configuración del widget de la pantalla principal. Puedes ajustar las dimensiones o el nombre de tu widget en este archivo.

Crear

java/com/example/homescreen_widgets/NewsWidget.kt

Contiene tu código Kotlin para agregar funcionalidad al widget de la pantalla principal.

Puedes encontrar más detalles sobre estos archivos a lo largo de este codelab.

Depura y prueba el widget de muestra

Ahora, ejecuta tu aplicación y observa el widget de la pantalla principal. Una vez que compiles la app, navega a la pantalla de selección de aplicaciones de tu dispositivo Android y mantén presionado el ícono de este proyecto de Flutter. Selecciona Widgets en el menú emergente.

dff7c9f9f85ef1c7.png

El dispositivo o emulador Android muestra el widget de la pantalla principal predeterminado para Android.

4. Envía datos de tu app de Flutter al widget de la pantalla principal

Puedes personalizar el widget básico de la pantalla principal que creaste. Actualizar el widget de la pantalla principal para mostrar el titular y el resumen de un artículo de noticias La siguiente captura de pantalla muestra un ejemplo del widget de la pantalla de inicio con un título y un resumen.

acb90343a3e51b6d.png

Para pasar datos entre tu app y el widget de la pantalla principal, debes escribir código nativo y de Dart. En esta sección, se divide este proceso en tres partes:

  1. Escribir código de Dart en tu app de Flutter que pueda usar tanto en Android como en iOS
  2. Cómo agregar funcionalidad nativa de iOS
  3. Cómo agregar funcionalidad nativa de Android

Usa grupos de apps para iOS

Para compartir datos entre una aplicación superior para iOS y una extensión de widget, ambos destinos deben pertenecer al mismo grupo de aplicaciones. Para obtener más información sobre los grupos de apps, consulta la documentación sobre grupos de apps de Apple.

Actualiza el identificador de paquete:

En Xcode, ve a la configuración de tu destino. En la sección Signing & En la pestaña Capacidades, verifica que tu equipo y el identificador de paquete estén configurados.

Agrega el grupo de apps a ambos, el objetivo Runner y el objetivo NewsWidgetExtension en Xcode:

Selecciona + Capacidad -> Grupos de apps y agrega uno nuevo. Repite el proceso para el objetivo del ejecutor (app superior) y el destino del widget.

135e1a8c4652dac.png

Agrega el código Dart

Tanto las apps para iOS como las de Android pueden compartir datos con una app de Flutter de diferentes maneras.Para comunicarte con estas apps, aprovecha la tienda local key/value del dispositivo. iOS llama a esta tienda UserDefaults, mientras que Android lo llama SharedPreferences. El paquete home_widget une estas APIs para simplificar el guardado de datos en cualquiera de las plataformas y permite que los widgets de la pantalla principal extraigan datos actualizados.

707ae86f6650ac55.png

Los datos del título y la descripción provienen del archivo news_data.dart. Este archivo contiene datos ficticios y una clase de datos 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,
  });
}

Cómo actualizar los valores del título y la descripción

Para agregar la funcionalidad que te permite actualizar el widget de la pantalla principal desde tu app creada con Flutter, navega al archivo lib/home_screen.dart. Reemplaza el contenido del archivo con el siguiente código. Luego, reemplaza <YOUR APP GROUP> por el identificador de tu grupo de apps.

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);
                    },
                  ),
                );
              },
            );
          },
        ));
  }
}

La función updateHeadline guarda los pares clave-valor en el almacenamiento local de tu dispositivo. La clave headline_title contiene el valor de newHeadline.title. La clave headline_description contiene el valor de newHeadline.description. La función también notifica a la plataforma nativa que se pueden recuperar y renderizar nuevos datos para los widgets de la pantalla de inicio.

Modifica el elementofloatActionButton

Llama a la función updateHeadline cuando presiones floatingActionButton, como se muestra a continuación:

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'),
      ),
...

Con este cambio, cuando un usuario presiona el botón Update Headline desde la página de un artículo, se actualizan los detalles del widget de la pantalla principal.

Actualiza el código de iOS para mostrar los datos del artículo

Para actualizar el widget de la pantalla principal para iOS, usa Xcode.

Abre el archivo NewsWidgets.swift en Xcode:

Configura TimelineEntry.

Reemplaza la struct SimpleEntry por el siguiente código:

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
}

Esta struct NewsArticleEntry define los datos entrantes que se pasarán al widget de la pantalla principal cuando se actualice. El tipo TimelineEntry requiere un parámetro de fecha.Para obtener más información sobre el protocolo TimelineEntry, consulta la documentación de TimeEntry de Apple.

Edita el objeto View que muestra el contenido.

Modifica el widget de la pantalla principal para que muestre el título y la descripción del artículo de noticias en lugar de la fecha. Para mostrar texto en SwiftUI, usa la vista Text. Para apilar vistas una encima de la otra en SwiftUI, usa la vista VStack.

Reemplaza la vista NewsWidgetEntryView generada por el siguiente código:

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

Edita el proveedor para indicarle al widget de la pantalla principal cuándo y cómo debe actualizarse

Reemplaza el Provider existente por el siguiente código. Luego, sustituye el identificador de tu grupo de apps por <YOUR APP GROUP>:

ios/NewsWidgets/NewsWidgets.swift

struct Provider: TimelineProvider {

// Placeholder is used as a placeholder when the widget is first displayed
    func placeholder(in context: Context) -> NewsArticleEntry {
//      Add some placeholder title and description, and get the current date
      NewsArticleEntry(date: Date(), title: "Placeholder Title", description: "Placeholder description")
    }

// Snapshot entry represents the current time and state
    func getSnapshot(in context: Context, completion: @escaping (NewsArticleEntry) -> ()) {
      let entry: NewsArticleEntry
      if context.isPreview{
        entry = placeholder(in: context)
      }
      else{
        //      Get the data from the user defaults to display
        let userDefaults = UserDefaults(suiteName: <YOUR APP GROUP>)
        let title = userDefaults?.string(forKey: "headline_title") ?? "No Title Set"
        let description = userDefaults?.string(forKey: "headline_description") ?? "No Description Set"
        entry = NewsArticleEntry(date: Date(), title: title, description: description)
      }
        completion(entry)
    }

//    getTimeline is called for the current and optionally future times to update the widget
    func getTimeline(in context: Context, completion: @escaping (Timeline<Entry>) -> ()) {
//      This just uses the snapshot function you defined earlier
      getSnapshot(in: context) { (entry) in
// atEnd policy tells widgetkit to request a new entry after the date has passed
        let timeline = Timeline(entries: [entry], policy: .atEnd)
                  completion(timeline)
              }
    }
}

El Provider del código anterior se ajusta a un TimelineProvider. Provider tiene tres métodos diferentes:

  1. El método placeholder genera una entrada de marcador de posición cuando el usuario obtiene una vista previa del widget de la pantalla principal por primera vez.

45a0f64240c12efe.png

  1. El método getSnapshot lee los datos de los valores predeterminados del usuario y genera la entrada para la hora actual.
  2. El método getTimeline muestra entradas de cronograma. Esto es útil cuando tienes momentos predecibles para actualizar tu contenido. En este codelab, se usa la función getSnapshot para obtener el estado actual. El método .atEnd le indica al widget de la pantalla principal que actualice los datos una vez transcurrido el tiempo actual.

Comenta en NewsWidgets_Previews

El uso de vistas previas está fuera del alcance de este codelab. Consulta la documentación de Apple sobre widgets de depuración para conocer más detalles sobre la vista previa de los widgets de la pantalla principal de SwiftUI.

Guarda todos los archivos y vuelve a ejecutar el destino de la app y el widget.

Vuelve a ejecutar los objetivos para validar que la app y el widget de la pantalla principal funcionen.

  1. Selecciona el esquema de la app en Xcode para ejecutar el destino de la app.
  2. Selecciona el esquema de extensión en Xcode para ejecutar el destino de la extensión.
  3. Navega a la página de un artículo en la app.
  4. Haz clic en el botón para actualizar el título. El widget de la pantalla principal también debe actualizar el título.

Cómo actualizar el código de Android

Agrega el XML del widget de la pantalla principal.

En Android Studio, actualiza los archivos generados en el paso anterior.Abre el archivo res/layout/news_widget.xml. Define la estructura y el diseño del widget de la pantalla principal. Selecciona Code en la esquina superior derecha y reemplaza el contenido de ese archivo con el siguiente código:

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>

Este XML define dos vistas de texto: una para el título del artículo y la otra para la descripción. Estas vistas de texto también definen el estilo. Regresarás a este archivo a lo largo de este codelab.

Cómo actualizar la funcionalidad del NewsWidget

Abre el archivo de código fuente de Kotlin NewsWidget.kt. Este archivo contiene una clase generada llamada NewsWidget que extiende la clase AppWidgetProvider.

La clase NewsWidget contiene tres métodos de su superclase. Modificarás el método onUpdate. Android llama a este método para los widgets a intervalos fijos.

Reemplaza el contenido del archivo NewsWidget.kt con el siguiente código:

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

Ahora, cuando se llame a onUpdate, Android obtendrá los valores más recientes del almacenamiento local usando el método the widgetData.getString() y, luego, llamará a setTextViewText para cambiar el texto que se muestra en el widget de la pantalla principal.

Prueba las actualizaciones

Prueba la app para asegurarte de que los widgets de la pantalla principal se actualicen con datos nuevos. Para actualizar los datos, usa la opción Actualizar pantalla principal FloatingActionButton en las páginas de los artículos. El widget de la pantalla principal debería actualizarse con el título del artículo.

5ce1c9914b43ad79.png

5. Cómo usar fuentes personalizadas de la app de Flutter en el widget de la pantalla principal de iOS

Hasta ahora, configuraste el widget de la pantalla de inicio para leer los datos que proporciona la app de Flutter. La app de Flutter incluye una fuente personalizada que te recomendamos usar en el widget de la pantalla principal. Puedes usar la fuente personalizada en el widget de la pantalla principal de tu dispositivo iOS. El uso de fuentes personalizadas en los widgets de la pantalla de inicio no está disponible en Android.

Actualiza el código de iOS

Flutter almacena sus recursos en el mainBundle de las aplicaciones para iOS. Puedes acceder a los recursos de este paquete desde el código del widget para la pantalla principal.

En la estructura NewsWidgetsEntryView de tu archivo NewsWidgets.swift, realiza los siguientes cambios.

Crea una función auxiliar para obtener la ruta de acceso al directorio de recursos de 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
       }
   ...
}

Registra la fuente usando la URL del archivo de fuentes personalizada.

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

Actualiza la vista Texto del título para usar tu fuente personalizada.

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

Cuando ejecutes el widget de la pantalla principal, este ahora usará la fuente personalizada para el título, como se muestra en la siguiente imagen:

93f8b9d767aacfb2.png

6. Cómo renderizar widgets de Flutter como una imagen

En esta sección, mostrarás un gráfico de tu app de Flutter como un widget de la pantalla principal.

Este widget supone un desafío mayor que el texto que se mostró en la pantalla principal. Es mucho más fácil mostrar el gráfico de Flutter como una imagen que intentar recrearlo con componentes nativos de la IU.

Programa el widget de la pantalla principal para renderizar tu gráfico de Flutter como un archivo PNG. El widget de la pantalla principal puede mostrar esa imagen.

Cómo escribir el código Dart

En Dart, agrega el método renderFlutterWidget desde el paquete home_widget. Este método toma un widget, un nombre de archivo y una clave. Devuelve una imagen del widget de Flutter y la guarda en un contenedor compartido. Proporciona el nombre de la imagen en tu código y asegúrate de que el widget de la pantalla principal pueda acceder al contenedor. key guarda la ruta de acceso completa al archivo como una cadena en el almacenamiento local del dispositivo. Esto permite que el widget de la pantalla principal encuentre el archivo si el nombre cambia en el código Dart.

En este codelab, la clase LineChart del archivo lib/article_screen.dart representa el gráfico. Su método de compilación muestra un CustomPainter que pinta este gráfico en la pantalla.

Para implementar esta función, abre el archivo lib/article_screen.dart. Importa el paquete home_widget. Luego, reemplaza el código de la clase _ArticleScreenState con el siguiente código:

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!),
        ],
      ),
    );
  }
}

En este ejemplo, se realizan tres cambios en la clase _ArticleScreenState.

Crea una GlobalKey

El GlobalKey obtiene el contexto del widget específico, que es necesario para obtener el tamaño de ese widget .

lib/article_screen.dart

class _ArticleScreenState extends State<ArticleScreen> {
   // New: add this GlobalKey
   final _globalKey = GlobalKey();
   ...
}

Agrega imagePath

La propiedad imagePath almacena la ubicación de la imagen en la que se renderiza el widget de Flutter.

lib/article_screen.dart

class _ArticleScreenState extends State<ArticleScreen> {
   ...
   // New: add this imagePath
   String? imagePath;
   ...
}

Agrega la clave al widget que se renderizará

El _globalKey contiene el widget de Flutter que se renderiza en la imagen. En este caso, el widget de Flutter es el centro que contiene el LineChart.

lib/article_screen.dart

class _ArticleScreenState extends State<ArticleScreen> {
   ...
   Center(
      // New: Add this key
 key: _globalKey,
 child: const LineChart(),
   ),
   ...
}
  1. Guarda el widget como una imagen

Cuando el usuario hace clic en floatingActionButton, se llama al método renderFlutterWidget. El método guarda el archivo PNG resultante como "captura de pantalla" al directorio del contenedor compartido. El método también guarda la ruta de acceso completa a la imagen como la clave del nombre de archivo en el almacenamiento del dispositivo.

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

Actualiza el código de iOS

En iOS, actualiza el código para obtener la ruta de acceso al archivo del almacenamiento y muestra el archivo como una imagen con SwiftUI.

Abre el archivo NewsWidgets.swift para realizar los siguientes cambios:

Agrega filename y displaySize a la struct NewsArticleEntry.

La propiedad filename contiene la cadena que representa la ruta al archivo de imagen. La propiedad displaySize contiene el tamaño del widget de la pantalla principal del dispositivo del usuario. El tamaño del widget de la pantalla principal proviene del context.

ios/NewsWidgets/NewsWidgets.swift

struct NewsArticleEntry: TimelineEntry {
   ...

   // New: add the filename and displaySize.
   let filename: String
   let displaySize: CGSize
}

Actualiza la función placeholder

Incluye un marcador de posición filename y 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)
    }

Obtén el nombre del archivo de userDefaults en getSnapshot

Esto establece la variable filename en el valor filename en el almacenamiento userDefaults cuando se actualiza el widget de la pantalla principal.

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

Crea una imagen de ChartImage que muestre la imagen de una ruta de acceso.

La vista ChartImage crea una imagen a partir del contenido del archivo generado en Dart. Aquí, estableces el tamaño en el 50% del marco.

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

Usa ChartImage en el cuerpo de NewsWidgetsEntryView.

Agrega la vista ChartImage al cuerpo de NewsWidgetsEntryView para mostrar la ChartImage en el widget de la pantalla principal.

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
}

Prueba los cambios

Para probar los cambios, vuelve a ejecutar el objetivo de la app de Flutter (Runner) y el de la extensión desde Xcode. Para ver la imagen, navega a una de las páginas del artículo en la app y presiona el botón para actualizar el widget de la pantalla principal.

33bdfe2cce908c48.png

Cómo actualizar el código de Android

El código de Android funciona igual que el código de iOS.

  1. Abre el archivo android/app/res/layout/news_widget.xml. Contiene los elementos de la IU del widget de la pantalla principal. Reemplaza su contenido con el siguiente código:

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>

Este código nuevo agrega una imagen al widget de la pantalla de inicio, que (por ahora) muestra un ícono de estrella genérico. Reemplaza este ícono de estrella por la imagen que guardaste en el código Dart.

  1. Abre el archivo NewsWidget.kt. Reemplaza su contenido con el siguiente código:

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

Este código Dart guarda una captura de pantalla en el almacenamiento local con la clave filename. También obtiene la ruta de acceso completa de la imagen y crea un objeto File a partir de ella. Si la imagen existe, el código Dart reemplaza la imagen del widget de la pantalla de inicio por la imagen nueva.

  1. Vuelve a cargar tu app y navega a la pantalla de un artículo. Presiona Actualizar pantalla principal. El widget de la pantalla principal muestra el gráfico.

7. Próximos pasos

¡Felicitaciones!

¡Felicitaciones! Lograste crear widgets de la pantalla de inicio para tus apps de Flutter para iOS y Android.

Cómo establecer vínculos a contenido en tu app creada con Flutter

Es posible que desees dirigir al usuario a una página específica de tu aplicación, según dónde haga clic. Por ejemplo, en la app de noticias de este codelab, es posible que quieras que el usuario vea el artículo de noticias del titular que se muestra.

Esta función no se incluye en este codelab. Puedes encontrar ejemplos de cómo usar una transmisión que proporciona el paquete home_widget para identificar los inicios de la app desde los widgets de la pantalla principal y enviar mensajes desde ese widget a través de la URL. Para obtener más información, consulta la documentación sobre vínculos directos en docs.flutter.dev.

Actualiza el widget en segundo plano

En este codelab, activaste una actualización del widget de la pantalla principal con un botón. Si bien esto es razonable para realizar pruebas, en el código de producción, es posible que desees que tu app actualice el widget de la pantalla principal en segundo plano. Puedes usar el complemento de Workmanager para crear tareas en segundo plano y actualizar los recursos que necesita el widget de pantalla principal. Para obtener más información, consulta la sección Background update en el paquete home_widget.

En iOS, también puedes hacer que el widget de la pantalla principal realice una solicitud de red para actualizar su IU. Para controlar las condiciones o la frecuencia de esa solicitud, usa la función Rutas. Para obtener más información sobre el uso de Rutas, consulta "Cómo mantener un widget actualizado" de Apple. en la documentación de Google Cloud.

Lecturas adicionales