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 hace referencia a los componentes de la IU creados con el framework 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 abrirla. En Android, los widgets se encuentran en la pantalla principal. En iOS, se pueden agregar a la pantalla principal, la pantalla de bloqueo o la vista Hoy.

f0027e8a7d0237e0.png b991e79ea72c8b65.png

¿Qué tan complejo puede ser un widget?

La mayoría de los widgets de la pantalla principal son sencillos. 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

Crea la IU para los widgets

Debido a estas limitaciones de la IU, no puedes dibujar directamente la IU de un widget de la pantalla principal con el framework de Flutter. En cambio, 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, y así evitar reescribir IU complejas.

Qué compilarás

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

  • Mostrar datos de tu app de Flutter
  • Mostrar texto con recursos de fuentes 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.
  • La segunda muestra el artículo completo con un gráfico creado con CustomPaint.

.

9c02f8b62c1faa3a.png d97d44051304cae4.png

Qué aprenderás

  • Cómo crear widgets para la pantalla principal en iOS y Android
  • Cómo usar el paquete home_widget para compartir datos entre el widget de la pantalla principal y la app de Flutter
  • Cómo reducir la cantidad de código que necesitas volver a escribir
  • 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. Puede ser Visual Studio Code con las extensiones de Flutter y Dart Code, o bien Android Studio o IntelliJ con los complementos de Flutter y Dart instalados.

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

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

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

  • 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. Esto instala el compilador necesario para compilar la versión para Android de tu app.

Obtén el código de partida

Descarga la versión inicial de tu proyecto desde GitHub

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

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

Después de clonar el repositorio, puedes encontrar el código de 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 inicio

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

Instalar paquetes

Todos los paquetes requeridos se agregaron 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 principal con las herramientas nativas de la plataforma.

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

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

  1. Ejecuta open ios/Runner.xcworkspace en una ventana de terminal desde el directorio de tu proyecto de Flutter. Como alternativa, haz clic con el botón derecho en la carpeta ios de VS Code y selecciona Open in Xcode. Se abrirá el espacio de trabajo predeterminado de Xcode en tu proyecto de Flutter.
  2. Selecciona File → New → Target en el menú. Esta acción agrega un nuevo destino al proyecto.
  3. Aparecerá una lista de plantillas. Selecciona Widget Extension.
  4. Escribe "NewsWidgets" en el cuadro Nombre del producto para este widget. Desmarca las casillas de verificación Include Live Activity y Include Configuration Intent.

Inspecciona el código de muestra

Cuando agregas un nuevo destino, Xcode genera 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 de la extensión de la app de Apple.

Depura y prueba tu widget de muestra

  1. Primero, actualiza la configuración de tu app de Flutter. Debes hacerlo cuando agregues paquetes nuevos a tu app de Flutter y planees 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 ver una lista de destinos. Selecciona el destino del widget que acabas de crear, NewsWidgets, y haz clic en Ejecutar. Ejecuta el destino del widget desde Xcode cuando cambies el código del widget para iOS.

bbb519df1782881d.png

  1. La pantalla del simulador o del dispositivo debe mostrar un widget básico de la pantalla principal. Si no lo ves, puedes agregarlo a la pantalla. Mantén presionado en la pantalla principal y, luego, haz clic en el signo + en la esquina superior izquierda.

18eff1cae152014d.png

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

a0c00df87615493e.png

  1. Una vez que agregues el widget de la pantalla principal, debería mostrar un texto simple con la hora.

Cómo crear un widget básico para Android

  1. Para agregar un widget de 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. Después de que se compile el proyecto, busca el directorio de la app en la esquina superior izquierda. Agrega el nuevo widget de la pantalla principal a este directorio. Haz clic con el botón derecho en el directorio y selecciona New -> Widget -> App Widget.

f19d8b7f95ab884e.png

  1. Android Studio muestra un nuevo formulario. Agrega información básica sobre el widget de la pantalla principal, como el nombre de la clase, la ubicación, el tamaño y el idioma de origen.

En este codelab, establece los siguientes valores:

  • Cuadro Class Name a NewsWidget
  • En el menú desplegable Ancho mínimo (celdas), selecciona 3.
  • Menú desplegable Minimum Height (cells) en 3

Inspecciona 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 widget de Noticias.

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. En este archivo, puedes ajustar las dimensiones o el nombre de tu widget.

Crear

java/com/example/homescreen_widgets/NewsWidget.kt

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

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

Depura y prueba tu widget de muestra

Ahora, ejecuta tu aplicación y mira 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 predeterminado de la pantalla principal para Android.

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

Puedes personalizar el widget básico de la pantalla principal que creaste. Actualiza el widget de la pantalla principal para mostrar un titular y un resumen de un artículo de noticias. En la siguiente captura de pantalla, se muestra un ejemplo del widget de la pantalla principal que muestra 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 el proceso en tres partes:

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

Cómo usar grupos de apps para iOS

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

Actualiza el identificador del paquete:

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

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

