Добавление WebView в ваше приложение Flutter

1. Введение

Last Updated: 2021-10-19

С помощью плагина WebView Flutter вы можете добавить виджет WebView в свое приложение Flutter для Android или iOS. На iOS виджет WebView основан на WKWebView , а на Android — на WebView . Плагин может отображать виджеты Flutter поверх веб-представления. Например, можно отобразить выпадающее меню поверх веб-представления.

Что вы построите

В этом практическом занятии вы шаг за шагом создадите мобильное приложение с использованием WebView и Flutter SDK. Ваше приложение будет:

  • Отображение веб-контента в WebView
  • Display Flutter widgets stacked over the WebView
  • React to page load progress events
  • Управляйте WebView через WebViewController
  • Block websites using the NavigationDelegate
  • Evaluate JavaScript expressions
  • Обработка обратных вызовов из JavaScript с помощью JavascriptChannels
  • Set, remove, add or show cookies
  • Загрузка и отображение HTML-кода из ресурсов, файлов или строк, содержащих HTML-код.

Симулятор iPhone, запускающий приложение Flutter со встроенным веб-представлением, отображающим домашнюю страницу Flutter.dev.

Эмулятор Android, запускающий приложение Flutter со встроенным веб-представлением, отображающим домашнюю страницу Flutter.dev.

Что вы узнаете

В этом практическом занятии вы узнаете, как использовать плагин webview_flutter различными способами, в том числе:

  • How to configure the webview_flutter plugin
  • Как отслеживать события, связанные с прогрессом загрузки страницы
  • How to control page navigation
  • Как заставить WebView перемещаться вперед и назад по своей истории
  • Как выполнить JavaScript-код, в том числе используя возвращаемые результаты.
  • Как зарегистрировать коллбэки для вызова кода Dart из JavaScript
  • Как управлять файлами cookie
  • Как загрузить и отобразить HTML-страницы из ресурсов, файлов или строк, содержащих HTML-код.

Что вам понадобится

  • Android Studio 4.1 или более поздняя версия (для разработки под Android)
  • Xcode 12 or later (for iOS development)
  • Flutter SDK
  • Редактор кода, например, Android Studio или Visual Studio Code .

2. Настройте среду разработки Flutter.

Для выполнения этой лабораторной работы вам понадобятся два программных компонента: Flutter SDK и редактор .

Вы можете выполнить это практическое задание, используя любое из следующих устройств:

  • Физическое устройство Android или iOS , подключенное к компьютеру и настроенное на режим разработчика.
  • Симулятор iOS (требуется установка инструментов Xcode).
  • Эмулятор Android (требуется настройка в Android Studio).

3. Начало работы

Getting started with Flutter

Существует несколько способов создания нового проекта Flutter, и для этой задачи существуют инструменты как в Android Studio, так и в Visual Studio Code. Вы можете либо следовать инструкциям по созданию проекта, либо выполнить следующие команды в удобном терминале командной строки.

$ flutter create --platforms=android,ios webview_in_flutter
Creating project webview_in_flutter...
Resolving dependencies in `webview_in_flutter`...
Downloading packages...
Got dependencies in `webview_in_flutter`.
Wrote 74 files.

All done!
You can find general documentation for Flutter at: https://docs.flutter.dev/
Detailed API documentation is available at: https://api.flutter.dev/
If you prefer video documentation, consider: https://www.youtube.com/c/flutterdev

In order to run your application, type:

  $ cd webview_in_flutter
  $ flutter run

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

Adding WebView Flutter plugin as a dependency

Наилучший способ добавления дополнительных функций в Flutter-приложение — использование пакетов Pub . В этом практическом занятии вы добавите плагин webview_flutter в свой проект. Выполните следующие команды в терминале.

$ cd webview_in_flutter
$ flutter pub add webview_flutter
Resolving dependencies...
Downloading packages...
  collection 1.18.0 (1.19.0 available)
  leak_tracker 10.0.5 (10.0.7 available)
  leak_tracker_flutter_testing 3.0.5 (3.0.7 available)
  material_color_utilities 0.11.1 (0.12.0 available)
