Como adicionar o WebView ao app do Flutter

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 pelo WebViewController;
  • 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 o ambiente de desenvolvimento do Flutter

Você precisa de dois softwares para concluir este laboratório: o SDK do Flutter e um editor.

É possível executar o codelab usando qualquer um destes dispositivos:

  • Um dispositivo físico Android ou iOS conectado ao seu computador e configurado para o modo de desenvolvedor.
  • O simulador para iOS, que exige a instalação de ferramentas do Xcode.
  • O Android Emulator, que requer configuração no Android Studio.

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 --platforms=android,ios webview_in_flutter
Creating project webview_in_flutter...
Running "flutter pub get" in webview_in_flutter...               1,728ms
Wrote 73 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

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 webview_flutter plugin no Android você precisa definir minSDK como 20. Modifique o arquivo android/app/build.gradle da seguinte maneira:

android/app/build.gradle

android {
    //...

    defaultConfig {
        applicationId "com.example.webview_in_flutter"
        minSdkVersion 20                           // MODIFY
        targetSdkVersion flutter.targetSdkVersion
        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(
      theme: ThemeData(useMaterial3: true),
      home: WebViewApp(),
    ),
  );
}

class WebViewApp extends StatefulWidget {
  const WebViewApp({super.key});

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

class _WebViewAppState extends State<WebViewApp> {
  late final WebViewController controller;

  @override
  void initState() {
    super.initState();
    controller = WebViewController()
      ..loadRequest(
        Uri.parse('https://flutter.dev'),
      );
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Flutter WebView'),
      ),
      body: WebViewWidget(
        controller: controller,
      ),
    );
  }
}

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, mas é necessário para renderizar o flutter.dev.

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({super.key});

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

class _WebViewStackState extends State<WebViewStack> {
  var loadingPercentage = 0;
  late final WebViewController controller;

  @override
  void initState() {
    super.initState();
    controller = WebViewController()
      ..setNavigationDelegate(NavigationDelegate(
        onPageStarted: (url) {
          setState(() {
            loadingPercentage = 0;
          });
        },
        onProgress: (progress) {
          setState(() {
            loadingPercentage = progress;
          });
        },
        onPageFinished: (url) {
          setState(() {
            loadingPercentage = 100;
          });
        },
      ))
      ..loadRequest(
        Uri.parse('https://flutter.dev'),
      );
  }

