Agrega WebView a tu app de Flutter

1. Introducción

Última actualización: 19‑10‑2021

Con el complemento de WebView de Flutter, puedes agregar un widget de WebView a tu app de Flutter para Android o iOS. En iOS, este widget funciona con WKWebView, mientras que, en Android, funciona con WebView. El complemento puede renderizar widgets de Flutter en la vista web. Por ejemplo, es posible renderizar un menú desplegable en la vista web.

Qué compilarás

En este codelab, compilarás una app para dispositivos móviles paso a paso mediante un WebView con el SDK de Flutter. Tu app hará lo siguiente:

  • Mostrará contenido web en un objeto WebView.
  • Mostrará los widgets de Flutter apilados sobre el objeto WebView.
  • Reaccionará a los eventos de progreso de carga de la página.
  • Controlará la WebView mediante WebViewController.
  • Bloqueará sitios web con el NavigationDelegate.
  • Evaluará expresiones de JavaScript.
  • Controlará las devoluciones de llamada de JavaScript con los JavascriptChannels.
  • Establecerá, quitará, agregará o mostrará cookies.
  • Cargará y mostrará el código HTML de los elementos, las strings o los archivos que contengan HTML.

Qué aprenderás

En este codelab, aprenderás a usar el complemento webview_flutter de varias maneras, incluidas las siguientes:

  • Cómo configurar el complemento webview_flutter
  • Cómo detectar eventos de progreso de carga de página
  • Cómo controlar la navegación de las páginas
  • Cómo indicarle a la WebView que retroceda y avance por su historial
  • Cómo evaluar JavaScript, incluido el uso de resultados mostrados
  • Cómo registrar devoluciones de llamada para llamar al código en Dart desde JavaScript
  • Cómo administrar cookies
  • Cómo cargar y mostrar páginas HTML de elementos, archivos o strings que contienen HTML

Requisitos

2. Configura tu entorno de Flutter

Para completar este lab, necesitas dos programas de software: el SDK de Flutter y un editor.

Puedes ejecutar este codelab con cualquiera de los siguientes dispositivos:

  • Un dispositivo móvil físico (Android o iOS) conectado a tu computadora y configurado en modo de desarrollador
  • El simulador de iOS (solo para macOS; requiere instalar las herramientas de Xcode)
  • Android Emulator (requiere configuración en Android Studio)

3. Primeros pasos

Primeros pasos con Flutter

Existen varias maneras de crear un nuevo proyecto de Flutter, ya que Android Studio y Visual Studio Code proporcionan herramientas para hacerlo. Sigue los procedimientos vinculados para crear un proyecto o ejecuta los siguientes comandos en una terminal de línea de comandos útil.

$ flutter create webview_in_flutter
Creating project webview_in_flutter...
[Listing of created files elided]
Wrote 81 files.

All done!
In order to run your application, type:

  $ cd webview_in_flutter
  $ flutter run

Your application code is in webview_in_flutter\lib\main.dart.

Agrega el complemento de WebView de Flutter como dependencia

Agregar funciones adicionales a una app creada con Flutter es muy fácil gracias a los paquetes de Pub. En este codelab, agregarás el complemento webview_flutter a tu proyecto. Ejecuta los siguientes comandos en la terminal.

$ cd webview_in_flutter
$ flutter pub add webview_flutter
Resolving dependencies...
  async 2.8.1 (2.8.2 available)
  characters 1.1.0 (1.2.0 available)
  matcher 0.12.10 (0.12.11 available)
+ plugin_platform_interface 2.0.2
  test_api 0.4.2 (0.4.8 available)
  vector_math 2.1.0 (2.1.1 available)
+ webview_flutter 3.0.0
+ webview_flutter_android 2.8.0
+ webview_flutter_platform_interface 1.8.0
+ webview_flutter_wkwebview 2.7.0
Downloading webview_flutter 3.0.0...
Downloading webview_flutter_wkwebview 2.7.0...
Downloading webview_flutter_android 2.8.0...
Changed 5 dependencies!

Si inspeccionas el archivo pubspec.yaml, verás que tiene una línea en la sección de dependencias para el complemento webview_flutter.

Configura el SDK mínimo de Android

Para usar el complemento webview_flutter en Android, debes configurar minSDK en 19 o 20, según la vista de la plataforma de Android que quieras usar. Obtén más información sobre la vista de la plataforma de Android en la página del complemento webview_flutter. Modifica tu archivo android/app/build.gradle de la siguiente manera:

android/app/build.gradle

defaultConfig {
    // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
    applicationId "com.example.webview_in_flutter"
    minSdkVersion 20        // MODIFY
    targetSdkVersion 30
    versionCode flutterVersionCode.toInteger()
    versionName flutterVersionName
}

4. Agrega el widget de WebView a la app de Flutter

En este paso, agregarás una WebView a tu aplicación. Las WebViews son vistas nativas alojadas. Tú, como desarrollador, puedes elegir cómo alojarlas en tu app. En Android, puedes elegir entre las pantallas virtuales (opción predeterminada actual para este SO) y la composición híbrida. Sin embargo, en iOS siempre se usa la composición híbrida.

Para obtener un análisis detallado de las diferencias entre las pantallas virtuales y la composición híbrida, lee la documentación sobre cómo alojar vistas nativas de iOS y Android en tu app de Flutter con vistas de la plataforma.

Muestra una WebView en la pantalla

Reemplaza el contenido de lib/main.dart con la siguiente información:

lib/main.dart

import 'package:flutter/material.dart';
import 'package:webview_flutter/webview_flutter.dart';

void main() {
  runApp(
    const MaterialApp(
      home: WebViewApp(),
    ),
  );
}

class WebViewApp extends StatefulWidget {
  const WebViewApp({Key? key}) : super(key: key);

  @override
  State<WebViewApp> createState() => _WebViewAppState();
}

class _WebViewAppState extends State<WebViewApp> {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Flutter WebView'),
      ),
      body: const WebView(
        initialUrl: 'https://flutter.dev',
      ),
    );
  }
}

Si se ejecuta en iOS o Android, aparecerá un WebView como una ventana del navegador con sangría completa, lo que significa que el navegador se muestra en pantalla completa en el dispositivo sin ningún tipo de borde o margen. A medida que te desplaces, notarás partes de la página que podrían tener un aspecto extraño. Esto se debe a que se requiere JavaScript para renderizar flutter.dev correctamente, pero se encuentra inhabilitado.

Habilita la composición híbrida

