1. Введение
Последнее обновление: 19 октября 2021 г.
С помощью плагина WebView Flutter вы можете добавить виджет WebView в свое приложение Flutter для Android или iOS. В iOS виджет WebView поддерживается WKWebView , а в Android виджет WebView поддерживается WebView . Плагин может отображать виджеты Flutter поверх веб-представления. Например, можно отобразить раскрывающееся меню поверх веб-представления.
Что ты построишь
В этой лаборатории кода вы шаг за шагом создадите мобильное приложение с WebView с использованием Flutter SDK. Ваше приложение будет:
- Отображение веб-контента в
WebView
- Отображать виджеты Flutter, расположенные поверх
WebView
- Реагировать на события прогресса загрузки страницы
- Управляйте
WebView
черезWebViewController
- Блокируйте веб-сайты с помощью
NavigationDelegate
- Оценка выражений JavaScript
- Обработка обратных вызовов из JavaScript с помощью
JavascriptChannels
- Установить, удалить, добавить или показать файлы cookie
- Загружать и отображать HTML из ресурсов, файлов или строк, содержащих HTML.
Что вы узнаете
В этой лаборатории вы узнаете, как использовать плагин webview_flutter
различными способами, в том числе:
- Как настроить плагин
webview_flutter
- Как прослушивать события хода загрузки страницы
- Как управлять навигацией по страницам
- Как дать команду
WebView
перемещаться вперед и назад по своей истории - Как оценить JavaScript, в том числе используя возвращаемые результаты
- Как зарегистрировать обратные вызовы для вызова кода Dart из JavaScript
- Как управлять файлами cookie
- Как загружать и отображать HTML-страницы из ресурсов или файлов или строки, содержащей HTML.
Что вам понадобится
- Android Studio 4.1 или новее (для разработки под Android)
- Xcode 12 или новее (для разработки под iOS)
- Флаттер SDK
- Редактор кода, например Android Studio , Visual Studio Code или Emacs .
2. Настройте среду разработки Flutter.
Для выполнения этой лабораторной работы вам понадобятся два программного обеспечения — Flutter SDK и редактор .
Вы можете запустить кодовую лабораторию, используя любое из этих устройств:
- Физическое устройство Android или iOS , подключенное к вашему компьютеру и переведенное в режим разработчика.
- Симулятор iOS (требуется установка инструментов Xcode).
- Эмулятор Android (требуется установка в Android Studio).
3. Начало работы
Начало работы с 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.
Добавление плагина WebView Flutter в качестве зависимости
Добавить дополнительные возможности в приложение 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 , вы увидите, что в разделе зависимостей есть строка для плагина webview_flutter
.
Настройка Android minSDK
Чтобы использовать плагин webview_flutter
на Android, вам необходимо установить minSDK
значение 20
. Измените файл android/app/build.gradle
следующим образом:
Android/приложение/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
в свое приложение. WebViews — это размещенные собственные представления, и вы, как разработчик приложения, можете выбрать, как разместить эти собственные представления в своем приложении. На Android у вас есть выбор между виртуальными дисплеями (в настоящее время используется по умолчанию для Android) и гибридной композицией. Однако iOS всегда использует гибридную композицию.
Для более подробного обсуждения различий между виртуальными дисплеями и гибридной композицией прочтите документацию по размещению собственных представлений Android и iOS в вашем приложении Flutter с представлениями платформы .
Вывод веб-просмотра на экран
Замените содержимое lib/main.dart
следующим:
библиотека/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, чтобы увидеть веб-просмотр, в котором отображается веб-сайт 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
следующим образом:
библиотека/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
.
Добавление элементов управления навигацией в AppBar
Имея в руках обновленный WebViewStack
и недавно созданные NavigationControls
, настало время объединить все это в обновленном WebViewApp
. Здесь мы создаем общий WebViewController
. Поскольку WebViewApp
находится в верхней части дерева виджетов в этом приложении, имеет смысл создать его на этом уровне.
Обновите файл lib/main.dart
следующим образом:
библиотека/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
В течение следующих нескольких шагов вы создадите кнопку меню в виджете 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
следующим образом:
библиотека/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.
],
);
}
}
Когда вы нажимаете на пункт меню «Показать пользовательский агент», результат выполнения выражения 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
в SnackBar
JavascriptChannel
.
11. Управление файлами cookie
Ваше приложение может управлять файлами cookie в WebView
с помощью класса CookieManager
. На этом этапе вы отобразите список файлов cookie, очистите список файлов cookie, удалите файлы 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
из режима без сохранения состояния в режим с сохранением состояния. Это изменение важно, поскольку Menu
должен владеть CookieManager
, а изменяемое состояние в виджетах без сохранения состояния — плохая комбинация.
Добавьте 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
:
<!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>
Для style.css используйте следующие несколько строк, чтобы установить стиль заголовка HTML:
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
сразу под импортом.
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()
. Добавьте в свой код следующие методы:
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-страницу. Добавьте в свой код следующий метод:
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. Все готово!
Поздравляю!!! Вы завершили кодовую лабораторию. Готовый код этой кодовой лаборатории вы можете найти в репозитории кодовой лаборатории .
Чтобы узнать больше, попробуйте другие лаборатории разработки кода Flutter .