+ plugin_platform_interface 2.1.8
  string_scanner 1.2.0 (1.3.0 available)
  test_api 0.7.2 (0.7.3 available)
+ webview_flutter 4.9.0
+ webview_flutter_android 3.16.7
+ webview_flutter_platform_interface 2.10.0
+ webview_flutter_wkwebview 3.15.0
Changed 5 dependencies!
6 packages have newer versions incompatible with dependency constraints.
Try `flutter pub outdated` for more information.

Если вы проверите свой файл pubspec.yaml , то увидите, что в разделе dependencies появилась строка, посвященная плагину webview_flutter .

Configure Android minSDK

Для использования плагина webview_flutter на Android необходимо установить minSDK равным 20 Измените файл android/app/build.gradle следующим образом:

android/app/build.gradle

android {
    //...

    defaultConfig {
        applicationId = "com.example.webview_in_flutter"
        minSdk = 20                                         // Modify this line
        targetSdk = flutter.targetSdkVersion
        versionCode = flutterVersionCode.toInteger()
        versionName = flutterVersionName
    }

4. Добавление виджета WebView в приложение Flutter

На этом шаге вы добавите WebView в свое приложение. WebView — это встроенные представления, и у вас, как у разработчика приложения, есть выбор, как разместить эти встроенные представления в своем приложении. На Android вы можете выбрать между виртуальными дисплеями (по умолчанию для Android) и гибридной композицией. Однако iOS всегда использует гибридную композицию.

Для более подробного обсуждения различий между виртуальными дисплеями и гибридной композицией ознакомьтесь с документацией по размещению нативных представлений Android и iOS в вашем приложении Flutter с помощью Platform Views .

Putting a Webview on the screen

Замените содержимое файла lib/main.dart следующим:

lib/main.dart

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

void main() {
  runApp(
    MaterialApp(
      theme: ThemeData(useMaterial3: true),
      home: const 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,
      ),
    );
  }
}

При запуске на iOS или Android WebView будет отображаться в полноэкранном режиме, без каких-либо границ или полей. При прокрутке вы можете заметить, что некоторые части страницы выглядят немного странно. Это связано с тем, что JavaScript отключен, а для корректной отрисовки flutter.dev требуется JavaScript.

Запуск приложения

Запустите приложение Flutter на iOS или Android, чтобы увидеть WebView, отображающий веб-сайт flutter.dev . В качестве альтернативы запустите приложение в эмуляторе Android или симуляторе iOS. Вы можете заменить исходный URL WebView, например, на адрес своего собственного веб-сайта.

$ flutter run

Предполагая, что у вас запущен соответствующий симулятор или эмулятор, или подключено физическое устройство, после компиляции и развертывания приложения на вашем устройстве вы должны увидеть примерно следующее:

Симулятор iPhone, запускающий приложение Flutter со встроенным веб-представлением, отображающим домашнюю страницу Flutter.dev.

Эмулятор Android, запускающий приложение Flutter со встроенным веб-представлением, отображающим домашнюю страницу Flutter.dev.

5. Отслеживание событий загрузки страницы

Виджет WebView предоставляет несколько событий, отслеживающих ход загрузки страницы, которые может прослушивать ваше приложение. Во время цикла загрузки страницы WebView срабатывают три различных события: onPageStarted , onProgress и onPageFinished . На этом шаге вы реализуете индикатор загрузки страницы. В качестве бонуса это покажет, что вы можете отображать контент Flutter поверх области контента WebView .

Добавление событий загрузки страницы в ваше приложение

Создайте новый исходный файл в папке lib/src/web_view_stack.dart и заполните его следующим содержимым:

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

В этом коде виджет WebView обернут в Stack , и при необходимости поверх WebView отображается LinearProgressIndicator , когда процент загрузки страницы меньше 100%. Поскольку это связано с изменяющимся со временем состоянием программы, вы сохранили это состояние в классе State , связанном с StatefulWidget .

