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
medianteWebViewController
. - 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
- Android Studio 4.1 o una versión posterior (para el desarrollo de Android)
- Xcode 12 o versiones posteriores (para el desarrollo de iOS)
- SDK de Flutter
- Un editor de código, como Android Studio, Visual Studio Code o Emacs
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 JavascriptChannel
s 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),
),
);
}
Agrega una cookie
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.'),
),
);
}
Configura una cookie con CookieManager
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.'),
),
);
}
Quita una cookie
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:
- Selecciona List cookies. Se deberían mostrar las cookies de Google Analytics configuradas por flutter.dev.
- Selecciona Clear cookies. Se debería mostrar que las cookies realmente se borraron.
- Vuelve a seleccionar Clear cookies. Se debería mostrar que no hay cookies disponibles para borrar.
- Selecciona List cookies. Se debería mostrar que no hay cookies.
- Selecciona Add cookie. Se debería mostrar que se agregó la cookie.
- Selecciona Set cookie. Se debería mostrar la cookie como configurada.
- 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:
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:
- Crea un directorio nuevo con el nombre
assets
en la carpeta raíz de tu proyecto. - Crea un directorio nuevo con el nombre
www
en la carpetaassets
. - Crea un directorio nuevo con el nombre
styles
en la carpetawww
. - Crea un archivo nuevo con el nombre
index.html
en la carpetawww
. - Crea un archivo nuevo con el nombre
style.css
en la carpetastyles
.
Copia y pega el siguiente código en el archivo 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:
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.
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:
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:
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.