Si quieres usar el modo de composición híbrida en dispositivos Android, puedes hacerlo con algunas modificaciones menores. Modifica el archivo lib/main.dart de la siguiente manera:

lib/main.dart

import 'dart:io';                            // Add this import.
import 'package:flutter/material.dart';
import 'package:webview_flutter/webview_flutter.dart';

void main() {
  runApp(
    const MaterialApp(
      home: WebViewApp(),
    ),
  );
}

class WebViewApp extends StatefulWidget {
  const WebViewApp({Key? key}) : super(key: key);

  @override
  State<WebViewApp> createState() => _WebViewAppState();
}

class _WebViewAppState extends State<WebViewApp> {
  // Add from here ...
  @override
  void initState() {
    if (Platform.isAndroid) {
      WebView.platform = SurfaceAndroidWebView();
    }
    super.initState();
  }
  // ... to here.

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Flutter WebView'),
      ),
      body: const WebView(
        initialUrl: 'https://flutter.dev',
      ),
    );
  }
}

No olvides cambiar la minSdkVersion del archivo build.gradle a 19 cuando quieras usar la vista de plataforma de composición híbrida.

Ejecuta la app

Ejecuta la app de Flutter en iOS o Android para ver una WebView en la que muestra el sitio web flutter.dev. También puedes ejecutar la app en un emulador de Android o en un simulador de iOS. Puedes reemplazar la URL inicial de WebView, por ejemplo, por la de tu sitio web.

$ flutter run

Supongamos que se está ejecutando el simulador o emulador adecuado (o que tienes conectado un dispositivo físico). Después de compilar e implementar la app en el dispositivo, deberías ver información similar a la siguiente:

5. Escucha eventos de carga de página

El widget WebView proporciona varios eventos de progreso de carga de la página, y tu app puede escucharlos. Durante el ciclo de carga de la página del objeto WebView, se activan tres eventos diferentes: onPageStarted, onProgress y onPageFinished. En este paso, implementarás un indicador de carga de página. Además, demostrarás que puedes renderizar contenido de Flutter en el área de contenido del objeto WebView.

Agrega eventos de carga de página a tu app

Crea un nuevo archivo de código fuente en la ruta lib/src/web_view_stack.dart y complétalo con el siguiente contenido:

lib/src/web_view_stack.dart

import 'package:flutter/material.dart';
import 'package:webview_flutter/webview_flutter.dart';

class WebViewStack extends StatefulWidget {
  const WebViewStack({Key? key}) : super(key: key);

  @override
  State<WebViewStack> createState() => _WebViewStackState();
}

class _WebViewStackState extends State<WebViewStack> {
  var loadingPercentage = 0;

  @override
  Widget build(BuildContext context) {
    return Stack(
      children: [
        WebView(
          initialUrl: 'https://flutter.dev',
          onPageStarted: (url) {
            setState(() {
              loadingPercentage = 0;
            });
          },
          onProgress: (progress) {
            setState(() {
              loadingPercentage = progress;
            });
          },
          onPageFinished: (url) {
            setState(() {
              loadingPercentage = 100;
            });
          },
        ),
        if (loadingPercentage < 100)
          LinearProgressIndicator(
            value: loadingPercentage / 100.0,
          ),
      ],
    );
  }
}

Este código unió el widget WebView en una Stack y superpone condicionalmente la WebView con un LinearProgressIndicator cuando el porcentaje de carga de la página es inferior al 100%. Como este proceso implica el estado del programa que cambia con el tiempo, debes almacenar este estado en una clase State asociada con un StatefulWidget.

Para usar este widget WebViewStack nuevo, modifica el archivo lib/main.dart de la siguiente manera:

import 'package:flutter/material.dart';
// Delete the package:webview_flutter/webview_flutter.dart import
import 'src/web_view_stack.dart';  // Add this import

void main() {
  runApp(
    const MaterialApp(
      home: WebViewApp(),
    ),
  );
}

class WebViewApp extends StatefulWidget {
  const WebViewApp({Key? key}) : super(key: key);

  @override
  State<WebViewApp> createState() => _WebViewAppState();
}

class _WebViewAppState extends State<WebViewApp> {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Flutter WebView'),
      ),
      body: const WebViewStack(),   // Replace the WebView widget with WebViewStack
    );
  }
}

Cuando ejecutes la app, según las condiciones de la red y si el navegador tiene almacenada en caché la página a la que estás navegando, verás un indicador de carga de página superpuesto en el área de contenido de la WebView.

6. Trabaja con el WebViewController

Accede al WebViewController desde el widget WebView

El widget WebView habilita el control programático mediante WebViewController. Este controlador está disponible después de la construcción del widget WebView por medio de una devolución de llamada. La naturaleza asíncrona de la disponibilidad de este controlador lo convierte en un gran candidato para la clase Completer<T> asíncrona de Dart.

Actualiza lib/src/web_view_stack.dart de la siguiente manera:

lib/src/web_view_stack.dart

import 'dart:async';     // Add this import for Completer
import 'package:flutter/material.dart';
import 'package:webview_flutter/webview_flutter.dart';

class WebViewStack extends StatefulWidget {
  const WebViewStack({required this.controller, Key? key}) : super(key: key); // Modify

  final Completer<WebViewController> controller;   // Add this attribute

  @override
  State<WebViewStack> createState() => _WebViewStackState();
}

class _WebViewStackState extends State<WebViewStack> {
  var loadingPercentage = 0;

  @override
  Widget build(BuildContext context) {
    return Stack(
      children: [
        WebView(
          initialUrl: 'https://flutter.dev',
          // Add from here ...
          onWebViewCreated: (webViewController) {
            widget.controller.complete(webViewController);
          },
          // ... to here.
          onPageStarted: (url) {
            setState(() {
              loadingPercentage = 0;
            });
          },
          onProgress: (progress) {
            setState(() {
              loadingPercentage = progress;
            });
          },
          onPageFinished: (url) {
            setState(() {
              loadingPercentage = 100;
            });
          },
        ),
        if (loadingPercentage < 100)
          LinearProgressIndicator(
            value: loadingPercentage / 100.0,
          ),
      ],
    );
  }
}

El widget WebViewStack ahora publica el controlador que se crea de forma asíncrona con Completer<WebViewController>. Esta es una alternativa más ligera a crear un argumento de función de devolución de llamada a fin de proporcionar el controlador al resto de la app.

Crea controles de navegación

Tener una WebView que funcione es lo esencial, pero también puedes agregar un conjunto de adiciones útiles, como la capacidad de avanzar y retroceder por el historial de páginas, así como de volver a cargar páginas. Afortunadamente, con un WebViewController puedes agregar estas funciones a tu app.