Чтобы использовать этот новый виджет WebViewStack , измените файл lib/main.dart следующим образом:

lib/main.dart

import 'package:flutter/material.dart';

import 'src/web_view_stack.dart';

void main() {
  runApp(
    MaterialApp(
      theme: ThemeData(useMaterial3: true),
      home: const 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(),
    );
  }
}

При запуске приложения, в зависимости от состояния вашей сети и от того, кэшировал ли браузер страницу, на которую вы переходите, вы увидите индикатор загрузки страницы, наложенный поверх области содержимого WebView .

6. Работа с WebViewController

Доступ к WebViewController из виджета WebView

Виджет WebView обеспечивает программное управление с помощью WebViewController . Этот контроллер становится доступен после создания виджета WebView через функцию обратного вызова. Асинхронный характер доступности этого контроллера делает его идеальным кандидатом для асинхронного класса Completer<T> в Dart.

Обновите lib/src/web_view_stack.dart следующим образом:

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

Теперь виджет WebViewStack использует контроллер, созданный в окружающем виджете. Это позволит использовать контроллер WebViewWidget совместно с другими частями приложения.

Создание элементов управления навигацией

Наличие работающего WebView — это одно, но возможность перемещаться вперед и назад по истории страниц и перезагружать страницу была бы полезным дополнением. К счастью, с помощью WebViewController вы можете добавить эту функциональность в свое приложение.

Создайте новый исходный файл в lib/src/navigation_controls.dart и заполните его следующим содержимым:

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

Этот виджет использует WebViewController , предоставленный ему на этапе создания, чтобы пользователь мог управлять WebView с помощью ряда IconButton .

Добавление элементов управления навигацией в панель приложений.

Имея на руках обновленный WebViewStack и новые NavigationControls , пришло время объединить все это в обновленное WebViewApp . Именно здесь мы создаем общий WebViewController . Поскольку WebViewApp находится в верхней части дерева виджетов в этом приложении, логично создавать его именно на этом уровне.

Обновите файл lib/main.dart следующим образом:

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(
    MaterialApp(
      theme: ThemeData(useMaterial3: true),
      home: const 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
    );
  }
}

При запуске приложения должна отобразиться веб-страница с элементами управления:

Симулятор iPhone, запускающий приложение Flutter со встроенным веб-представлением, отображающим домашнюю страницу Flutter.dev с элементами управления «Предыдущая страница», «Следующая страница» и «Перезагрузка страницы».

Эмулятор Android, запускающий приложение Flutter со встроенным веб-представлением, отображающим домашнюю страницу Flutter.dev с элементами управления «Предыдущая страница», «Следующая страница» и «Перезагрузка страницы».

7. Отслеживание навигации с помощью NavigationDelegate

WebView предоставляет вашему приложению NavigationDelegate, который позволяет отслеживать и управлять навигацией по страницам виджета WebView . Когда WebView, например, когда пользователь щелкает по ссылке, вызывается NavigationDelegate . Коллбэк NavigationDelegate можно использовать для управления тем, продолжит ли WebView навигацию.

Зарегистрируйте пользовательский NavigationDelegate

На этом шаге вы зарегистрируете функцию обратного вызова NavigationDelegate для блокировки перехода на YouTube.com . Обратите внимание, что эта упрощенная реализация также блокирует встроенный контент YouTube, что указано на различных страницах документации Flutter API.

Обновите файл lib/src/web_view_stack.dart следующим образом:

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

На следующем шаге вы добавите пункт меню, позволяющий тестировать ваш NavigationDelegate с помощью класса WebViewController . В качестве упражнения читателю предлагается дополнить логику обратного вызова, чтобы блокировать только полноэкранную навигацию на YouTube.com, но при этом разрешить отображение встроенного контента YouTube в документации API.

8. Добавление кнопки меню на панель приложений.

На следующих этапах вы создадите кнопку меню в виджете AppBar , которая будет использоваться для выполнения JavaScript-кода, вызова JavaScript-каналов и управления файлами cookie. В целом, это действительно полезное меню.

