1. Introdução
Última atualização: 19/10/2021
O plug-in do Flutter do WebView permite incluir um widget do WebView ao app do Flutter no Android ou no iOS. No iOS, o widget usa o WKWebView. Já no Android, ele usa um WebView (links em inglês). O plug-in pode renderizar widgets do Flutter pela visualização da Web. Por exemplo, um menu suspenso.
O que você vai criar
Neste codelab, você vai criar um app para dispositivos móveis com um WebView usando o SDK do Flutter. Esse app vai:
- mostrar conteúdo da Web em uma
WebView
; - mostrar widgets do Flutter empilhados sobre o
WebView
; - reagir a eventos de progresso de carregamento de página;
- controlar o
WebView
peloWebViewController
; - bloquear sites usando
NavigationDelegate
; - avaliar expressões de JavaScript;
- processar callbacks do JavaScript com
JavascriptChannels
; - definir, remover, adicionar ou mostrar cookies;
- carregar e mostrar HTML em recursos, arquivos ou strings que contenham HTML.
O que você vai aprender
Neste codelab, você vai aprender a usar o plug-in webview_flutter
(em inglês) de várias formas, por exemplo:
- Como configurar o plug-in
webview_flutter
. - Como detectar eventos de progresso de carregamento da página.
- Como controlar a navegação nas páginas.
- Como comandar o
WebView
para navegar pelo histórico. - Como avaliar o JavaScript, inclusive com os resultados retornados.
- Como registrar callbacks para chamar o código Dart no JavaScript.
- Como gerenciar cookies
- Como carregar e mostrar páginas em HTML em recursos, arquivos ou uma string que contenha HTML.
Pré-requisitos
- Android Studio 4.1 ou mais recente (para desenvolvimento no Android)
- Xcode 12 ou versão mais recente (para desenvolvimento no iOS)
- SDK do Flutter (em inglês)
- Um editor de código, como o Android Studio, o Visual Studio Code ou o Emacs (links em inglês).
2. Configurar seu ambiente do Flutter
Você precisa de dois softwares para concluir este codelab: o SDK do Flutter e um editor (links em inglês).
É possível completar este codelab usando qualquer um dos seguintes dispositivos:
- Um dispositivo físico (Android ou iOS) conectado ao computador e configurado para o modo de desenvolvedor
- O iOS Simulator: somente para macOS e exige a instalação de ferramentas do Xcode (em inglês)
- O Android Emulator, que requer configuração no Android Studio (em inglês).
3. Primeiros passos
Primeiros passos com o Flutter
Há várias formas de criar um novo projeto do Flutter. O Android Studio e o Visual Studio Code oferecem ferramentas para isso (links em inglês). Siga os procedimentos indicados ou execute os comandos a seguir em um terminal de linha de comando.
$ 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.
Como adicionar o plug-in do Flutter do WebView como uma dependência
É fácil adicionar outros recursos a um app do Flutter usando os pacotes do Pub. Neste codelab, você vai adicionar o plug-in webview_flutter
ao seu projeto (links em inglês). Execute estes comandos no 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!
Se você inspecionar o arquivo pubspec.yaml, vai ver que ele tem uma linha na seção de dependências do plug-in webview_flutter
.
Configurar o minSDK do Android
Para usar o plug-in webview_flutter
no Android, defina a minSDK
como 19 ou 20, dependendo da visualização da Plataforma Android que você quer usar. Veja mais informações sobre a visualização da Plataforma Android na página do plug-in webview_flutter
(em inglês). Modifique o arquivo android/app/build.gradle
da seguinte maneira:
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. Como adicionar o widget do WebView ao app do Flutter
Nesta etapa, você vai adicionar um WebView
ao aplicativo. As WebViews são visualizações nativas hospedadas que o desenvolvedor de apps pode escolher como hospedar no app. No Android, as opções são a composição híbrida e a exibição virtual, que atualmente é o padrão do Android. O iOS sempre usa a composição híbrida.
Para ver mais informações sobre as diferenças entre as exibições virtuais e a composição híbrida, leia a documentação sobre como hospedar visualizações nativas do Android e do iOS no app do Flutter com visualizações da plataforma (em inglês).
Como colocar um WebView na tela
Substitua o conteúdo de lib/main.dart
pelo seguinte:
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',
),
);
}
}
Quando você executa essa funcionalidade no iOS ou no Android, o WebView aparece como uma janela do navegador sem margens no dispositivo, ou seja, o navegador é mostrado em tela cheia, sem bordas. Ao rolar a tela, você vai perceber que algumas partes da página podem parecer um pouco estranhas. Isso acontece porque o JavaScript está desativado, e ele é necessário para renderizar o flutter.dev.
Como ativar a composição híbrida
Se você quiser usar o modo de composição híbrida para dispositivos Android, precisa fazer algumas modificações. Mude o lib/main.dart
da seguinte forma:
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',
),
);
}
}
Mude minSdkVersion
em build.gradle
para 19 quando quiser usar a visualização da plataforma da composição híbrida.
Executar o app
Execute o app do Flutter no iOS ou no Android para ver um WebView, que mostra o site flutter.dev (em inglês). Outra opção é executar o app em um Android Emulator ou no simulador de iOS. Se quiser, substitua o URL inicial do WebView pelo seu próprio site, por exemplo.
$ flutter run
Se você conectou o dispositivo físico certo ou está executando o simulador ou emulador apropriado, a imagem a seguir aparece após a compilação e a implantação do app:
5. Como detectar eventos de carregamento de página
O widget do WebView
fornece vários eventos do processo de carregamento da página que podem ser detectados pelo app. Durante o ciclo de carregamento de página do WebView
, três eventos de carregamento de página diferentes são disparados: onPageStarted
, onProgress
e onPageFinished
. Nesta etapa, você vai implementar um indicador de carregamento de página. Você também vai ver que é possível renderizar o conteúdo do Flutter na área de conteúdo do WebView
.
Como adicionar eventos de carregamento de página ao app
Crie um novo arquivo de origem em lib/src/web_view_stack.dart
e insira o seguinte conteúdo:
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,
),
],
);
}
}
Esse código uniu o widget do WebView
em Stack
, sobrepondo condicionalmente o WebView
com LinearProgressIndicator
quando a porcentagem de carregamento da página está abaixo de 100%. Como isso envolve um estado do programa que muda com o tempo, você armazenou esse estado em uma classe State
associada a StatefulWidget
.
Para usar esse novo widget do WebViewStack
, modifique o lib/main.dart desta forma:
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
);
}
}
Quando você executa o app, dependendo das condições da sua rede e caso o navegador armazene em cache ou não a página acessada, um indicador de carregamento de página aparece sobre a área do conteúdo do WebView
.
6. Como trabalhar com o WebViewController
Como acessar o WebViewController pelo widget do WebView
O widget do WebView
permite o controle programático com WebViewController
. Esse controlador é disponibilizado após a criação do widget do WebView
com um callback. Como a disponibilidade desse controlador é assíncrona, ele é um forte candidato para a classe assíncrona Completer<T>
do Dart.
Atualize o lib/src/web_view_stack.dart
desta maneira:
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,
),
],
);
}
}
Agora o widget WebViewStack
publica o controlador que é criado de forma assíncrona usando Completer<WebViewController>
. Essa opção é mais leve do que criar um argumento de função de callback para fornecer o controlador ao restante do app.
Como criar controles de navegação
O WebView
pode até estar funcionando, mas ainda falta poder atualizar a página e avançar ou voltar o histórico. Felizmente, é possível usar um WebViewController
para incluir essa funcionalidade no app.
Crie um arquivo de origem em lib/src/navigation_controls.dart
e insira o seguinte:
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();
},
),
],
);
},
);
}
}
Esse widget usa um widget FutureBuilder<T>
para se redesenhar quando o controlador está disponível. Enquanto aguarda a disponibilização do controlador, uma linha com três ícones é renderizada. No entanto, depois que o controlador aparece, ela é substituída por Row
de IconButton
s com gerenciadores onPressed
que usam controller
para implementar esse recurso.
Como adicionar controles de navegação à barra de apps
Depois de atualizar WebViewStack
e criar NavigationControls
, vamos colocar tudo isso em um WebViewApp
atualizado. Você viu como usar Completer<T>
, mas não onde ele foi criado. Como WebViewApp
está na parte de cima da árvore de widgets desse app, esse é o nível ideal para a criação.
Atualize o arquivo lib/main.dart
desta forma:
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
);
}
}
A execução do app revela uma página da Web com controles:
7. Como rastrear a navegação com NavigationDelegate
O WebView
fornece ao app com NavigationDelegate,
, que permite acompanhar e controlar a navegação na página do widget do WebView
. Quando uma navegação é iniciada pelo WebView,
, como quando um usuário clica em um link, NavigationDelegate
é chamado. É possível usar o callback NavigationDelegate
para decidir se o WebView
continua a navegação.
Registrar NavigationDelegate personalizado
Nesta etapa, você vai registrar um callback NavigationDelegate
para bloquear a navegação no YouTube.com. Essa implementação simplista também bloqueia conteúdo inline do YouTube, que aparece em várias páginas de documentação da API do Flutter.
Atualize o lib/src/web_view_stack.dart
desta forma:
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,
),
],
);
}
}
Na próxima etapa, você vai adicionar um item de menu para ativar o teste de NavigationDelegate
usando a classe WebViewController
. Nesse exercício, o leitor aumenta a lógica do callback para bloquear apenas a navegação de página inteira no YouTube.com, mas permite o conteúdo inline do YouTube na documentação da API.
8. Como adicionar um botão de menu à barra de apps
Nas próximas etapas, você vai criar um botão de menu no widget de AppBar
usado para avaliar o JavaScript, invocar canais JavaScript e gerenciar cookies. Resumindo, um menu muito útil.
Crie um arquivo de origem em lib/src/menu.dart
e insira o seguinte:
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'),
),
],
);
},
);
}
}
Quando o usuário seleciona a opção de menu Navigate to YouTube, o método loadUrl
do WebViewController
é executado. Essa navegação é bloqueada pelo callback navigationDelegate
que você criou na etapa anterior.
Para adicionar o menu à tela do WebViewApp
, modifique lib/main.dart
da seguinte maneira:
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),
);
}
}
Execute o app e toque no item de menu Navigate to YouTube. Uma snackbar informa que o controlador de navegação bloqueou o acesso ao YouTube.
9. Como avaliar o JavaScript
WebViewController
pode avaliar expressões JavaScript no contexto da página atual. Para isso, existem duas opções: se o código JavaScript não retornar um valor, use runJavaScript
. Se retornar, use runJavaScriptReturningResult
.
Para ativar o JavaScript, defina a propriedade javaScriptMode
como JavascriptMode.unrestricted
no widget do WebView
. Por padrão, javascriptMode
é definido como JavascriptMode.disabled
.
Atualize a classe _WebViewStackState
adicionando a configuração javascriptMode
da seguinte maneira:
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,
),
],
);
}
}
Agora que o WebView
pode executar o JavaScript, podemos incluir uma opção ao menu para usar o método runJavaScriptReturningResult
.
Mude lib/src/menu.dart
da seguinte maneira:
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.
],
);
},
);
}
}
Quando você toca na opção do menu "Show user-agent", o resultado da execução da expressão JavaScript navigator.userAgent
aparece em um Snackbar
. Ao executar o app, a página do Flutter.dev pode parecer diferente. Isso acontece porque o JavaScript está ativado.
10. Como trabalhar com canais JavaScript
JavascriptChannel
s permitem que o app registre gerenciadores de callback no contexto JavaScript do WebView
que podem ser invocados para transmitir valores de volta ao código Dart do app. Nesta etapa, você vai registrar um canal de SnackBar
que será chamado com o resultado de uma XMLHttpRequest
.
Atualize a classe WebViewStack
desta forma:
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
em Set
, um objeto de canal é disponibilizado no contexto do JavaScript como uma propriedade de janela com o mesmo nome de JavascriptChannel.name
. Para usar isso no contexto do JavaScript, é preciso chamar postMessage
no JavaScriptChannel para enviar uma mensagem que é transmitida ao gerenciador de callback onMessageReceived
de JavascriptChannel
nomeado.
Para usar o JavascriptChannel
adicionado acima, inclua outro item de menu que executa XMLHttpRequest
no contexto JavaScript e transmite os resultados usando JavascriptChannel
de SnackBar
.
Agora que o WebView
tem conhecimento de JavascriptChannels,
, inclua um exemplo para expandir ainda mais o app. Para isso, adicione mais PopupMenuItem
à classe Menu
e inclua a funcionalidade extra.
Atualize _MenuOptions
com a opção de menu extra: adicione o valor de enumeração javascriptChannel
e uma implementação à classe Menu
da seguinte forma:
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.
],
);
},
);
}
}
Esse JavaScript é executado quando o usuário escolhe a opção de menu JavaScript Channel Example.
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();
Esse código envia uma solicitação GET
a uma API de endereço IP público para retornar o endereço IP do dispositivo. Esse resultado aparece em SnackBar
invocando postMessage
em JavascriptChannel
de SnackBar
.
11. Como gerenciar cookies
Seu app pode gerenciar cookies no WebView
usando a classe CookieManager
. Nesta etapa, você vai definir e excluir cookies e mostrar e limpar uma lista de cookies. Adicione entradas a _MenuOptions
para cada um dos casos de uso de cookies da seguinte maneira:
lib/src/menu.dart
enum _MenuOptions {
navigationDelegate,
userAgent,
javascriptChannel,
// Add from here ...
listCookies,
clearCookies,
addCookie,
setCookie,
removeCookie,
// ... to here.
}
As outras mudanças feitas nesta etapa são relacionadas à classe Menu
, que passa de sem estado para com estado. Essa mudança é importante porque Menu
precisa ser o proprietário de CookieManager
, e o estado mutável em widgets sem estado não é apropriado.
Com o seu editor ou o teclado, converta a classe Menu para StatefulWidget e adicione CookieManager à classe State resultante da seguinte forma:
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) {
// ...
A classe _MenuState
vai conter o código incluído na classe Menu
e o CookieManager
adicionado. Nas próximas seções, você vai adicionar funções auxiliares a _MenuState
, que será invocado pelos itens de menu a serem adicionados.
Receber uma lista de todos os cookies
Você vai usar o JavaScript para acessar uma lista com todos os cookies. Para isso, adicione o método auxiliar _onListCookies
no fim da classe _MenuState
. Seu método auxiliar usa o método runJavaScriptReturningResult
para executar document.cookie
no contexto do JavaScript, retornando uma lista de todos os cookies.
Adicione este código à classe _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.'),
),
);
}
Limpar todos os cookies
Para limpar todos os cookies no WebView, use o método clearCookies
da classe CookieManager
. O método retorna Future<bool>
, que é resolvido para true
quando CookieManager
limpa os cookies, e para false
quando não há cookies.
Adicione este código à classe _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),
),
);
}
Adicionar um cookie
É possível invocar o JavaScript para adicionar cookies. A API usada para adicionar um cookie a um documento JavaScript é documentada em detalhes no MDN.
Adicione este código à classe _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.'),
),
);
}
Como definir cookies com CookieManager
Uma opção para definir cookies é usar CookieManager (em inglês) como descrito a seguir.
Adicione este código à classe _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.'),
),
);
}
Remover cookies
Defina uma data de vencimento passada em um cookie para que ele seja excluído.
Adicione este código à classe _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.'),
),
);
}
Como adicionar os itens de menu de CookieManager
Agora você só precisa incluir e conectar as opções do menu aos métodos auxiliares que você acabou de adicionar. Atualize a classe _MenuState
desta forma:
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.
],
);
},
);
}
Como usar CookieManager
Para usar todas as funcionalidades que você acabou de adicionar ao app, siga estas etapas:
- Selecione Listar cookies. Os cookies do Google Analytics definidos pelo flutter.dev são listados.
- Selecione Limpar cookies. A mensagem informa que os cookies realmente foram apagados.
- Selecione Limpar cookies mais uma vez. A mensagem informa que não há cookies para limpar.
- Selecione Listar cookies. A mensagem informa que não há cookies.
- Selecione Adicionar cookie. A mensagem informa que o cookie foi adicionado.
- Selecione Definir cookie. A mensagem informa que o cookie foi definido.
- Selecione Listar cookies e, em seguida, Remover cookie.
12. Carregar recursos, arquivos e strings HTML do Flutter no WebView
O app pode carregar arquivos HTML usando diferentes métodos e mostrar na WebView. Nesta etapa, você vai carregar um recurso do Flutter especificado no arquivo pubspec.yaml
, um arquivo localizado no caminho especificado e uma página usando uma string HTML.
Se você quer carregar um arquivo localizado em um caminho especificado, adicione path_provider
a pubspec.yaml
. Esse plug-in do Flutter ajuda a encontrar locais usados com frequência no sistema de arquivos.
No arquivo pubspec.yaml, adicione esta linha:
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 carregar o recurso, especifique o caminho dele em pubspec.yaml
. Em pubspec.yaml
, adicione estas linhas:
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 adicionar os recursos ao seu projeto, siga estas etapas:
- Crie um novo diretório com o nome
assets
na pasta raiz do projeto. - Crie um novo diretório com o nome
www
na pastaassets
. - Crie um novo diretório com o nome
styles
na pastawww
. - Crie um novo arquivo com o nome
index.html
na pastawww
. - Crie um novo arquivo com o nome
style.css
na pastastyles
.
Copie e cole o seguinte código no arquivo 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>
Em style.css, use as seguintes linhas para definir o estilo do cabeçalho HTML:
h1 {
color: blue;
}
Agora que os recursos estão definidos e prontos para uso, é possível implementar os métodos necessários para carregar e mostrar recursos, arquivos ou strings HTML do Flutter.
Carregar o recurso do Flutter
Para carregar o recurso que você acabou de criar, chame o método loadFlutterAsset
usando WebViewController
e forneça o caminho do recurso como parâmetro. Adicione o seguinte método no final do seu código:
lib/src/menu.dart
Future<void> _onLoadFlutterAssetExample(
WebViewController controller, BuildContext context) async {
await controller.loadFlutterAsset('assets/www/index.html');
}
Carregar um arquivo local
Para carregar um arquivo no seu dispositivo, adicione um método que vai usar o método loadFile
com WebViewController
, que usa a String
com o caminho do arquivo.
Primeiro, crie um arquivo com o código HTML. Para isso, adicione o código HTML como uma string na parte de cima do código no arquivo menu.dart
, logo abaixo das importações.
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 criar File
e gravar a string HTML no arquivo, adicione dois métodos. _onLoadLocalFileExample
vai carregar o arquivo fornecendo o caminho como uma string retornada pelo método _prepareLocalFile()
. Adicione estes métodos ao 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.
Carregar uma string HTML
É bem simples mostrar uma página fornecendo uma string HTML. WebViewController
tem um método para isso chamado loadHtmlString
, em que você fornece a string HTML como um argumento. Depois disso, WebView
mostra a página HTML fornecida. Adicione este método ao seu 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.
Adicionar os itens de menu
Agora que os recursos foram definidos e estão prontos para uso, e os métodos com todas as funcionalidades foram criados, já podemos atualizar o menu. Adicione as seguintes entradas ao tipo enumerado _MenuOptions
:
lib/src/menu.dart
enum _MenuOptions {
navigationDelegate,
userAgent,
javascriptChannel,
listCookies,
clearCookies,
addCookie,
setCookie,
removeCookie,
// Add from here ...
loadFlutterAsset,
loadLocalFile,
loadHtmlString,
// ... to here.
}
Agora que a enumeração foi atualizada, inclua as opções do menu e conecte a eles os métodos auxiliares que você adicionou. Atualize a classe _MenuState
desta forma:
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.
],
);
},
);
}
Como testar os recursos, o arquivo e a string HTML
Agora verifique se tudo deu certo. No seu dispositivo, execute o código que você acabou de implementar e clique em um dos itens de menu recém-adicionados. Veja como _onLoadFlutterAssetExample
usa o style.css
que adicionamos para deixar o cabeçalho do arquivo HTML na cor azul.
13. Pronto!
Parabéns! Você concluiu o codelab. O código completo deste codelab está disponível no repositório do codelab (em inglês).
Para saber mais, veja os outros codelabs do Flutter (link em inglês).