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 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 IconButtons 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

JavascriptChannels 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),
    ),
  );
}

É 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.'),
    ),
  );
}

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.'),
    ),
  );
}

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:

  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.

No arquivo pubspec.yaml, adicione esta linha:

pubspec.yaml

dependencies:
 flutter:
   sdk: flutter

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

Para 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: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:

lib/src/menu.dart

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

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

 await controller.loadFile(pathToIndex);
}

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

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

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

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