Создайте новый исходный файл в lib/src/menu.dart и заполните его следующим содержимым:

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'));
        }
      },
      itemBuilder: (context) => [
        const PopupMenuItem<_MenuOptions>(
          value: _MenuOptions.navigationDelegate,
          child: Text('Navigate to YouTube'),
        ),
      ],
    );
  }
}

Когда пользователь выбирает пункт меню «Перейти на YouTube» , выполняется метод loadRequest контроллера WebViewController . Эта навигация будет заблокирована колбэком navigationDelegate , созданным на предыдущем шаге.

Чтобы добавить меню на экран WebViewApp , измените lib/main.dart следующим образом:

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(
    MaterialApp(
      theme: ThemeData(useMaterial3: true),
      home: const 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),
    );
  }
}

Запустите приложение и нажмите на пункт меню «Перейти на YouTube» . Вы должны увидеть SnackBar, сообщающий о том, что контроллер навигации заблокировал переход на YouTube.

Эмулятор Android, запускающий приложение Flutter со встроенным веб-представлением, отображающим домашнюю страницу Flutter.dev с пунктом меню, предлагающим перейти на YouTube.

Эмулятор Android, запускающий приложение Flutter со встроенным веб-представлением, отображающим домашнюю страницу Flutter.dev с всплывающим сообщением «Блокировка навигации на m.youtube.com».

9. Оценка JavaScript

WebViewController может выполнять JavaScript-выражения в контексте текущей страницы. Существует два разных способа выполнения JavaScript-кода: для кода JavaScript, который не возвращает значение, используйте runJavaScript , а для кода JavaScript, который возвращает значение, используйте runJavaScriptReturningResult .

Для включения JavaScript необходимо настроить WebViewController , установив свойство javaScriptMode в значение JavascriptMode.unrestricted . По умолчанию javascriptMode установлено в значение JavascriptMode.disabled .

Обновите класс _WebViewStackState , добавив параметр javascriptMode следующим образом:

lib/src/web_view_stack.dart

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

  @override
  void initState() {
    super.initState();
    widget.controller
      ..setNavigationDelegate(              // Modify this line to use .. instead of .
        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);        // Add this line
  }

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

Теперь, когда WebViewWidget может выполнять JavaScript, вы можете добавить в меню опцию для использования метода runJavaScriptReturningResult .

Используя редактор или клавиатуру, преобразуйте класс Menu в StatefulWidget. Измените lib/src/menu.dart в соответствии со следующим:

lib/src/menu.dart

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

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

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

  final WebViewController controller;

  @override                                               // Add from here
  State<Menu> createState() => _MenuState();
}

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

При нажатии на пункт меню «Показать user-agent» результат выполнения JavaScript-выражения navigator.userAgent отображается в Snackbar . При запуске приложения вы можете заметить, что страница Flutter.dev выглядит иначе. Это результат запуска с включенным JavaScript.

Симулятор iPhone, на котором запущено приложение Flutter со встроенным веб-представлением, отображающим домашнюю страницу Flutter.dev с пунктами меню, предлагающими варианты «Перейти на YouTube» или «Показать user-agent».

Симулятор iPhone, на котором запущено приложение Flutter со встроенным веб-представлением, отображающим домашнюю страницу Flutter.dev с всплывающим сообщением, показывающим строку пользовательского агента.

10. Работа с каналами JavaScript

Каналы JavaScript позволяют вашему приложению регистрировать обработчики обратных вызовов в контексте JavaScript WebViewWidget , которые могут быть вызваны для передачи значений обратно в код Dart приложения. На этом шаге вы зарегистрируете канал SnackBar , который будет вызываться с результатом XMLHttpRequest .

Обновите класс WebViewStack следующим образом:

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

Для каждого канала JavaScript в Set объектов объект канала становится доступен в контексте JavaScript в качестве свойства окна, имеющего то же имя, что и name канала JavaScript. Использование этого объекта из контекста JavaScript включает вызов postMessage для канала JavaScript, чтобы отправить сообщение, которое передается обработчику обратного вызова onMessageReceived именованного JavascriptChannel .