Crea un archivo de origen nuevo en el objeto lib/src/navigation_controls.dart y complétalo con la siguiente información:

lib/src/navigation_controls.dart

import 'dart:async';
import 'package:flutter/material.dart';
import 'package:webview_flutter/webview_flutter.dart';

class NavigationControls extends StatelessWidget {
  const NavigationControls({required this.controller, Key? key})
      : super(key: key);

  final Completer<WebViewController> controller;

  @override
  Widget build(BuildContext context) {
    return FutureBuilder<WebViewController>(
      future: controller.future,
      builder: (context, snapshot) {
        final WebViewController? controller = snapshot.data;
        if (snapshot.connectionState != ConnectionState.done ||
            controller == null) {
          return Row(
            children: const <Widget>[
              Icon(Icons.arrow_back_ios),
              Icon(Icons.arrow_forward_ios),
              Icon(Icons.replay),
            ],
          );
        }

        return Row(
          children: <Widget>[
            IconButton(
              icon: const Icon(Icons.arrow_back_ios),
              onPressed: () async {
                if (await controller.canGoBack()) {
                  await controller.goBack();
                } else {
                  ScaffoldMessenger.of(context).showSnackBar(
                    const SnackBar(content: Text('No back history item')),
                  );
                  return;
                }
              },
            ),
            IconButton(
              icon: const Icon(Icons.arrow_forward_ios),
              onPressed: () async {
                if (await controller.canGoForward()) {
                  await controller.goForward();
                } else {
                  ScaffoldMessenger.of(context).showSnackBar(
                    const SnackBar(content: Text('No forward history item')),
                  );
                  return;
                }
              },
            ),
            IconButton(
              icon: const Icon(Icons.replay),
              onPressed: () {
                controller.reload();
              },
            ),
          ],
        );
      },
    );
  }
}

Este widget usa un widget FutureBuilder<T> para volver a procesar la imagen correctamente cuando el control esté disponible. Se renderiza una fila de tres íconos mientras se espera a que el controlador esté disponible, pero, cuando aparece, se reemplaza por una Row de IconButton con controladores onPressed que usan el controller para implementar su funcionalidad.

Agrega controles de navegación a la AppBar

Con la WebViewStack actualizada y los NavigationControls recién creados, es momento de ubicarlo todo en una WebViewApp actualizada. Anteriormente, viste cómo usar Completer<T>, pero no dónde se creó. Como la WebViewApp se ubica cerca de la parte superior del árbol de widgets de esta app, tiene sentido crearla en este nivel.

Actualiza el archivo lib/main.dart de la siguiente manera:

lib/main.dart

import 'dart:async';                                    // Add this import

import 'package:flutter/material.dart';
import 'package:webview_flutter/webview_flutter.dart';  // Add this import back

import 'src/navigation_controls.dart';                  // Add this import
import 'src/web_view_stack.dart';

void main() {
  runApp(
    const MaterialApp(
      home: WebViewApp(),
    ),
  );
}

class WebViewApp extends StatefulWidget {
  const WebViewApp({Key? key}) : super(key: key);

  @override
  State<WebViewApp> createState() => _WebViewAppState();
}

class _WebViewAppState extends State<WebViewApp> {
  final controller = Completer<WebViewController>();    // Instantiate the controller

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Flutter WebView'),
        // Add from here ...
        actions: [
          NavigationControls(controller: controller),
        ],
        // ... to here.
      ),
      body: WebViewStack(controller: controller),       // Add the controller argument
    );
  }
}

Cuando se ejecute la app, deberías ver una página web con los siguientes controles:

7. Realiza un seguimiento de la navegación con NavigationDelegate

El objeto WebView le proporciona a tu app un elemento NavigationDelegate, que te permite realizar un seguimiento y controlar la navegación de la página del widget WebView. Cuando el objeto WebView, inicia una navegación, por ejemplo, cuando un usuario hace clic en un vínculo, se llama al elemento NavigationDelegate. Se puede usar la devolución de llamada NavigationDelegate para controlar si el objeto WebView continúa con la navegación.

Registra un NavigationDelegate personalizado

En este paso, registrarás una devolución de llamada NavigationDelegate para bloquear la navegación a YouTube.com. Ten en cuenta que esta implementación sencilla también bloquea el contenido intercalado de YouTube, que aparece en varias páginas de la documentación de la API de Flutter.

Actualiza el archivo lib/src/web_view_stack.dart de la siguiente manera:

lib/src/web_view_stack.dart

import 'dart:async';
import 'package:flutter/material.dart';
import 'package:webview_flutter/webview_flutter.dart';

class WebViewStack extends StatefulWidget {
  const WebViewStack({required this.controller, Key? key}) : super(key: key);

  final Completer<WebViewController> controller;

  @override
  State<WebViewStack> createState() => _WebViewStackState();
}

class _WebViewStackState extends State<WebViewStack> {
  var loadingPercentage = 0;

  @override
  Widget build(BuildContext context) {
    return Stack(
      children: [
        WebView(
          initialUrl: 'https://flutter.dev',
          onWebViewCreated: (webViewController) {
            widget.controller.complete(webViewController);
          },
          onPageStarted: (url) {
            setState(() {
              loadingPercentage = 0;
            });
          },
          onProgress: (progress) {
            setState(() {
              loadingPercentage = progress;
            });
          },
          onPageFinished: (url) {
            setState(() {
              loadingPercentage = 100;
            });
          },
          // Add from here ...
          navigationDelegate: (navigation) {
            final host = Uri.parse(navigation.url).host;
            if (host.contains('youtube.com')) {
              ScaffoldMessenger.of(context).showSnackBar(
                SnackBar(
                  content: Text(
                    'Blocking navigation to $host',
                  ),
                ),
              );
              return NavigationDecision.prevent;
            }
            return NavigationDecision.navigate;
          },
          // ... to here.
        ),
        if (loadingPercentage < 100)
          LinearProgressIndicator(
            value: loadingPercentage / 100.0,
          ),
      ],
    );
  }
}

En el siguiente paso, agregarás un elemento de menú para poder probar el NavigationDelegate mediante la clase WebViewController. Se deja como un ejercicio para que el lector mejore la lógica de la devolución de llamada a fin de bloquear solo la navegación de la página completa en YouTube.com, a la vez que se permite el contenido intercalado de YouTube en la documentación de la API.

8. Agrega un botón de menú a la AppBar