  @override
  Widget build(BuildContext context) {
    return Stack(
      children: [
        WebViewWidget(
          controller: controller,
        ),
        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:

lib/main.dart

import 'package:flutter/material.dart';

import 'src/web_view_stack.dart';

void main() {
  runApp(
    const MaterialApp(
      theme: ThemeData(useMaterial3: true),
      home: WebViewApp(),
    ),
  );
}

class WebViewApp extends StatefulWidget {
  const WebViewApp({super.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(),
    );
  }
}

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 'package:flutter/material.dart';
import 'package:webview_flutter/webview_flutter.dart';

class WebViewStack extends StatefulWidget {
  const WebViewStack({required this.controller, super.key}); // MODIFY

  final WebViewController controller;                        // ADD

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

class _WebViewStackState extends State<WebViewStack> {
  var loadingPercentage = 0;
  // REMOVE the controller that was here

  @override
  void initState() {
    super.initState();
    // Modify from here...
    widget.controller.setNavigationDelegate(
      NavigationDelegate(
        onPageStarted: (url) {
          setState(() {
            loadingPercentage = 0;
          });
        },
        onProgress: (progress) {
          setState(() {
            loadingPercentage = progress;
          });
        },
        onPageFinished: (url) {
          setState(() {
            loadingPercentage = 100;
          });
        },
      ),
    );
    // ...to here.
  }

  @override
  Widget build(BuildContext context) {
    return Stack(
      children: [
        WebViewWidget(
          controller: widget.controller,                     // MODIFY
        ),
        if (loadingPercentage < 100)
          LinearProgressIndicator(
            value: loadingPercentage / 100.0,
          ),
      ],
    );
  }
}

Agora o WebViewStack usa um controle criado no widget próximo. Assim, o controle do WebViewWidget pode ser compartilhado facilmente com outras partes 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 'package:flutter/material.dart';
import 'package:webview_flutter/webview_flutter.dart';

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

  final WebViewController controller;

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

Esse widget usa o WebViewController compartilhado no momento da criação para permitir que o usuário controle WebView em uma série de IconButtons.

Como adicionar controles de navegação à barra de apps

Depois de atualizar WebViewStack e criar NavigationControls, vamos colocar tudo isso em um WebViewApp atualizado. É aqui que vamos criar o WebViewController compartilhado. Como WebViewApp está na parte de cima da árvore de widgets desse app, esse é o nível ideal para fazermos isso.

Atualize o arquivo lib/main.dart desta forma:

lib/main.dart

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

import 'src/navigation_controls.dart';                  // ADD
import 'src/web_view_stack.dart';

void main() {
  runApp(
    const MaterialApp(
      theme: ThemeData(useMaterial3: true),
      home: WebViewApp(),
    ),
  );
}

class WebViewApp extends StatefulWidget {
  const WebViewApp({super.key});

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

class _WebViewAppState extends State<WebViewApp> {
  // Add from here...
  late final WebViewController controller;

  @override
  void initState() {
    super.initState();
    controller = WebViewController()
      ..loadRequest(
        Uri.parse('https://flutter.dev'),
      );
  }
  // ...to here.

  @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),       // MODIFY
    );
  }
}

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 'package:flutter/material.dart';
import 'package:webview_flutter/webview_flutter.dart';

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

  final WebViewController controller;

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

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

  @override
  void initState() {
    super.initState();
    widget.controller.setNavigationDelegate(
      NavigationDelegate(
        onPageStarted: (url) {
          setState(() {
            loadingPercentage = 0;
          });
        },
        onProgress: (progress) {
          setState(() {
            loadingPercentage = progress;
          });
        },
        onPageFinished: (url) {
          setState(() {
            loadingPercentage = 100;
          });
        },
        // Add from here...
        onNavigationRequest: (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.
      ),
    );
  }

  @override
  Widget build(BuildContext context) {
    return Stack(
      children: [
        WebViewWidget(
          controller: widget.controller,
        ),
        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 'package:flutter/material.dart';
import 'package:webview_flutter/webview_flutter.dart';

enum _MenuOptions {
  navigationDelegate,
}

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

  final WebViewController controller;

  @override
  Widget build(BuildContext context) {
    return PopupMenuButton<_MenuOptions>(
      onSelected: (value) async {
        switch (value) {
          case _MenuOptions.navigationDelegate:
            await controller.loadRequest(Uri.parse('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 loadRequest 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 'package:flutter/material.dart';
import 'package:webview_flutter/webview_flutter.dart';

import 'src/menu.dart';                               // ADD
import 'src/navigation_controls.dart';
import 'src/web_view_stack.dart';

void main() {
  runApp(
    const MaterialApp(
      theme: ThemeData(useMaterial3: true),
      home: WebViewApp(),
    ),
  );
}

class WebViewApp extends StatefulWidget {
  const WebViewApp({super.key});

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

class _WebViewAppState extends State<WebViewApp> {
  late final WebViewController controller;

  @override
  void initState() {
    super.initState();
    controller = WebViewController()
      ..loadRequest(
        Uri.parse('https://flutter.dev'),
      );
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Flutter WebView'),
        actions: [
          NavigationControls(controller: controller),
          Menu(controller: controller),               // ADD
        ],
      ),
      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 retornar um valor, use runJavaScriptReturningResult. Caso contrário, use runJavaScript.

Para ativar o JavaScript, defina a propriedade javaScriptMode como JavascriptMode.unrestricted no widget do WebViewController. 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
  void initState() {
    super.initState();
    widget.controller
      ..setNavigationDelegate(
        NavigationDelegate(
          onPageStarted: (url) {
            setState(() {
              loadingPercentage = 0;
            });
          },
          onProgress: (progress) {
            setState(() {
              loadingPercentage = progress;
            });
          },
          onPageFinished: (url) {
            setState(() {
              loadingPercentage = 100;
            });
          },
          onNavigationRequest: (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;
          },
        ),
      )
      ..setJavaScriptMode(JavaScriptMode.unrestricted);
  }

  @override
  Widget build(BuildContext context) {
    return Stack(
      children: [
        WebViewWidget(
          controller: widget.controller,
        ),
        if (loadingPercentage < 100)
          LinearProgressIndicator(
            value: loadingPercentage / 100.0,
          ),
      ],
    );
  }
}

Agora que o WebViewWidget pode executar o JavaScript, é possível incluir uma opção no menu para usar o método runJavaScriptReturningResult.

Use o editor ou o teclado para converter a classe "Menu" em um "StatefulWidget". Modifique lib/src/menu.dart da seguinte forma:

lib/src/menu.dart

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

enum _MenuOptions {
  navigationDelegate,
  userAgent,
}

class Menu extends StatefulWidget {
  const Menu({required this.controller, super.key});

  final WebViewController controller;

  @override
  State<Menu> createState() => _MenuState();
}

class _MenuState extends State<Menu> {
  @override
  Widget build(BuildContext context) {
    return PopupMenuButton<_MenuOptions>(
      onSelected: (value) async {
        switch (value) {
          case _MenuOptions.navigationDelegate:
            await widget.controller
                .loadRequest(Uri.parse('https://youtube.com'));
            break;
          case _MenuOptions.userAgent:
            final userAgent = await widget.controller
                .runJavaScriptReturningResult('navigator.userAgent');
            if (!mounted) return;
            ScaffoldMessenger.of(context).showSnackBar(SnackBar(
              content: Text('$userAgent'),
            ));
            break;
        }
      },
      itemBuilder: (context) => [
        const PopupMenuItem<_MenuOptions>(
          value: _MenuOptions.navigationDelegate,
          child: Text('Navigate to YouTube'),
        ),
        const PopupMenuItem<_MenuOptions>(
          value: _MenuOptions.userAgent,
          child: Text('Show user-agent'),
        ),
      ],
    );
  }
}

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

Os canais JavaScript permitem que o app registre gerenciadores de callback no contexto dessa linguagem para o WebViewWidget. Eles 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, super.key});

  final WebViewController controller;

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

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

  @override
  void initState() {
    super.initState();
    widget.controller
      ..setNavigationDelegate(
        NavigationDelegate(
          onPageStarted: (url) {
            setState(() {
              loadingPercentage = 0;
            });
          },
          onProgress: (progress) {
            setState(() {
              loadingPercentage = progress;
            });
          },
          onPageFinished: (url) {
            setState(() {
              loadingPercentage = 100;
            });
          },
          onNavigationRequest: (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;
          },
        ),
      )
      // Modify from here...
      ..setJavaScriptMode(JavaScriptMode.unrestricted)
      ..addJavaScriptChannel(
        'SnackBar',
        onMessageReceived: (message) {
          ScaffoldMessenger.of(context)
              .showSnackBar(SnackBar(content: Text(message.message)));
        },
      );
      // ...to here.
  }

  @override
  Widget build(BuildContext context) {
    return Stack(
      children: [
        WebViewWidget(
          controller: widget.controller,
        ),
        if (loadingPercentage < 100)
          LinearProgressIndicator(
            value: loadingPercentage / 100.0,
          ),
      ],
    );
  }
}

Para cada canal JavaScript no Set, um objeto é disponibilizado no contexto dessa linguagem como uma propriedade de janela com o mesmo nome de name. Para usar o objeto no contexto do JavaScript, é preciso chamar postMessage no canal para enviar uma mensagem que é transmitida ao gerenciador de callback onMessageReceived do JavascriptChannel.

Para usar o canal JavaScript adicionado acima, inclua outro item de menu que execute um XMLHttpRequest no contexto dessa linguagem e transmita os resultados de volta usando o canal JavaScript do SnackBar.

Agora que WebViewWidget já sabe sobre nossos canais JavaScript ,você vai adicionar um exemplo para expandir 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,
}

class Menu extends StatefulWidget {
  const Menu({required this.controller, super.key});

  final WebViewController controller;

  @override
  State<Menu> createState() => _MenuState();
}

class _MenuState extends State<Menu> {
  @override
  Widget build(BuildContext context) {
    return PopupMenuButton<_MenuOptions>(
      onSelected: (value) async {
        switch (value) {
          case _MenuOptions.navigationDelegate:
            await widget.controller
                .loadRequest(Uri.parse('https://youtube.com'));
            break;
          case _MenuOptions.userAgent:
            final userAgent = await widget.controller
                .runJavaScriptReturningResult('navigator.userAgent');
            if (!mounted) return;
            ScaffoldMessenger.of(context).showSnackBar(SnackBar(
              content: Text('$userAgent'),
            ));
            break;
          case _MenuOptions.javascriptChannel:
            await widget.controller.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;
        }
      },
      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'),
        ),
      ],
    );
  }
}

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, incluindo a conversão da classe Menu 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.

Adicione o CookieManager à classe de estado resultante, como mostramos a seguir:

lib/src/menu.dart

class Menu extends StatefulWidget {
  const Menu({required this.controller, super.key});

  final WebViewController controller;

  @override
  State<Menu> createState() => _MenuState();
}

class _MenuState extends State<Menu> {
  final cookieManager = WebViewCookieManager();       // 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') as String;
  if (!mounted) return;
  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.';
  }
  if (!mounted) return;
  ScaffoldMessenger.of(context).showSnackBar(
    SnackBar(
      content: Text(message),
    ),
  );
}

É 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();''');
  if (!mounted) return;
  ScaffoldMessenger.of(context).showSnackBar(
    const SnackBar(
      content: Text('Custom cookie added.'),
    ),
  );
}

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'),
  );
  if (!mounted) return;
  ScaffoldMessenger.of(context).showSnackBar(
    const SnackBar(
      content: Text('Custom cookie is set.'),
    ),
  );
}

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" ');
  if (!mounted) return;
  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 = WebViewCookieManager();

  @override
  Widget build(BuildContext context) {
    return PopupMenuButton<_MenuOptions>(
      onSelected: (value) async {
        switch (value) {
          case _MenuOptions.navigationDelegate:
            await widget.controller
                .loadRequest(Uri.parse('https://youtube.com'));
            break;
          case _MenuOptions.userAgent:
            final userAgent = await widget.controller
                .runJavaScriptReturningResult('navigator.userAgent');
            if (!mounted) return;
            ScaffoldMessenger.of(context).showSnackBar(SnackBar(
              content: Text('$userAgent'),
            ));
            break;
          case _MenuOptions.javascriptChannel:
            await widget.controller.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:
            await _onClearCookies();
            break;
          case _MenuOptions.listCookies:
            await _onListCookies(widget.controller);
            break;
          case _MenuOptions.addCookie:
            await _onAddCookie(widget.controller);
            break;
          case _MenuOptions.setCookie:
            await _onSetCookie(widget.controller);
            break;
          case _MenuOptions.removeCookie:
            await _onRemoveCookie(widget.controller);
            break;
        }
      },
      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'),
        ),
      ],
    );
  }

Como usar CookieManager

Para usar todas as funcionalidades que você acabou de adicionar ao app, siga estas etapas:

  1. Selecione Listar cookies. Os cookies do Google Analytics definidos pelo flutter.dev são listados.
  2. Selecione Limpar cookies. A mensagem informa que os cookies realmente foram apagados.
  3. Selecione Limpar cookies mais uma vez. A mensagem informa que não há cookies para limpar.
  4. Selecione Listar cookies. A mensagem informa que não há cookies.
  5. Selecione Adicionar cookie. A mensagem informa que o cookie foi adicionado.
  6. Selecione Definir cookie. A mensagem informa que o cookie foi definido.
  7. 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.

Insira os seguintes valores na linha de comando:

$ flutter pub add path_provider

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:

  1. Crie um novo diretório com o nome assets na pasta raiz do projeto.
  2. Crie um novo diretório com o nome www na pasta assets.
  3. Crie um novo diretório com o nome styles na pasta www.
  4. Crie um novo arquivo com o nome index.html na pasta www.
  5. Crie um novo arquivo com o nome style.css na pasta styles.

Copie e cole o seguinte código no arquivo index.html:

assets/www/index.html

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

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

</body>
</html>

Em style.css, use as seguintes linhas para definir o estilo do cabeçalho HTML:

assets/www/styles/style.css

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.

lib/src/menu.dart

import 'dart:io';                                   // Add this line,
import 'package:flutter/material.dart';
import 'package:path_provider/path_provider.dart';  // And this one.
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:

lib/src/menu.dart

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;
}

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:

lib/src/menu.dart

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

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

  await controller.loadFile(pathToIndex);
}

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

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

  return indexFile.path;
}

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

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 = WebViewCookieManager();

  @override
  Widget build(BuildContext context) {
    return PopupMenuButton<_MenuOptions>(
      onSelected: (value) async {
        switch (value) {
          case _MenuOptions.navigationDelegate:
            await widget.controller
                .loadRequest(Uri.parse('https://youtube.com'));
            break;
          case _MenuOptions.userAgent:
            final userAgent = await widget.controller
                .runJavaScriptReturningResult('navigator.userAgent');
            if (!mounted) return;
            ScaffoldMessenger.of(context).showSnackBar(SnackBar(
              content: Text('$userAgent'),
            ));
            break;
          case _MenuOptions.javascriptChannel:
            await widget.controller.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:
            await _onClearCookies();
            break;
          case _MenuOptions.listCookies:
            await _onListCookies(widget.controller);
            break;
          case _MenuOptions.addCookie:
            await _onAddCookie(widget.controller);
            break;
          case _MenuOptions.setCookie:
            await _onSetCookie(widget.controller);
            break;
          case _MenuOptions.removeCookie:
            await _onRemoveCookie(widget.controller);
            break;
          case _MenuOptions.loadFlutterAsset:
            await _onLoadFlutterAssetExample(widget.controller, context);
            break;
          case _MenuOptions.loadLocalFile:
            await _onLoadLocalFileExample(widget.controller, context);
            break;
          case _MenuOptions.loadHtmlString:
            await _onLoadHtmlStringExample(widget.controller, context);
            break;
        }
      },
      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'),
        ),
        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'),
        ),
      ],
    );
  }

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).