Чтобы использовать добавленный ранее JavaScript-канал, добавьте еще один пункт меню, который выполняет XMLHttpRequest в контексте JavaScript и передает результаты обратно, используя JavaScript-канал SnackBar .

Теперь, когда WebViewWidget знает о наших каналах JavaScript , вы можете добавить пример, чтобы расширить функциональность приложения. Для этого добавьте дополнительный элемент PopupMenuItem в класс Menu и добавьте дополнительную функциональность.

Обновите класс _MenuOptions , добавив дополнительный пункт меню, указав значение перечисления javascriptChannel , и добавьте реализацию в класс Menu следующим образом:

lib/src/menu.dart

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

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'));
          case _MenuOptions.userAgent:
            final userAgent = await widget.controller
                .runJavaScriptReturningResult('navigator.userAgent');
            if (!context.mounted) return;
            ScaffoldMessenger.of(context).showSnackBar(SnackBar(
              content: Text('$userAgent'),
            ));
          case _MenuOptions.javascriptChannel:            // Add from here
            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();''');                                          // 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>(                // Add from here
          value: _MenuOptions.javascriptChannel,
          child: Text('Lookup IP Address'),
        ),                                                // To here.
      ],
    );
  }
}

Этот JavaScript-код выполняется, когда пользователь выбирает пункт меню «Пример канала JavaScript» .

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

Этот код отправляет GET запрос к API общедоступных IP-адресов, возвращая IP-адрес устройства. Результат отображается в SnackBar путем вызова postMessage в JavascriptChannel SnackBar .

11. Управление файлами cookie

Ваше приложение может управлять файлами cookie в WebView с помощью класса CookieManager . На этом шаге вы отобразите список файлов cookie, очистите список, удалите файлы cookie и установите новые. Добавьте записи в _MenuOptions для каждого из вариантов использования файлов cookie следующим образом:

lib/src/menu.dart

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

Остальные изменения на этом этапе сосредоточены на классе Menu , включая преобразование класса Menu из stateless в stateful. Это изменение важно, поскольку Menu должен владеть CookieManager , а изменяемое состояние в stateless-виджетах — неудачное сочетание.

Добавьте класс CookieManager в получившийся класс State следующим образом:

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

Класс _MenuState будет содержать код, ранее добавленный в класс Menu , а также недавно добавленный класс CookieManager . В следующих разделах вы добавите в класс _MenuState вспомогательные функции, которые, в свою очередь, будут вызываться еще не добавленными пунктами меню.

Получить список всех файлов cookie

Вам нужно будет использовать JavaScript для получения списка всех cookie-файлов. Для этого добавьте в конец класса _MenuState вспомогательный метод с именем _onListCookies . Используя метод runJavaScriptReturningResult , ваш вспомогательный метод выполнит document.cookie в контексте JavaScript, возвращая список всех cookie-файлов.

Добавьте следующий код в класс _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.'),
    ),
  );
}

Очистить все файлы cookie

Чтобы удалить все файлы cookie в WebView, используйте метод clearCookies класса CookieManager . Метод возвращает объект Future<bool> , который принимает значение true , если CookieManager очистил файлы cookie, и false если файлов cookie для очистки не было.

Добавьте следующий код в класс _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),
    ),
  );
}

Добавить cookie можно с помощью JavaScript. API, используемый для добавления Cookie в документ JavaScript, подробно описан на MDN .

Добавьте следующий код в класс _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.'),
    ),
  );
}

Также файлы cookie можно устанавливать с помощью CookieManager следующим образом.

Добавьте следующий код в класс _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.'),
    ),
  );
}

Удаление cookie-файла подразумевает добавление cookie-файла с датой истечения срока действия, установленной в прошлом.

Добавьте следующий код в класс _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.'),
    ),
  );
}

Добавление пунктов меню CookieManager

Осталось только добавить пункты меню и связать их с только что добавленными вспомогательными методами. Обновите класс _MenuState следующим образом:

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'));
          case _MenuOptions.userAgent:
            final userAgent = await widget.controller
                .runJavaScriptReturningResult('navigator.userAgent');
            if (!context.mounted) return;
            ScaffoldMessenger.of(context).showSnackBar(SnackBar(
              content: Text('$userAgent'),
            ));
          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();''');
          case _MenuOptions.clearCookies:                        // Add from here
            await _onClearCookies();
          case _MenuOptions.listCookies:
            await _onListCookies(widget.controller);
          case _MenuOptions.addCookie:
            await _onAddCookie(widget.controller);
          case _MenuOptions.setCookie:
            await _onSetCookie(widget.controller);
          case _MenuOptions.removeCookie:
            await _onRemoveCookie(widget.controller);            // 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>(                       // Add from here
          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.
      ],
    );
  }