Selecciona + Capability -> App Groups y agrega un nuevo grupo de apps. Repite el proceso para el destino de Runner (app principal) y el destino del widget.

135e1a8c4652dac.png

Agrega el código de Dart

Las apps para iOS y Android pueden compartir datos con una app de Flutter de varias maneras.Para comunicarse con estas apps, aprovecha el almacén key/value local del dispositivo. iOS llama a este almacén UserDefaults, y Android lo llama SharedPreferences. El paquete home_widget encapsula 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 simulados 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,
  });
}

Actualiza los valores del título y la descripción

Para agregar la función que actualiza el widget de la pantalla principal desde tu app de Flutter, navega al archivo lib/home_screen.dart. Reemplaza el contenido del archivo por el siguiente código. Luego, reemplaza <YOUR APP GROUP> por el identificador de tu grupo de aplicaciones.

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 datos nuevos para los widgets de la pantalla principal.

Modifica el elemento floatingActionButton

Llama a la función updateHeadline cuando se presione 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 Actualizar título en 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 estructura 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 estructura 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 TimelineEntry de Apple.

Edita el 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 sobre 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 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 devuelve entradas de la línea de tiempo. Esto resulta útil cuando tienes puntos temporales predecibles para actualizar tu contenido. En este codelab, se usa la función getSnapshot para obtener el estado actual. El método.atEndle indica al widget de la pantalla principal que actualice los datos después de que pase la hora actual.

Comenta el NewsWidgets_Previews.

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

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

Vuelve a ejecutar los destinos 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 la 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 debería actualizar el título.

Actualiza 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 Código en la esquina superior derecha y reemplaza el contenido de ese archivo por 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 otra para la descripción del artículo. Estas vistas de texto también definen el diseño. Volverás a este archivo a lo largo del codelab.

Actualiza la funcionalidad de 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 en intervalos fijos.

Reemplaza el contenido del archivo NewsWidget.kt por 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 llama a onUpdate, Android obtiene los valores más recientes del almacenamiento local con el método the widgetData.getString() y, luego, llama 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 el Update Home Screen 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 principal para que lea los datos que proporciona la app de Flutter. La app de Flutter incluye una fuente personalizada que tal vez quieras usar en el widget de la pantalla principal. Puedes usar la fuente personalizada en el widget de la pantalla principal de iOS. El uso de fuentes personalizadas en los widgets de la pantalla principal 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 de la pantalla principal.

En la struct 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 con la URL de tu archivo de fuente 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 de 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 ejecutas el widget de la pantalla principal, ahora usa la fuente personalizada para el encabezado, 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 presenta un desafío mayor que el texto que se muestra en la pantalla principal. Es mucho más fácil mostrar el gráfico de Flutter como una imagen que intentar recrearlo con componentes de IU nativos.

Codifica tu 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.

Escribe el código de Dart

En el lado de Dart, agrega el método renderFlutterWidget del 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. El método key guarda la ruta de acceso completa del 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 de Dart.

En este codelab, la clase LineChart en el archivo lib/article_screen.dart representa el gráfico. Su método de compilación devuelve 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. A continuación, reemplaza el código de la clase _ArticleScreenState por el siguiente:

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

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 para renderizar

El _globalKey contiene el widget de Flutter que se renderiza en la imagen. En este caso, el widget de Flutter es el Center 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

Se llama al método renderFlutterWidget cuando el usuario hace clic en el floatingActionButton. El método guarda el archivo PNG resultante como "screenshot" en el 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 desde el almacenamiento y mostrar 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 de acceso al archivo de imagen. La propiedad displaySize contiene el tamaño del widget de la pantalla principal en el dispositivo del usuario. El tamaño del widget de la pantalla principal proviene de 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 marcadores 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 de archivo de userDefaults en getSnapshot.

Esto establece la variable filename en el valor filename del 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 un ChartImage que muestre la imagen desde una ruta de acceso

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

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 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 destino de tu app de Flutter (Runner) y el destino de tu 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

Actualiza el código de Android

El código de Android funciona de la misma manera 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 por 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 nuevo código agrega una imagen al widget de la pantalla principal, que (por ahora) muestra un ícono de estrella genérico. Reemplaza este ícono de estrella por la imagen que guardaste en el código de Dart.

  1. Abre el archivo NewsWidget.kt. Reemplaza su contenido por 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 de Dart reemplaza la imagen del widget de la pantalla principal por la imagen nueva.

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

7. Próximos pasos

¡Felicitaciones!

Felicitaciones. Lograste crear widgets para la pantalla principal de tus apps de Flutter para iOS y Android.

Cómo vincular contenido en tu app de Flutter

Es posible que desees dirigir al usuario a una página específica de tu app, 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 título que se muestra.

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

Cómo actualizar 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 las 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 WorkManager para crear tareas en segundo plano que actualicen los recursos que necesita el widget de la pantalla principal. Para obtener más información, consulta la sección Actualización en segundo plano del 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 Rutas. Para obtener más información sobre el uso de la línea de tiempo, consulta la documentación de Apple sobre cómo mantener un widget actualizado.

Lecturas adicionales