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.
¿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.
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.
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
.
.
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:
- 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. - Selecciona File → New → Target en el menú. Esto agrega un destino nuevo al proyecto.
- Aparecerá una lista de plantillas. Selecciona Extensión de widgets.
- 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
- 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
- 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.
- 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.
- Busca el nombre de la app. Para este codelab, busca "Widgets de la pantalla principal".
- 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
- 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.
- 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
- 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 |
| Agrega un nuevo receptor que registra el NewsWidget. |
Crear |
| Define la IU del widget de la pantalla principal. |
Crear |
| Define la configuración del widget de la pantalla principal. Puedes ajustar las dimensiones o el nombre de tu widget en este archivo. |
Crear |
| 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.
El dispositivo o emulador Android muestra el widget de la pantalla principal predeterminado para Android.
4. Envía datos de la 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.
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:
- Escribir código de Dart en tu app de Flutter que pueda usar tanto en Android como en iOS
- Cómo agregar funcionalidad nativa de iOS
- 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.
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.
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:
- 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.
- El método
getSnapshot
lee los datos de los valores predeterminados del usuario y genera la entrada para la hora actual. - 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.
- Selecciona el esquema de la app en Xcode para ejecutar el destino de la app.
- Selecciona el esquema de extensión en Xcode para ejecutar el destino de la extensión.
- Navega a la página de un artículo en la app.
- 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.
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:
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(),
),
...
}
- 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.
Cómo actualizar el código de Android
El código de Android funciona igual que el código de iOS.
- 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.
- 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.
- 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.