Использование CookieManager

Чтобы использовать все функции, которые вы только что добавили в приложение, выполните следующие шаги:

  1. Выберите «Список файлов cookie» . В нем должен отобразиться список файлов cookie Google Analytics, установленных flutter.dev.
  2. Выберите «Очистить файлы cookie» . Должно отобразиться сообщение о том, что файлы cookie действительно были удалены.
  3. Снова выберите «Очистить файлы cookie» . Должно появиться сообщение о том, что файлы cookie для очистки отсутствуют.
  4. Выберите пункт «Список файлов cookie» . Должно отобразиться сообщение об отсутствии файлов cookie.
  5. Выберите «Добавить cookie» . Система должна сообщить, что cookie добавлен.
  6. Выберите «Установить cookie» . Система должна сообщить, что cookie установлен.
  7. Выберите «Список файлов cookie» , а затем, в завершение, выберите «Удалить файл cookie» .

Эмулятор Android, запускающий приложение Flutter со встроенным веб-представлением, отображающим домашнюю страницу Flutter.dev со списком пунктов меню, включающих переход на YouTube, отображение пользовательского агента и взаимодействие с файлами cookie браузера.

Эмулятор Android, запускающий приложение Flutter со встроенным веб-представлением, отображающим домашнюю страницу Flutter.dev с всплывающим сообщением, показывающим файлы cookie, установленные в браузере.

Эмулятор Android, запускающий приложение Flutter со встроенным веб-представлением, отображающим домашнюю страницу Flutter.dev с всплывающим сообщением: «Были файлы cookie. Теперь их нет!»

Эмулятор Android, запускающий приложение Flutter со встроенным веб-представлением, отображающим домашнюю страницу Flutter.dev с всплывающим сообщением «Добавлен пользовательский cookie».

12. Загрузка ресурсов Flutter, файлов и HTML-строк в WebView.

Ваше приложение может загружать HTML-файлы различными способами и отображать их в WebView. На этом шаге вы загрузите ресурс Flutter, указанный в файле pubspec.yaml , загрузите файл, расположенный по указанному пути, и загрузите страницу, используя HTML-строку.

Если вы хотите загрузить файл, расположенный по указанному пути, вам потребуется добавить path_provider в файл pubspec.yaml . Это плагин Flutter для поиска часто используемых расположений в файловой системе.

В командной строке выполните следующую команду:

$ flutter pub add path_provider

Для загрузки ресурса необходимо указать путь к нему в файле pubspec.yaml . Добавьте в pubspec.yaml следующие строки:

pubspec.yaml

# The following section is specific to Flutter packages.
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.

Чтобы добавить ресурсы в свой проект, выполните следующие шаги:

  1. Создайте новую директорию с именем assets в корневой папке вашего проекта.
  2. Создайте новую директорию с именем www в папке assets .
  3. Создайте новую директорию с именем styles в папке www .
  4. Создайте новый файл с именем index.html в папке www .
  5. Создайте новый файл с именем style.css в папке styles .

Скопируйте и вставьте следующий код в файл 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>

Для настройки стиля заголовка HTML в файле style.css используйте следующие несколько строк:

assets/www/styles/style.css

