1. Introduction
Last Updated: 2021-10-19
With the WebView Flutter plugin you can add a WebView widget to your Android or iOS Flutter app. On iOS the WebView widget is backed by a WKWebView, while on Android the WebView widget is backed by a WebView. The plugin can render Flutter widgets over the web view. So for example it's possible to render a drop down menu over the web view.
What you'll build
In this codelab, you'll build a mobile app step by step featuring a WebView using the Flutter SDK. Your app will:
- Display web content in a
WebView
- Display Flutter widgets stacked over the
WebView
- React to page load progress events
- Control the
WebView
through theWebViewController
- Block websites using the
NavigationDelegate
- Evaluate JavaScript expressions
- Handle callbacks from JavaScript with
JavascriptChannels
- Set, remove, add or show cookies
- Load and display HTML from assets, files or Strings containing HTML
What you'll learn
In this codelab you'll learn how to use the webview_flutter
plugin in a variety of ways, including:
- How to configure the
webview_flutter
plugin - How to listen for page load progress events
- How to control page navigation
- How to command the
WebView
to go backwards and forwards through its history - How to evaluate JavaScript, including using returned results
- How to register callbacks to call Dart code from JavaScript
- How to manage cookies
- How to load and display HTML pages from assets or files or String containing HTML
What you'll need
- Android Studio 4.1 or later (for Android development)
- Xcode 12 or later (for iOS development)
- Flutter SDK
- A code editor, such as Android Studio, Visual Studio Code, or Emacs.
2. Set up your Flutter development environment
You need two pieces of software to complete this lab—the Flutter SDK and an editor.
You can run the codelab using any of these devices:
- A physical Android or iOS device connected to your computer and set to Developer mode.
- The iOS simulator (requires installing Xcode tools).
- The Android Emulator (requires setup in Android Studio).
3. Getting started
Getting started with Flutter
There are a variety of ways of creating a new Flutter project, with both Android Studio and Visual Studio Code providing tooling for this task. Either follow the linked procedures to create a project, or execute the following commands in a handy command line terminal.
$ 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
Adding additional capability to a Flutter app is easy using Pub packages. In this codelab you will add the webview_flutter
plugin to your project. Run the following commands in the terminal.
$ 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.
If you inspect your pubspec.yaml, you will now see it has a line in the dependencies section for the webview_flutter
plugin.
Configure Android minSDK
To use the webview_flutter
plugin on Android you need to set the minSDK
to 20
. Modify your android/app/build.gradle
file as follows:
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. Adding WebView widget to the Flutter App
In this step you will add a WebView
to your application. WebViews are hosted native views, and you as an app developer have a choice on how to host these native views in your app. On Android you have a choice between Virtual Displays, currently the default for Android, and Hybrid composition. However, iOS always uses Hybrid composition.
For an in depth discussion of the differences between Virtual Displays and Hybrid composition, please read through the documentation on Hosting native Android and iOS views in your Flutter app with Platform Views.
Putting a Webview on the screen
Replace the contents of lib/main.dart
with the following:
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,
),
);
}
}
Running this on iOS or Android will show a WebView as a full bleed browser window on your device, which means that the browser is shown on your device in fullscreen without any form of border or margin. As you scroll, you will notice parts of the page that might look a bit odd. This is because JavaScript is currently disabled and rendering flutter.dev properly requires JavaScript.
Running the app
Run the Flutter app in either iOS or Android to see a Webview, which displays the flutter.dev website. Alternatively run the app in either an Android emulator or an iOS simulator. Feel free to replace the initial WebView URL with for example your own website.
$ flutter run
Assuming that you have the appropriate simulator or emulator running, or a physical device attached, after compiling and deploying the app to your device, you should see something like the following:
5. Listening for page load events
The WebView
widget provides several page load progress events, which your app can listen to. During the WebView
page load cycle there are three different page load events that are fired: onPageStarted
, onProgress
, and onPageFinished
. In this step you will implement a page load indicator. As a bonus, this will show that you can render Flutter content over the WebView
content area.
Adding page load events to your app
Create a new source file at lib/src/web_view_stack.dart
and fill it with the following content:
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,
),
],
);
}
}
This code has wrapped the WebView
widget in a Stack
, conditionally overlaying the WebView
with a LinearProgressIndicator
when the page load percentage is less than 100%. As this involves program state that changes over time, you have stored this state in a State
class associated with a StatefulWidget
.
To make use of this new WebViewStack
widget, modify your lib/main.dart
as follows:
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(),
);
}
}
When you run the app, depending on your network conditions, and whether the browser has cached the page you are navigating to, you will see a page loading indicator overlaid on top of the WebView
content area.
6. Working with the WebViewController
Accessing the WebViewController from the WebView Widget
The WebView
widget enables programmatic control with a WebViewController
. This controller is made available after the construction of the WebView
widget through a callback. The asynchronous nature of the availability of this controller makes it a prime candidate for Dart's asynchronous Completer<T>
class.
Update lib/src/web_view_stack.dart
as follows:
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,
),
],
);
}
}
The WebViewStack
widget now uses a controller created in the surrounding widget. This will enable the controller for the WebViewWidget
to be shared with other parts of the app easily.
Crafting Navigation Controls
Having a working WebView
is one thing, but being able to navigate backwards and forwards through the page history, and reload the page, would be a useful set of additions. Thankfully, with a WebViewController
you can add this functionality to your app.
Create a new source file at lib/src/navigation_controls.dart
and fill it with the following:
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();
},
),
],
);
}
}
This widget uses the WebViewController
shared with it at construction time to enable the user control the WebView
through a series of IconButton
s.
Adding navigation controls to the AppBar
With the updated WebViewStack
, and the newly crafted NavigationControls
in hand, it is now time for you to put it all together in an updated WebViewApp
. This is where we construct the shared WebViewController
. With WebViewApp
near the top of the Widget tree in this app, it makes sense to create it at this level.
Update lib/main.dart
file as follows:
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
);
}
}
Running the app should reveal a web page with controls:
7. Keeping track of navigation with the NavigationDelegate
WebView
provides your app with a NavigationDelegate,
which enables your app to track and control the page navigation of the WebView
widget. When a navigation is initiated by the WebView,
for example when a user clicks on a link, the NavigationDelegate
is called. The NavigationDelegate
callback can be used to control whether the WebView
proceeds with the navigation.
Register a custom NavigationDelegate
In this step, you will register a NavigationDelegate
callback to block navigation to YouTube.com. Note, this simplistic implementation also blocks inline YouTube content, which appears in various Flutter API documentation pages.
Update the lib/src/web_view_stack.dart
as follows:
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,
),
],
);
}
}
In the next step, you will add a menu item to enable testing your NavigationDelegate
by using the WebViewController
class. It is left as an exercise to the reader to augment the logic of the callback to only block full page navigation to YouTube.com, and still allow the inline YouTube content in the API documentation.
8. Adding a menu button to the AppBar
Over the next few steps, you will craft a menu button in the AppBar
widget that is used to evaluate JavaScript, invoke JavaScript channels, and manage cookies. All in all, a useful menu indeed.
Create a new source file at lib/src/menu.dart
and fill it with the following:
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'),
),
],
);
}
}
When the user selects the Navigate to YouTube menu option, the WebViewController
's loadRequest
method is executed. This navigation will be blocked by the navigationDelegate
callback you created in the previous step.
To add the menu to the WebViewApp
's screen, modify lib/main.dart
as follows:
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),
);
}
}
Run your app and tap on the Navigate to YouTube menu item. You should be greeted with a SnackBar informing you that the navigation controller blocked navigating to YouTube.
9. Evaluating JavaScript
The WebViewController
can evaluate JavaScript expressions in the context of the current page. There are two different ways to evaluate JavaScript: for JavaScript code that doesn't return a value, use runJavaScript
, and for JavaScript code that does return a value, use runJavaScriptReturningResult
.
To enable JavaScript, you need to configure the WebViewController
with the javaScriptMode
property set to JavascriptMode.unrestricted
. By default, javascriptMode
is set to JavascriptMode.disabled
.
Update the _WebViewStackState
class by adding the javascriptMode
setting as follows:
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,
),
],
);
}
}
Now that the WebViewWidget
can execute JavaScript, you can add an option to the menu to use the runJavaScriptReturningResult
method.
Using either your Editor or some keyboard work, convert the Menu class to a StatefulWidget. Modify lib/src/menu.dart
to match the following:
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.
],
);
}
}
When you tap on the ‘Show user-agent' menu option, the result of executing the JavaScript expression navigator.userAgent
is shown in a Snackbar
. When running the app, you might notice that the Flutter.dev page looks different. This is the result of running with JavaScript enabled.
10. Working with JavaScript Channels
Javascript Channels enable your app to register callback handlers in the WebViewWidget
's JavaScript context that can be invoked to convey values back to the App's Dart code. In this step you will register a SnackBar
channel that will be called with the result of a XMLHttpRequest
.
Update the WebViewStack
class as follows:
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,
),
],
);
}
}
For each JavaScript Channel in the Set
, a channel object is made available in the JavaScript context as a window property named with the same name as the JavaScript Channel name
. Using this from the JavaScript context involves calling postMessage
on the JavaScript Channel to send a message that is passed to the named JavascriptChannel
's onMessageReceived
callback handler.
To make use of the Javascript Channel added above, add another menu item that executes an XMLHttpRequest
in the JavaScript context and passes back the results using the SnackBar
JavaScript Channel.
Now that WebViewWidget
knows about our JavaScript Channels,
you will add an example to expand the app further. To do this, add an extra PopupMenuItem
to the Menu
class and add the extra functionality.
Update _MenuOptions
with the extra menu option, by adding the javascriptChannel
enumeration value, and add an implementation to the Menu
class as follows:
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.
],
);
}
}
This JavaScript is executed when the user chooses the JavaScript Channel Example menu option.
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();
This code sends a GET
request to a Public IP Address API, returning the device's IP address. This result is shown in a SnackBar
by invoking postMessage
on the SnackBar
JavascriptChannel
.
11. Managing Cookies
Your app can manage cookies in the WebView
by using the CookieManager
class. In this step, you are going to show a list of cookies, clear the list of cookies, delete cookies, and set new cookies. Add entries to the _MenuOptions
for each of the cookie use cases as follows:
lib/src/menu.dart
enum _MenuOptions {
navigationDelegate,
userAgent,
javascriptChannel,
// Add from here ...
listCookies,
clearCookies,
addCookie,
setCookie,
removeCookie,
// ... to here.
}
The rest of the changes in this step are focused on the Menu
class, including the conversion of the Menu
class from stateless to stateful. This change is important because the Menu
needs to own the CookieManager
, and mutable state in stateless widgets is a bad combination.
Add the CookieManager to the resulting State class as follows:
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) {
// ...
The _MenuState
class will contain the code previously added in the Menu
class, along with the newly added CookieManager
. In the next series of sections, you will add helper functions to _MenuState
that will, in turn, be invoked by the yet to be added menu items.
Get a list of all cookies
You are going to use JavaScript to get a list of all the cookies. To achieve this, add a helper method to the end of _MenuState
class, called _onListCookies
. Using the runJavaScriptReturningResult
method, your helper method executes document.cookie
in the JavaScript context, returning a list of all cookies.
Add the following to the _MenuState
class:
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.'),
),
);
}
Clear all cookies
To clear all the cookies in the WebView, use the clearCookies
method of the CookieManager
class. The method returns a Future<bool>
that resolves to true
if the CookieManager
cleared the cookies, and false
if there were no cookies to clear.
Add the following to the _MenuState
class:
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),
),
);
}
Add a cookie
Adding a cookie can be done by invoking JavaScript. The API used to add a Cookie to a JavaScript document is documented in depth on MDN.
Add the following to the _MenuState
class:
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.'),
),
);
}
Setting a cookie with the CookieManager
Cookies can also be set using the CookieManager as follows.
Add the following to the _MenuState
class:
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.'),
),
);
}
Remove a cookie
Removing a cookie involves adding a cookie, with an expiry date set in the past.
Add the following to the _MenuState
class:
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.'),
),
);
}
Adding the CookieManager Menu items
All that remains is to add the menu options, and wire them to the helper methods you just added. Update the _MenuState
class as follows:
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.
],
);
}
Exercising the CookieManager
To use all the functionality you have just added to the app, try the following steps:
- Select List cookies. It should list the Google Analytics cookies set by flutter.dev.
- Select Clear cookies. It should report that the cookies were indeed cleared.
- Select Clear cookies again. It should report that no cookies were available to clear.
- Select List cookies. It should report that there are no cookies.
- Select Add cookie. It should report the cookie as added.
- Select Set cookie. It should report the cookie as set.
- Select List cookies, and then as a final flourish, select Remove cookie.
12. Load Flutter assets, files and HTML strings in the WebView
Your app can load HTML files using different methods and display them in the WebView. In this step you will load a Flutter asset specified in the pubspec.yaml
file, load a file located at the specified path and load a page using a HTML String.
If you want to load a file located at a specified path, you will need to add the path_provider
to the pubspec.yaml
. This is a Flutter plugin for finding commonly used locations on the filesystem.
On the command line, run the following command:
$ flutter pub add path_provider
For loading the asset we need to specify the path to the asset in the pubspec.yaml
. In the pubspec.yaml
add the following lines:
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.
To add the assets to your project, do the following steps:
- Create a new Directory with the name
assets
in the root folder of your project. - Create a new Directory with the name
www
in theassets
folder. - Create a new Directory with the name
styles
in thewww
folder. - Create a new File with the name
index.html
in thewww
folder. - Create a new File with the name
style.css
in thestyles
folder.
Copy and paste the following code in the index.html
file:
<!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>
For the style.css use the following few lines to set the HTML header style:
h1 {
color: blue;
}
Now that the assets are set and ready to use, you can implement the methods that are needed for loading and displaying Flutter assets, files or HTML Strings.
Load Flutter asset
For loading the asset you just created, all you need to do is call the loadFlutterAsset
method using the WebViewController
and give as parameter the path to the asset. Add the following method at the end of your code:
lib/src/menu.dart
Future<void> _onLoadFlutterAssetExample(
WebViewController controller, BuildContext context) async {
await controller.loadFlutterAsset('assets/www/index.html');
}
Load local file
For loading a file on your device you can add a method that will use the loadFile
method, again by using the WebViewController
which takes a String
containing the path to the file.
You need to make a file containing the HTML code first. You can simply do this by adding the HTML code as a String at the top of your code in the menu.dart
file just beneath the imports.
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.
To create a File
and write the HTML String to the file you will add two methods. The _onLoadLocalFileExample
will load the file by providing the path as a String which is returned by the _prepareLocalFile()
method. Add the following methods to your code:
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;
}
Load HTML String
To display a page by providing a HTML string is pretty straight forward. The WebViewController
has a method you can use called loadHtmlString
where you can give the HTML String as an argument. The WebView
will then display the provided HTML page. Add the following method to your code:
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.
Add the menu items
Now that the assets are set and ready for use, and the methods with all the functionality are made, the menu can be updated. Add the following entries to the _MenuOptions
enum:
lib/src/menu.dart
enum _MenuOptions {
navigationDelegate,
userAgent,
javascriptChannel,
listCookies,
clearCookies,
addCookie,
setCookie,
removeCookie,
// Add from here ...
loadFlutterAsset,
loadLocalFile,
loadHtmlString,
// ... to here.
}
Now that the enum is updated you can add the menu options, and wire them to the helper methods you just added. Update the _MenuState
class as follows:
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.
],
);
}
Testing the assets, file, and HTML string
To test if the code worked that you just implemented, you can run the code on your device and click on one of the newly added menu items. Notice how the _onLoadFlutterAssetExample
uses the style.css
we added to change the header of the HTML file to the color blue.
13. All done!
Congratulations!!! You have completed the codelab. You can find the completed code for this codelab in the codelab repository.
To learn more, try the other Flutter codelabs.