En los próximos pasos, crearás un botón de menú en el widget AppBar que se usa para evaluar JavaScript, invocar canales de JavaScript y administrar cookies. En definitiva, es un menú muy útil.

Crea un nuevo archivo de código fuente en la ruta lib/src/menu.dart y complétalo con la siguiente información:

lib/src/menu.dart

import 'dart:async';
import 'package:flutter/material.dart';
import 'package:webview_flutter/webview_flutter.dart';

enum _MenuOptions {
  navigationDelegate,
}

class Menu extends StatelessWidget {
  const Menu({required this.controller, Key? key}) : super(key: key);

  final Completer<WebViewController> controller;

  @override
  Widget build(BuildContext context) {
    return FutureBuilder<WebViewController>(
      future: controller.future,
      builder: (context, controller) {
        return PopupMenuButton<_MenuOptions>(
          onSelected: (value) async {
            switch (value) {
              case _MenuOptions.navigationDelegate:
                controller.data!.loadUrl('https://youtube.com');
                break;
            }
          },
          itemBuilder: (context) => [
            const PopupMenuItem<_MenuOptions>(
              value: _MenuOptions.navigationDelegate,
              child: Text('Navigate to YouTube'),
            ),
          ],
        );
      },
    );
  }
}

Cuando el usuario selecciona la opción de menú Navigate to YouTube, se ejecuta el método loadUrl de WebViewController. La devolución de llamada navigationDelegate que creaste en el paso anterior bloqueará esta navegación.

Para agregar el menú a la pantalla de la WebViewApp, modifica el archivo lib/main.dart de la siguiente manera:

lib/main.dart

import 'dart:async';

import 'package:flutter/material.dart';
import 'package:webview_flutter/webview_flutter.dart';

import 'src/menu.dart';                                // Add this import
import 'src/navigation_controls.dart';
import 'src/web_view_stack.dart';

void main() {
  runApp(
    const MaterialApp(
      home: WebViewApp(),
    ),
  );
}

class WebViewApp extends StatefulWidget {
  const WebViewApp({Key? key}) : super(key: key);

  @override
  State<WebViewApp> createState() => _WebViewAppState();
}

class _WebViewAppState extends State<WebViewApp> {
  final controller = Completer<WebViewController>();

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Flutter WebView'),
        actions: [
          NavigationControls(controller: controller),
          Menu(controller: controller),                // Add this line
        ],
      ),
      body: WebViewStack(controller: controller),
    );
  }
}

Ejecuta la app y presiona el elemento de menú Navigate to YouTube. Deberías ver una barra de notificaciones en la que se te informe que el controlador de navegación bloqueó la navegación a YouTube.

9. Evalúa JavaScript

El WebViewController puede evaluar expresiones de JavaScript en el contexto de la página actual. Hay dos maneras diferentes de evaluar JavaScript. Si tienes código en JavaScript que no muestra un valor, usa runJavaScript. Si el código muestra un valor, usa runJavaScriptReturningResult.

Para habilitar JavaScript, debes configurar el widget WebView con la propiedad javaScriptMode establecida en JavascriptMode.unrestricted. De forma predeterminada, javascriptMode se configura en JavascriptMode.disabled.

Para actualizar la clase _WebViewStackState, agrega el parámetro de configuración javascriptMode de la siguiente manera:

lib/src/web_view_stack.dart

class _WebViewStackState extends State<WebViewStack> {
  var loadingPercentage = 0;

  @override
  Widget build(BuildContext context) {
    return Stack(
      children: [
        WebView(
          initialUrl: 'https://flutter.dev',
          onWebViewCreated: (webViewController) {
            widget.controller.complete(webViewController);
          },
          onPageStarted: (url) {
            setState(() {
              loadingPercentage = 0;
            });
          },
          onProgress: (progress) {
            setState(() {
              loadingPercentage = progress;
            });
          },
          onPageFinished: (url) {
            setState(() {
              loadingPercentage = 100;
            });
          },
          navigationDelegate: (navigation) {
            final host = Uri.parse(navigation.url).host;
            if (host.contains('youtube.com')) {
              ScaffoldMessenger.of(context).showSnackBar(
                SnackBar(
                  content: Text(
                    'Blocking navigation to $host',
                  ),
                ),
              );
              return NavigationDecision.prevent;
            }
            return NavigationDecision.navigate;
          },
          javascriptMode: JavascriptMode.unrestricted,        // Add this line
        ),
        if (loadingPercentage < 100)
          LinearProgressIndicator(
            value: loadingPercentage / 100.0,
          ),
      ],
    );
  }
}

Ahora que la WebView puede ejecutar JavaScript, puedes agregar una opción al menú para usar el método runJavaScriptReturningResult.

Modifica el archivo lib/src/menu.dart de la siguiente manera:

lib/src/menu.dart

enum _MenuOptions {
  navigationDelegate,
  userAgent,                                          // Add this line
}

class Menu extends StatelessWidget {
  const Menu({required this.controller, Key? key}) : super(key: key);

  final Completer<WebViewController> controller;

  @override
  Widget build(BuildContext context) {
    return FutureBuilder<WebViewController>(
      future: controller.future,
      builder: (context, controller) {
        return PopupMenuButton<_MenuOptions>(
          onSelected: (value) async {
            switch (value) {
              case _MenuOptions.navigationDelegate:
                controller.data!.loadUrl('https://youtube.com');
                break;
              // Add from here ...
              case _MenuOptions.userAgent:
                final userAgent = await controller.data!
                    .runJavascriptReturningResult('navigator.userAgent');
                ScaffoldMessenger.of(context).showSnackBar(SnackBar(
                  content: Text(userAgent),
                ));
                break;
              // ... to here.
            }
          },
          itemBuilder: (context) => [
            const PopupMenuItem<_MenuOptions>(
              value: _MenuOptions.navigationDelegate,
              child: Text('Navigate to YouTube'),
            ),
            // Add from here ...
            const PopupMenuItem<_MenuOptions>(
              value: _MenuOptions.userAgent,
              child: Text('Show user-agent'),
            ),
            // ... to here.
          ],
        );
      },
    );
  }
}

Cuando presionas la opción del menú “Show user-agent”, el resultado de ejecutar la expresión de JavaScript navigator.userAgent se muestra en una Snackbar. Cuando ejecutes la app, es posible que notes que la página de Flutter.dev tiene un aspecto diferente porque la ejecución se realizó con JavaScript habilitado.

10. Trabaja con canales de JavaScript

