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-код.
|
|
Что вы узнаете
В этом практическом занятии вы узнаете, как использовать плагин webview_flutter различными способами, в том числе:
- How to configure the
webview_flutterplugin - Как отслеживать события, связанные с прогрессом загрузки страницы
- 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
Предполагая, что у вас запущен соответствующий симулятор или эмулятор, или подключено физическое устройство, после компиляции и развертывания приложения на вашем устройстве вы должны увидеть примерно следующее:
|
|
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
);
}
}
При запуске приложения должна отобразиться веб-страница с элементами управления:
|
|
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.
|
|
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.
|
|
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
Добавить 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
Также файлы 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-файла подразумевает добавление 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
Чтобы использовать все функции, которые вы только что добавили в приложение, выполните следующие шаги:
- Выберите «Список файлов cookie» . В нем должен отобразиться список файлов cookie Google Analytics, установленных flutter.dev.
- Выберите «Очистить файлы cookie» . Должно отобразиться сообщение о том, что файлы cookie действительно были удалены.
- Снова выберите «Очистить файлы cookie» . Должно появиться сообщение о том, что файлы cookie для очистки отсутствуют.
- Выберите пункт «Список файлов cookie» . Должно отобразиться сообщение об отсутствии файлов cookie.
- Выберите «Добавить cookie» . Система должна сообщить, что cookie добавлен.
- Выберите «Установить cookie» . Система должна сообщить, что cookie установлен.
- Выберите «Список файлов cookie» , а затем, в завершение, выберите «Удалить файл 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.
Чтобы добавить ресурсы в свой проект, выполните следующие шаги:
- Создайте новую директорию с именем
assetsв корневой папке вашего проекта. - Создайте новую директорию с именем
wwwв папкеassets. - Создайте новую директорию с именем
stylesв папкеwww. - Создайте новый файл с именем
index.htmlв папкеwww. - Создайте новый файл с именем
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-файла на синий.
|
|
13. Всё готово!
Поздравляем!!! Вы успешно завершили выполнение задания. Полный код для этого задания можно найти в репозитории codelab .
Чтобы узнать больше, попробуйте другие примеры кода Flutter .