h1 {
  color: blue;
}

Теперь, когда ресурсы настроены и готовы к использованию, вы можете реализовать методы, необходимые для загрузки и отображения ресурсов Flutter, файлов или HTML-строк.

Загрузить ресурс Flutter

Для загрузки только что созданного ресурса вам нужно всего лишь вызвать метод ` loadFlutterAsset с помощью ` WebViewController и передать в качестве параметра путь к ресурсу. Добавьте следующий метод в конец вашего кода:

lib/src/menu.dart

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

Загрузить локальный файл

Для загрузки файла на ваше устройство вы можете добавить метод, который будет использовать метод loadFile , опять же, используя WebViewController , который принимает String , содержащую путь к файлу.

Сначала вам нужно создать файл, содержащий HTML-код. Это можно сделать, добавив HTML-код в виде строки в начало файла menu.dart , сразу после импортов.

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.

Для создания File и записи в него HTML-строки вам потребуется добавить два метода. Метод ` _onLoadLocalFileExample загрузит файл, указав путь в виде строки, возвращаемой методом _prepareLocalFile() . Добавьте следующие методы в свой код:

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

Загрузить HTML-строку

Отобразить страницу, передав HTML-строку, довольно просто. В WebViewController есть метод ` loadHtmlString , в который можно передать HTML-строку в качестве аргумента. Затем WebView отобразит предоставленную HTML-страницу. Добавьте следующий метод в свой код:

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.

Добавить пункты меню

Теперь, когда ресурсы настроены и готовы к использованию, а методы со всей необходимой функциональностью созданы, меню можно обновить. Добавьте следующие записи в перечисление _MenuOptions :

lib/src/menu.dart

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

Теперь, когда перечисление обновлено, вы можете добавить пункты меню и связать их с только что добавленными вспомогательными методами. Обновите класс _MenuState следующим образом:

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'));
          case _MenuOptions.userAgent:
            final userAgent = await widget.controller
                .runJavaScriptReturningResult('navigator.userAgent');
            if (!context.mounted) return;
            ScaffoldMessenger.of(context).showSnackBar(SnackBar(
              content: Text('$userAgent'),
            ));
          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();''');
          case _MenuOptions.clearCookies:
            await _onClearCookies();
          case _MenuOptions.listCookies:
            await _onListCookies(widget.controller);
          case _MenuOptions.addCookie:
            await _onAddCookie(widget.controller);
          case _MenuOptions.setCookie:
            await _onSetCookie(widget.controller);
          case _MenuOptions.removeCookie:
            await _onRemoveCookie(widget.controller);
          case _MenuOptions.loadFlutterAsset:             // Add from here
            if (!mounted) return;
            await _onLoadFlutterAssetExample(widget.controller, context);
          case _MenuOptions.loadLocalFile:
            if (!mounted) return;
            await _onLoadLocalFileExample(widget.controller, context);
          case _MenuOptions.loadHtmlString:
            if (!mounted) return;
            await _onLoadHtmlStringExample(widget.controller, context);
                                                          // 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'),
        ),
        const PopupMenuItem<_MenuOptions>(                // Add from here
          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.
      ],
    );
  }

Проверка ресурсов, файла и HTML-строки.

Чтобы проверить работоспособность только что реализованного кода, запустите его на своем устройстве и щелкните один из новых пунктов меню. Обратите внимание, как _onLoadFlutterAssetExample использует добавленный нами style.css для изменения цвета заголовка HTML-файла на синий.

Эмулятор Android, запускающий приложение Flutter со встроенным веб-представлением, отображает страницу с заголовком «Локальная демонстрационная страница», выделенным синим цветом.

Эмулятор Android, запускающий приложение Flutter со встроенным веб-представлением, отображает страницу с заголовком «Локальная демонстрационная страница», выделенным черным цветом.

13. Всё готово!

Поздравляем!!! Вы успешно завершили выполнение задания. Полный код для этого задания можно найти в репозитории codelab .

Чтобы узнать больше, попробуйте другие примеры кода Flutter .