Los JavascriptChannels permiten que tu app registre controladores de devolución de llamada en el contexto de JavaScript de la WebView, que se pueden invocar para transmitir los valores de vuelta al código en Dart de la app. En este paso, deberás registrar un canal SnackBar al que se llamará con el resultado de una XMLHttpRequest.

Actualiza la clase WebViewStack de la siguiente manera:

lib/src/web_view_stack.dart

class WebViewStack extends StatefulWidget {
  const WebViewStack({required this.controller, Key? key}) : super(key: key);

  final Completer<WebViewController> controller;

  @override
  State<WebViewStack> createState() => _WebViewStackState();
}

class _WebViewStackState extends State<WebViewStack> {
  var loadingPercentage = 0;

  @override
  Widget build(BuildContext context) {
    return Stack(
      children: [
        WebView(
          initialUrl: 'https://flutter.dev',
          onWebViewCreated: (webViewController) {
            widget.controller.complete(webViewController);
          },
          onPageStarted: (url) {
            setState(() {
              loadingPercentage = 0;
            });
          },
          onProgress: (progress) {
            setState(() {
              loadingPercentage = progress;
            });
          },
          onPageFinished: (url) {
            setState(() {
              loadingPercentage = 100;
            });
          },
          navigationDelegate: (navigation) {
            final host = Uri.parse(navigation.url).host;
            if (host.contains('youtube.com')) {
              ScaffoldMessenger.of(context).showSnackBar(
                SnackBar(
                  content: Text(
                    'Blocking navigation to $host',
                  ),
                ),
              );
              return NavigationDecision.prevent;
            }
            return NavigationDecision.navigate;
          },
          javascriptMode: JavascriptMode.unrestricted,
          javascriptChannels: _createJavascriptChannels(context),  // Add this line
        ),
        if (loadingPercentage < 100)
          LinearProgressIndicator(
            value: loadingPercentage / 100.0,
          ),
      ],
    );
  }

  // Add from here ...
  Set<JavascriptChannel> _createJavascriptChannels(BuildContext context) {
    return {
      JavascriptChannel(
        name: 'SnackBar',
        onMessageReceived: (message) {
          ScaffoldMessenger.of(context)
              .showSnackBar(SnackBar(content: Text(message.message)));
        },
      ),
    };
  }
  // ... to here.
}

Para cada JavascriptChannel del Set, hay un objeto de canal disponible en el contexto de JavaScript como una propiedad de ventana con el mismo nombre del objeto JavascriptChannel.name. Para usarla desde el contexto de JavaScript, se debe llamar a postMessage en el JavaScriptChannel a fin de enviar un mensaje que se transmita al controlador de devolución de llamada onMessageReceived del JavascriptChannel mencionado.

Para usar el JavascriptChannel que se agregó anteriormente, incluye otro elemento de menú que ejecute una XMLHttpRequest en el contexto de JavaScript y devuelva los resultados mediante el JavascriptChannel de la SnackBar.

Ahora que la WebView conoce nuestros JavascriptChannels, deberás agregar un ejemplo para expandir aún más la app. Para ello, agrega un PopupMenuItem adicional a la clase Menu e incorpora la funcionalidad extra.

Actualiza las _MenuOptions con la opción de menú adicional agregando el valor de enumeración javascriptChannel y, luego, una implementación a la clase Menu de la siguiente manera:

lib/src/menu.dart

enum _MenuOptions {
  navigationDelegate,
  userAgent,
  javascriptChannel,                                    // Add this line
}

class Menu extends StatelessWidget {
  const Menu({required this.controller, Key? key}) : super(key: key);

  final Completer<WebViewController> controller;

  @override
  Widget build(BuildContext context) {
    return FutureBuilder<WebViewController>(
      future: controller.future,
      builder: (context, controller) {
        return PopupMenuButton<_MenuOptions>(
          onSelected: (value) async {
            switch (value) {
              case _MenuOptions.navigationDelegate:
                controller.data!.loadUrl('https://youtube.com');
                break;
              case _MenuOptions.userAgent:
                final userAgent = await controller.data!
                    .runJavascriptReturningResult('navigator.userAgent');
                ScaffoldMessenger.of(context).showSnackBar(SnackBar(
                  content: Text(userAgent),
                ));
                break;
              // Add from here ...
              case _MenuOptions.javascriptChannel:
                await controller.data!.runJavascript('''
var req = new XMLHttpRequest();
req.open('GET', "https://api.ipify.org/?format=json");
req.onload = function() {
  if (req.status == 200) {
    let response = JSON.parse(req.responseText);
    SnackBar.postMessage("IP Address: " + response.ip);
  } else {
    SnackBar.postMessage("Error: " + req.status);
  }
}
req.send();''');
                break;
              // ... to here.
            }
          },
          itemBuilder: (context) => [
            const PopupMenuItem<_MenuOptions>(
              value: _MenuOptions.navigationDelegate,
              child: Text('Navigate to YouTube'),
            ),
            const PopupMenuItem<_MenuOptions>(
              value: _MenuOptions.userAgent,
              child: Text('Show user-agent'),
            ),
            // Add from here ...
            const PopupMenuItem<_MenuOptions>(
              value: _MenuOptions.javascriptChannel,
              child: Text('Lookup IP Address'),
            ),
            // ... to here.
          ],
        );
      },
    );
  }
}

Este código en JavaScript se ejecuta cuando el usuario elige la opción JavaScript Channel Example en el menú.

var req = new XMLHttpRequest();
req.open('GET', "https://api.ipify.org/?format=json");
req.onload = function() {
  if (req.status == 200) {
    SnackBar.postMessage(req.responseText);
  } else {
    SnackBar.postMessage("Error: " + req.status);
  }
}
req.send();

Este código envía una solicitud GET a una API de dirección IP pública y muestra la dirección IP del dispositivo. Este resultado se muestra en una SnackBar invocando postMessage en el JavascriptChannel de la SnackBar.

11. Administra cookies

Tu app puede administrar cookies en la WebView mediante la clase CookieManager. En este paso, mostrarás y borrarás una lista de cookies, borrarás cookies, y establecerás otras nuevas. Agrega entradas a las _MenuOptions de cada uno de los casos de uso de cookies de la siguiente manera:

lib/src/menu.dart

enum _MenuOptions {
  navigationDelegate,
  userAgent,
  javascriptChannel,
  // Add from here ...
  listCookies,
  clearCookies,
  addCookie,
  setCookie,
  removeCookie,
  // ... to here.
}

El resto de los cambios de este paso se centran en la clase Menu, incluida la conversión de la clase Menu de sin estado a con estado. Este cambio es importante porque el Menu debe ser propietario del CookieManager y el estado mutable de los widgets sin estado es una mala combinación.

Usando un editor o una combinación de teclas, convierte la clase Menu en un StatefulWidget y agrega el CookieManager a la clase State resultante de la siguiente manera:

lib/src/menu.dart

class Menu extends StatefulWidget {                           // Convert to StatefulWidget
  const Menu({required this.controller, Key? key}) : super(key: key);

  final Completer<WebViewController> controller;

  @override
  State<Menu> createState() => _MenuState();                  // Add this line
}

class _MenuState extends State<Menu> {                       // New State class
  final CookieManager cookieManager = CookieManager();       // Add this line

  @override
  Widget build(BuildContext context) {
  // ...

La clase _MenuState contendrá el código que se agregó antes a la clase Menu, junto con el CookieManager agregado recientemente. En la siguiente serie de secciones, agregarás funciones auxiliares a _MenuState que, a su vez, las invocarán los elementos del menú que aún no han incluido.

Obtén una lista de todas las cookies

Deberás usar JavaScript para obtener una lista de todas las cookies. Para ello, agrega un método auxiliar llamado _onListCookies al final de la clase _MenuState. Con el método runJavaScriptReturningResult, el método auxiliar ejecuta document.cookie en el contexto de JavaScript y muestra una lista de todas las cookies.

Agrega la siguiente información a la clase _MenuState:

lib/src/menu.dart

Future<void> _onListCookies(WebViewController controller) async {
  final String cookies =
      await controller.runJavascriptReturningResult('document.cookie');
  ScaffoldMessenger.of(context).showSnackBar(
    SnackBar(
      content: Text(cookies.isNotEmpty ? cookies : 'There are no cookies.'),
    ),
  );
}

Borra todas las cookies

Para borrar todas las cookies de WebView, usa el método clearCookies de la clase CookieManager. El método muestra un objeto Future<bool> que se resuelve en true si el CookieManager borró las cookies y en false si no hay cookies para borrar.

Agrega la siguiente información a la clase _MenuState:

lib/src/menu.dart

Future<void> _onClearCookies() async {
  final hadCookies = await cookieManager.clearCookies();
  String message = 'There were cookies. Now, they are gone!';
  if (!hadCookies) {
    message = 'There were no cookies to clear.';
  }
  ScaffoldMessenger.of(context).showSnackBar(
    SnackBar(
      content: Text(message),
    ),
  );
}

Para agregar una cookie, puedes invocar JavaScript. La API que se usa para agregar una cookie a un documento de JavaScript se documenta en detalle en MDN.

Agrega la siguiente información a la clase _MenuState:

lib/src/menu.dart

Future<void> _onAddCookie(WebViewController controller) async {
  await controller.runJavascript('''var date = new Date();
  date.setTime(date.getTime()+(30*24*60*60*1000));
  document.cookie = "FirstName=John; expires=" + date.toGMTString();''');
  ScaffoldMessenger.of(context).showSnackBar(
    const SnackBar(
      content: Text('Custom cookie added.'),
    ),
  );
}

Las cookies también se pueden configurar con CookieManager, como se indica a continuación.

Agrega la siguiente información a la clase _MenuState:

lib/src/menu.dart

Future<void> _onSetCookie(WebViewController controller) async {
  await cookieManager.setCookie(
    const WebViewCookie(name: 'foo', value: 'bar', domain: 'flutter.dev'),
  );
  ScaffoldMessenger.of(context).showSnackBar(
    const SnackBar(
      content: Text('Custom cookie is set.'),
    ),
  );
}

Para quitar una cookie debes agregar una nueva, con una fecha de vencimiento establecida en el pasado.

Agrega la siguiente información a la clase _MenuState:

lib/src/menu.dart

Future<void> _onRemoveCookie(WebViewController controller) async {
  await controller.runJavascript(
      'document.cookie="FirstName=John; expires=Thu, 01 Jan 1970 00:00:00 UTC" ');
  ScaffoldMessenger.of(context).showSnackBar(
    const SnackBar(
      content: Text('Custom cookie removed.'),
    ),
  );
}

Agrega elementos de menú de CookieManager

Solo falta agregar las opciones del menú y conectarlas a los métodos auxiliares que acabas de agregar. Actualiza la clase _MenuState de la siguiente manera:

lib/src/menu.dart

class _MenuState extends State<Menu> {
  final CookieManager cookieManager = CookieManager();

  @override
  Widget build(BuildContext context) {
    return FutureBuilder<WebViewController>(
      future: widget.controller.future,
      builder: (context, controller) {
        return PopupMenuButton<_MenuOptions>(
          onSelected: (value) async {
            switch (value) {
              case _MenuOptions.navigationDelegate:
                controller.data!.loadUrl('https://youtube.com');
                break;
              case _MenuOptions.userAgent:
                final userAgent = await controller.data!
                    .runJavascriptReturningResult('navigator.userAgent');
                ScaffoldMessenger.of(context).showSnackBar(SnackBar(
                  content: Text(userAgent),
                ));
                break;
              case _MenuOptions.javascriptChannel:
                await controller.data!.runJavascript('''
var req = new XMLHttpRequest();
req.open('GET', "https://api.ipify.org/?format=json");
req.onload = function() {
  if (req.status == 200) {
    let response = JSON.parse(req.responseText);
    SnackBar.postMessage("IP Address: " + response.ip);
  } else {
    SnackBar.postMessage("Error: " + req.status);
  }
}
req.send();''');
                break;
              // Add from here ...
              case _MenuOptions.clearCookies:
                _onClearCookies();
                break;
              case _MenuOptions.listCookies:
                _onListCookies(controller.data!);
                break;
              case _MenuOptions.addCookie:
                _onAddCookie(controller.data!);
                break;
              case _MenuOptions.setCookie:
                _onSetCookie(controller.data!);
                break;
              case _MenuOptions.removeCookie:
                _onRemoveCookie(controller.data!);
                break;
              // ... to here.
            }
          },
          itemBuilder: (context) => [
            const PopupMenuItem<_MenuOptions>(
              value: _MenuOptions.navigationDelegate,
              child: Text('Navigate to YouTube'),
            ),
            const PopupMenuItem<_MenuOptions>(
              value: _MenuOptions.userAgent,
              child: Text('Show user-agent'),
            ),
            const PopupMenuItem<_MenuOptions>(
              value: _MenuOptions.javascriptChannel,
              child: Text('Lookup IP Address'),
            ),
            // Add from here ...
            const PopupMenuItem<_MenuOptions>(
              value: _MenuOptions.clearCookies,
              child: Text('Clear cookies'),
            ),
            const PopupMenuItem<_MenuOptions>(
              value: _MenuOptions.listCookies,
              child: Text('List cookies'),
            ),
            const PopupMenuItem<_MenuOptions>(
              value: _MenuOptions.addCookie,
              child: Text('Add cookie'),
            ),
            const PopupMenuItem<_MenuOptions>(
              value: _MenuOptions.setCookie,
              child: Text('Set cookie'),
            ),
            const PopupMenuItem<_MenuOptions>(
              value: _MenuOptions.removeCookie,
              child: Text('Remove cookie'),
            ),
            // ... to here.
          ],
        );
      },
    );
  }

Ejecuta CookieManager

Para usar todas las funciones que acabas de agregar a la app, sigue estos pasos:

  1. Selecciona List cookies. Se deberían mostrar las cookies de Google Analytics configuradas por flutter.dev.
  2. Selecciona Clear cookies. Se debería mostrar que las cookies realmente se borraron.
  3. Vuelve a seleccionar Clear cookies. Se debería mostrar que no hay cookies disponibles para borrar.
  4. Selecciona List cookies. Se debería mostrar que no hay cookies.
  5. Selecciona Add cookie. Se debería mostrar que se agregó la cookie.
  6. Selecciona Set cookie. Se debería mostrar la cookie como configurada.
  7. Selecciona List cookies y, finalmente, selecciona Remove cookie.

12. Carga elementos, archivos y strings HTML de Flutter en WebView

Tu app puede cargar archivos HTML con diferentes métodos y mostrarlos en WebView. En este paso, cargarás un elemento de Flutter especificado en el archivo pubspec.yaml, un archivo ubicado en la ruta de acceso especificada y una página con una string HTML.

Si quieres cargar un archivo ubicado en una ruta específica, deberás agregar path_provider al archivo pubspec.yaml. Este es un complemento de Flutter para encontrar ubicaciones de uso general en el sistema de archivos.

Agrega esta línea a pubspec.yaml:

pubspec.yaml

dependencies:
 flutter:
   sdk: flutter

 # The following adds the Cupertino Icons font to your application.
 # Use with the CupertinoIcons class for iOS style icons.
 cupertino_icons: ^1.0.2
 webview_flutter: ^3.0.0
 path_provider: ^2.0.7   # Add this line

Para cargar el elemento, debemos especificar la ruta de acceso a él en el archivo pubspec.yaml. Agrega las siguientes líneas a pubspec.yaml:

pubspec.yaml

# The following section is specific to Flutter.
flutter:

 # The following line ensures that the Material Icons font is
 # included with your application, so that you can use the icons in
 # the material Icons class.
 uses-material-design: true
 # Add from here
 assets:
   - assets/www/index.html
   - assets/www/styles/style.css
 # ... to here.

Para agregar elementos a tu proyecto, sigue estos pasos:

  1. Crea un directorio nuevo con el nombre assets en la carpeta raíz de tu proyecto.
  2. Crea un directorio nuevo con el nombre www en la carpeta assets.
  3. Crea un directorio nuevo con el nombre styles en la carpeta www.
  4. Crea un archivo nuevo con el nombre index.html en la carpeta www.
  5. Crea un archivo nuevo con el nombre style.css en la carpeta styles.

Copia y pega el siguiente código en el archivo index.html:

assets/www/index.html

<!DOCTYPE html>
<!-- Copyright 2013 The Flutter Authors. All rights reserved.
Use of this source code is governed by a BSD-style license that can be
found in the LICENSE file. -->
<html lang="en">
<head>
<title>Load file or HTML string example</title>
<link rel="stylesheet" href="styles/style.css" />
</head>
<body>

<h1>Local demo page</h1>
<p>
 This is an example page used to demonstrate how to load a local file or HTML
 string using the <a href="https://pub.dev/packages/webview_flutter">Flutter
 webview</a> plugin.
</p>

</body>
</html>

En style.css, usa las siguientes líneas para establecer el estilo del encabezado HTML:

assets/www/styles/style.css

h1 {
   color: blue;
}

Ahora que se configuraron los elementos y ya se pueden usar, implementa los métodos necesarios para cargar y mostrar elementos, archivos o strings HTML de Flutter.

Carga un elemento de Flutter

Para cargar el elemento que acabas de crear, solo debes llamar al método loadFlutterAsset con el WebViewController y asignar como parámetro la ruta al elemento. Agrega el siguiente método al final del código:

lib/src/menu.dart

Future<void> _onLoadFlutterAssetExample(
   WebViewController controller, BuildContext context) async {
 await controller.loadFlutterAsset('assets/www/index.html');
}

Carga archivos locales

Para cargar un archivo almacenado en tu dispositivo, puedes agregar un método que use el método loadFile, nuevamente con el WebViewController, que recibe una String con la ruta de acceso al archivo.

Primero, debes crear un archivo que contenga el código HTML. Para hacerlo, agrega el código HTML como una string en la parte superior de tu código, en el archivo menu.dart, justo debajo de las importaciones.

lib/src/menu.dart

import 'dart:async';
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:path_provider/path_provider.dart';
import 'package:webview_flutter/webview_flutter.dart';

// Add from here ...
const String kExamplePage = '''
<!DOCTYPE html>
<html lang="en">
<head>
<title>Load file or HTML string example</title>
</head>
<body>

<h1>Local demo page</h1>
<p>
 This is an example page used to demonstrate how to load a local file or HTML
 string using the <a href="https://pub.dev/packages/webview_flutter">Flutter
 webview</a> plugin.
</p>

</body>
</html>
''';
// ... to here.

Para crear un File y escribir la string HTML en el archivo, deberás agregar dos métodos. Para cargar el archivo, _onLoadLocalFileExample proporcionará la ruta de acceso como una string que muestra el método _prepareLocalFile(). Agrega los siguientes métodos a tu código:

lib/src/menu.dart

Future<void> _onLoadFlutterAssetExample(
   WebViewController controller, BuildContext context) async {
 await controller.loadFlutterAsset('assets/www/index.html');
}

Future<void> _onLoadLocalFileExample(
   WebViewController controller, BuildContext context) async {
 final String pathToIndex = await _prepareLocalFile();

 await controller.loadFile(pathToIndex);
}

static Future<String> _prepareLocalFile() async {
 final String tmpDir = (await getTemporaryDirectory()).path;
 final File indexFile = File('$tmpDir/www/index.html');

 await Directory('$tmpDir/www').create(recursive: true);
 await indexFile.writeAsString(kExamplePage);

 return indexFile.path;
}
// ... to here.

Carga strings HTML

Es muy sencillo mostrar una página en la que aparezca una string HTML. El WebViewController tiene un método llamado loadHtmlString que admite la string HTML como argumento. Luego, la WebView mostrará la página HTML proporcionada. Agrega el siguiente método a tu código:

lib/src/menu.dart

Future<void> _onLoadFlutterAssetExample(
   WebViewController controller, BuildContext context) async {
 await controller.loadFlutterAsset('assets/www/index.html');
}

Future<void> _onLoadLocalFileExample(
   WebViewController controller, BuildContext context) async {
 final String pathToIndex = await _prepareLocalFile();

 await controller.loadFile(pathToIndex);
}

static Future<String> _prepareLocalFile() async {
 final String tmpDir = (await getTemporaryDirectory()).path;
 final File indexFile = File('$tmpDir/www/index.html');

 await Directory('$tmpDir/www').create(recursive: true);
 await indexFile.writeAsString(kExamplePage);

 return indexFile.path;
}

// Add here ...
Future<void> _onLoadHtmlStringExample(
   WebViewController controller, BuildContext context) async {
 await controller.loadHtmlString(kExamplePage);
}
// ... to here.

Agrega los elementos de menú

Ahora que se configuraron los elementos y ya se pueden usar, y los métodos con todas las funcionalidades están listos, se puede actualizar el menú. Agrega las siguientes entradas a la enum _MenuOptions:

lib/src/menu.dart

enum _MenuOptions {
  navigationDelegate,
  userAgent,
  javascriptChannel,
  listCookies,
  clearCookies,
  addCookie,
  setCookie,
  removeCookie,
  // Add from here ...
  loadFlutterAsset,
  loadLocalFile,
  loadHtmlString,
  // ... to here.
}

Ahora que se actualizó la enum, puedes agregar las opciones del menú y conectarlas a los métodos auxiliares que acabas de agregar. Actualiza la clase _MenuState de la siguiente manera:

lib/src/menu.dart

class _MenuState extends State<Menu> {
 final CookieManager cookieManager = CookieManager();

 @override
 Widget build(BuildContext context) {
   return FutureBuilder<WebViewController>(
     future: widget.controller.future,
     builder: (context, controller) {
       return PopupMenuButton<_MenuOptions>(
         onSelected: (value) async {
           switch (value) {
             case _MenuOptions.navigationDelegate:
               controller.data!.loadUrl('https://youtube.com');
               break;
             case _MenuOptions.userAgent:
               final userAgent = await controller.data!
                   .runJavascriptReturningResult('navigator.userAgent');
               ScaffoldMessenger.of(context).showSnackBar(SnackBar(
                 content: Text(userAgent),
               ));
               break;
             case _MenuOptions.javascriptChannel:
               await controller.data!.runJavascript('''
var req = new XMLHttpRequest();
req.open('GET', "https://api.ipify.org/?format=json");
req.onload = function() {
 if (req.status == 200) {
   let response = JSON.parse(req.responseText);
   SnackBar.postMessage("IP Address: " + response.ip);
 } else {
   SnackBar.postMessage("Error: " + req.status);
 }
}
req.send();''');
               break;
             case _MenuOptions.clearCookies:
               _onClearCookies();
               break;
             case _MenuOptions.listCookies:
               _onListCookies(controller.data!);
               break;
             case _MenuOptions.addCookie:
               _onAddCookie(controller.data!);
               break;
             case _MenuOptions.setCookie:
               _onSetCookie(controller.data!);
               break;
             case _MenuOptions.removeCookie:
               _onRemoveCookie(controller.data!);
               Break;
             // Add from here ...
             case _MenuOptions.loadFlutterAsset:
               _onLoadFlutterAssetExample(controller.data!, context);
               break;
             case _MenuOptions.loadLocalFile:
               _onLoadLocalFileExample(controller.data!, context);
               break;
             case _MenuOptions.loadHtmlString:
               _onLoadHtmlStringExample(controller.data!, context);
               Break;
             // ... to here.
           }
         },
         itemBuilder: (context) => [
           const PopupMenuItem<_MenuOptions>(
             value: _MenuOptions.navigationDelegate,
             child: Text('Navigate to YouTube'),
           ),
           const PopupMenuItem<_MenuOptions>(
             value: _MenuOptions.userAgent,
             child: Text('Show user-agent'),
           ),
           const PopupMenuItem<_MenuOptions>(
             value: _MenuOptions.javascriptChannel,
             child: Text('Lookup IP Address'),
           ),
           const PopupMenuItem<_MenuOptions>(
             value: _MenuOptions.clearCookies,
             child: Text('Clear cookies'),
           ),
           const PopupMenuItem<_MenuOptions>(
             value: _MenuOptions.listCookies,
             child: Text('List cookies'),
           ),
           const PopupMenuItem<_MenuOptions>(
             value: _MenuOptions.addCookie,
             child: Text('Add cookie'),
           ),
           const PopupMenuItem<_MenuOptions>(
             value: _MenuOptions.setCookie,
             child: Text('Set cookie'),
           ),
           const PopupMenuItem<_MenuOptions>(
             value: _MenuOptions.removeCookie,
             child: Text('Remove cookie'),
           ),
           // Add from here ...
           const PopupMenuItem<_MenuOptions>(
             value: _MenuOptions.loadFlutterAsset,
             child: Text('Load Flutter Asset'),
           ),
           const PopupMenuItem<_MenuOptions>(
             value: _MenuOptions.loadHtmlString,
             child: Text('Load HTML string'),
           ),
           const PopupMenuItem<_MenuOptions>(
             value: _MenuOptions.loadLocalFile,
             child: Text('Load local file'),
           ),
           // ... to here.
         ],
       );
     },
   );
 }

Prueba los elementos, el archivo y la string HTML

Para probar el código que acabas de implementar, ejecútalo en tu dispositivo y haz clic en uno de los elementos de menú agregados recientemente. Observa cómo _onLoadFlutterAssetExample usa el archivo style.css que agregamos para cambiar el encabezado del archivo HTML al color azul.

13. Todo listo

¡Felicitaciones! Completaste el codelab. Puedes encontrar el código completo de este codelab en el repositorio de codelabs.

Para obtener más información, prueba los otros codelabs de Flutter.