Adding WebView to your Flutter app

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 the WebViewController
  • 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

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:

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 webview_in_flutter
Creating project webview_in_flutter...
[Listing of created files elided]
Wrote 81 files.

All done!
In order to run your application, type:

  $ cd webview_in_flutter
  $ flutter run

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

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...
  async 2.8.1 (2.8.2 available)
  characters 1.1.0 (1.2.0 available)
  matcher 0.12.10 (0.12.11 available)
+ plugin_platform_interface 2.0.2
  test_api 0.4.2 (0.4.8 available)
  vector_math 2.1.0 (2.1.1 available)
+ webview_flutter 3.0.0
+ webview_flutter_android 2.8.0
+ webview_flutter_platform_interface 1.8.0
+ webview_flutter_wkwebview 2.7.0
Downloading webview_flutter 3.0.0...
Downloading webview_flutter_wkwebview 2.7.0...
Downloading webview_flutter_android 2.8.0...
Changed 5 dependencies!

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 19 or 20, depending which Android Platform View you want to use. More information about the Android Platform View can be found on the webview_flutter plugin page. Modify your android/app/build.gradle file as follows:

android/app/build.gradle

defaultConfig {
    // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
    applicationId "com.example.webview_in_flutter"
    minSdkVersion 20        // MODIFY
    targetSdkVersion 30
    versionCode flutterVersionCode.toInteger()
    versionName flutterVersionName
}

4. 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 content 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(
    const MaterialApp(
      home: 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 WebView(
        initialUrl: 'https://flutter.dev',
      ),
    );
  }
}

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.

Enabling Hybrid Composition

If you would like to use the Hybrid Composition mode for Android devices, you can do so with a couple of minor modifications. Modify your lib/main.dart as follows:

lib/main.dart

import 'dart:io';                            // Add this import. 
import 'package:flutter/material.dart';
import 'package:webview_flutter/webview_flutter.dart';

void main() {
  runApp(
    const MaterialApp(
      home: WebViewApp(),
    ),
  );
}

class WebViewApp extends StatefulWidget {
  const WebViewApp({super.key});

  @override
  State<WebViewApp> createState() => _WebViewAppState();
}

class _WebViewAppState extends State<WebViewApp> {
  // Add from here ...
  @override
  void initState() {
    if (Platform.isAndroid) {
      WebView.platform = SurfaceAndroidWebView(); 
    }
    super.initState();
  }
  // ... to here.

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Flutter WebView'),
      ),
      body: const WebView(
        initialUrl: 'https://flutter.dev',
      ),
    );
  }
}

Don't forget to change the minSdkVersion in the build.gradle to 19 when you want to use the Hybrid composition Platform View.

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;

  @override
  Widget build(BuildContext context) {
    return Stack(
      children: [
        WebView(
          initialUrl: 'https://flutter.dev',
          onPageStarted: (url) {
            setState(() {
              loadingPercentage = 0;
            });
          },
          onProgress: (progress) {
            setState(() {
              loadingPercentage = progress;
            });
          },
          onPageFinished: (url) {
            setState(() {
              loadingPercentage = 100;
            });
          },
        ),
        if (loadingPercentage < 100)
          LinearProgressIndicator(
            value: loadingPercentage / 100.0,
          ),
      ],
    );
  }
}

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:

import 'package:flutter/material.dart';
// Delete the package:webview_flutter/webview_flutter.dart import
import 'src/web_view_stack.dart';  // Add this import

void main() {
  runApp(
    const MaterialApp(
      home: WebViewApp(),
    ),
  );
}

class WebViewApp extends StatefulWidget {
  const WebViewApp({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(),   // Replace the WebView widget with 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 'dart:async';     // Add this import for Completer
import 'package:flutter/material.dart';
import 'package:webview_flutter/webview_flutter.dart';

class WebViewStack extends StatefulWidget {
  const WebViewStack({required this.controller, super.key}); // Modify 

  final Completer<WebViewController> controller;   // Add this attribute

  @override
  State<WebViewStack> createState() => _WebViewStackState();
}

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

  @override
  Widget build(BuildContext context) {
    return Stack(
      children: [
        WebView(
          initialUrl: 'https://flutter.dev',
          // Add from here ...
          onWebViewCreated: (webViewController) {
            widget.controller.complete(webViewController);
          },
          // ... to here.
          onPageStarted: (url) {
            setState(() {
              loadingPercentage = 0;
            });
          },
          onProgress: (progress) {
            setState(() {
              loadingPercentage = progress;
            });
          },
          onPageFinished: (url) {
            setState(() {
              loadingPercentage = 100;
            });
          },
        ),
        if (loadingPercentage < 100)
          LinearProgressIndicator(
            value: loadingPercentage / 100.0,
          ),
      ],
    );
  }
}

The WebViewStack widget now publishes the controller that is created asynchronously using the Completer<WebViewController>. This is a lighter weight alternative to creating a callback function argument to provide the controller to the rest of the app.

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 'dart:async';
import 'package:flutter/material.dart';
import 'package:webview_flutter/webview_flutter.dart';

class NavigationControls extends StatelessWidget {
  const NavigationControls({required this.controller, super.key});

  final Completer<WebViewController> controller;

  @override
  Widget build(BuildContext context) {
    return FutureBuilder<WebViewController>(
      future: controller.future,
      builder: (context, snapshot) {
        final WebViewController? controller = snapshot.data;
        if (snapshot.connectionState != ConnectionState.done ||
            controller == null) {
          return Row(
            children: const <Widget>[
              Icon(Icons.arrow_back_ios),
              Icon(Icons.arrow_forward_ios),
              Icon(Icons.replay),
            ],
          );
        }

        return Row(
          children: <Widget>[
            IconButton(
              icon: const Icon(Icons.arrow_back_ios),
              onPressed: () async {
                if (await controller.canGoBack()) {
                  await controller.goBack();
                } else {
                  ScaffoldMessenger.of(context).showSnackBar(
                    const SnackBar(content: Text('No back history item')),
                  );
                  return;
                }
              },
            ),
            IconButton(
              icon: const Icon(Icons.arrow_forward_ios),
              onPressed: () async {
                if (await controller.canGoForward()) {
                  await controller.goForward();
                } else {
                  ScaffoldMessenger.of(context).showSnackBar(
                    const SnackBar(content: Text('No forward history item')),
                  );
                  return;
                }
              },
            ),
            IconButton(
              icon: const Icon(Icons.replay),
              onPressed: () {
                controller.reload();
              },
            ),
          ],
        );
      },
    );
  }
}

This widget uses a FutureBuilder<T> widget to appropriately repaint itself when the controller becomes available. While waiting for the controller to become available, a row of three icons is rendered, but once the controller appears, it is replaced with a Row of IconButtons with onPressed handlers that use the controller to implement their functionality.

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. Previously, you have seen how to use the Completer<T>, but not where it was created. 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 'dart:async';                                    // Add this import

import 'package:flutter/material.dart';
import 'package:webview_flutter/webview_flutter.dart';  // Add this import back

import 'src/navigation_controls.dart';                  // Add this import
import 'src/web_view_stack.dart';

void main() {
  runApp(
    const MaterialApp(
      home: WebViewApp(),
    ),
  );
}

class WebViewApp extends StatefulWidget {
  const WebViewApp({super.key});

  @override
  State<WebViewApp> createState() => _WebViewAppState();
}

class _WebViewAppState extends State<WebViewApp> {
  final controller = Completer<WebViewController>();    // Instantiate the controller

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Flutter WebView'),
        // Add from here ...
        actions: [
          NavigationControls(controller: controller),
        ],
        // ... to here.
      ),
      body: WebViewStack(controller: controller),       // Add the controller argument
    );
  }
}

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 'dart:async';
import 'package:flutter/material.dart';
import 'package:webview_flutter/webview_flutter.dart';

class WebViewStack extends StatefulWidget {
  const WebViewStack({required this.controller, super.key});

  final Completer<WebViewController> controller;

  @override
  State<WebViewStack> createState() => _WebViewStackState();
}

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

  @override
  Widget build(BuildContext context) {
    return Stack(
      children: [
        WebView(
          initialUrl: 'https://flutter.dev',
          onWebViewCreated: (webViewController) {
            widget.controller.complete(webViewController);
          },
          onPageStarted: (url) {
            setState(() {
              loadingPercentage = 0;
            });
          },
          onProgress: (progress) {
            setState(() {
              loadingPercentage = progress;
            });
          },
          onPageFinished: (url) {
            setState(() {
              loadingPercentage = 100;
            });
          },
          // Add from here ...
          navigationDelegate: (navigation) {
            final host = Uri.parse(navigation.url).host;
            if (host.contains('youtube.com')) {
              ScaffoldMessenger.of(context).showSnackBar(
                SnackBar(
                  content: Text(
                    'Blocking navigation to $host',
                  ),
                ),
              );
              return NavigationDecision.prevent;
            }
            return NavigationDecision.navigate;
          },
          // ... to here.
        ),
        if (loadingPercentage < 100)
          LinearProgressIndicator(
            value: loadingPercentage / 100.0,
          ),
      ],
    );
  }
}

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 'dart:async';
import 'package:flutter/material.dart';
import 'package:webview_flutter/webview_flutter.dart';

enum _MenuOptions {
  navigationDelegate,
}

class Menu extends StatelessWidget {
  const Menu({required this.controller, super.key});

  final Completer<WebViewController> controller;

  @override
  Widget build(BuildContext context) {
    return FutureBuilder<WebViewController>(
      future: controller.future,
      builder: (context, controller) {
        return PopupMenuButton<_MenuOptions>(
          onSelected: (value) async {
            switch (value) {
              case _MenuOptions.navigationDelegate:
                await controller.data!.loadUrl('https://youtube.com');
                break;
            }
          },
          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 loadUrl 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 'dart:async';

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

import 'src/menu.dart';                                // Add this import
import 'src/navigation_controls.dart';
import 'src/web_view_stack.dart';

void main() {
  runApp(
    const MaterialApp(
      home: WebViewApp(),
    ),
  );
}

class WebViewApp extends StatefulWidget {
  const WebViewApp({super.key});

  @override
  State<WebViewApp> createState() => _WebViewAppState();
}

class _WebViewAppState extends State<WebViewApp> {
  final controller = Completer<WebViewController>();

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Flutter WebView'),
        actions: [
          NavigationControls(controller: controller),
          Menu(controller: controller),                // Add this line
        ],
      ),
      body: WebViewStack(controller: controller),
    );
  }
}

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 WebView widget 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
  Widget build(BuildContext context) {
    return Stack(
      children: [
        WebView(
          initialUrl: 'https://flutter.dev',
          onWebViewCreated: (webViewController) {
            widget.controller.complete(webViewController);
          },
          onPageStarted: (url) {
            setState(() {
              loadingPercentage = 0;
            });
          },
          onProgress: (progress) {
            setState(() {
              loadingPercentage = progress;
            });
          },
          onPageFinished: (url) {
            setState(() {
              loadingPercentage = 100;
            });
          },
          navigationDelegate: (navigation) {
            final host = Uri.parse(navigation.url).host;
            if (host.contains('youtube.com')) {
              ScaffoldMessenger.of(context).showSnackBar(
                SnackBar(
                  content: Text(
                    'Blocking navigation to $host',
                  ),
                ),
              );
              return NavigationDecision.prevent;
            }
            return NavigationDecision.navigate;
          },
          javascriptMode: JavascriptMode.unrestricted,        // Add this line
        ),
        if (loadingPercentage < 100)
          LinearProgressIndicator(
            value: loadingPercentage / 100.0,
          ),
      ],
    );
  }
}

Now that the WebView 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 as follows:

lib/src/menu.dart

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

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

  final Completer<WebViewController> controller;

  @override
  State<Menu> createState() => _MenuState();
}

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

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

JavascriptChannels enable your app to register callback handlers in the WebView'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 Completer<WebViewController> controller;

  @override
  State<WebViewStack> createState() => _WebViewStackState();
}

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

  @override
  Widget build(BuildContext context) {
    return Stack(
      children: [
        WebView(
          initialUrl: 'https://flutter.dev',
          onWebViewCreated: (webViewController) {
            widget.controller.complete(webViewController);
          },
          onPageStarted: (url) {
            setState(() {
              loadingPercentage = 0;
            });
          },
          onProgress: (progress) {
            setState(() {
              loadingPercentage = progress;
            });
          },
          onPageFinished: (url) {
            setState(() {
              loadingPercentage = 100;
            });
          },
          navigationDelegate: (navigation) {
            final host = Uri.parse(navigation.url).host;
            if (host.contains('youtube.com')) {
              ScaffoldMessenger.of(context).showSnackBar(
                SnackBar(
                  content: Text(
                    'Blocking navigation to $host',
                  ),
                ),
              );
              return NavigationDecision.prevent;
            }
            return NavigationDecision.navigate;
          },
          javascriptMode: JavascriptMode.unrestricted,
          javascriptChannels: _createJavascriptChannels(context),  // Add this line
        ),
        if (loadingPercentage < 100)
          LinearProgressIndicator(
            value: loadingPercentage / 100.0,
          ),
      ],
    );
  }

  // Add from here ...
  Set<JavascriptChannel> _createJavascriptChannels(BuildContext context) {
    return {
      JavascriptChannel(
        name: 'SnackBar',
        onMessageReceived: (message) {
          ScaffoldMessenger.of(context)
              .showSnackBar(SnackBar(content: Text(message.message)));
        },
      ),
    };
  }
  // ... to here.
}

For each JavascriptChannel in the Set, a channel object is made available in the JavaScript context as a window property named with the same name as the JavascriptChannel.name. Using this from the JavaScript context involves calling postMessage on the JavascriptChannel to send a message that is passed to the named JavascriptChannel's onMessageReceived callback handler.

To make use of the JavascriptChannel added above, add another menu item that executes an XMLHttpRequest in the JavaScript context and passes back the results using the SnackBar JavascriptChannel.

Now that the WebView knows about our JavascriptChannels,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 line
}

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

  final Completer<WebViewController> controller;

  @override
  State<Menu> createState() => _MenuState();
}

class _MenuState extends State<Menu> {
  @override
  Widget build(BuildContext context) {
    return FutureBuilder<WebViewController>(
      future: widget.controller.future,
      builder: (context, controller) {
        return PopupMenuButton<_MenuOptions>(
          onSelected: (value) async {
            switch (value) {
              case _MenuOptions.navigationDelegate:
                await controller.data!.loadUrl('https://youtube.com');
                break;
              case _MenuOptions.userAgent:
                final userAgent = await controller.data!
                    .runJavascriptReturningResult('navigator.userAgent');
                if (!mounted) return;
                ScaffoldMessenger.of(context).showSnackBar(SnackBar(
                  content: Text(userAgent),
                ));
                break;
              // Add from here ...
              case _MenuOptions.javascriptChannel:
                await controller.data!.runJavascript('''
var req = new XMLHttpRequest();
req.open('GET', "https://api.ipify.org/?format=json");
req.onload = function() {
  if (req.status == 200) {
    let response = JSON.parse(req.responseText);
    SnackBar.postMessage("IP Address: " + response.ip);
  } else {
    SnackBar.postMessage("Error: " + req.status);
  }
}
req.send();''');
                break;
              // ... to here.
            }
          },
          itemBuilder: (context) => [
            const PopupMenuItem<_MenuOptions>(
              value: _MenuOptions.navigationDelegate,
              child: Text('Navigate to YouTube'),
            ),
            const PopupMenuItem<_MenuOptions>(
              value: _MenuOptions.userAgent,
              child: Text('Show user-agent'),
            ),
            // Add from here ...
            const PopupMenuItem<_MenuOptions>(
              value: _MenuOptions.javascriptChannel,
              child: Text('Lookup IP Address'),
            ),
            // ... to here.
          ],
        );
      },
    );
  }
}

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 Completer<WebViewController> controller;

  @override
  State<Menu> createState() => _MenuState();
}

class _MenuState extends State<Menu> {
  final CookieManager cookieManager = CookieManager();       // 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');
  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),
    ),
  );
}

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

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

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 cookieManager = CookieManager();

  @override
  Widget build(BuildContext context) {
    return FutureBuilder<WebViewController>(
      future: widget.controller.future,
      builder: (context, controller) {
        return PopupMenuButton<_MenuOptions>(
          onSelected: (value) async {
            switch (value) {
              case _MenuOptions.navigationDelegate:
                await controller.data!.loadUrl('https://youtube.com');
                break;
              case _MenuOptions.userAgent:
                final userAgent = await controller.data!
                    .runJavascriptReturningResult('navigator.userAgent');
                if (!mounted) return;
                ScaffoldMessenger.of(context).showSnackBar(SnackBar(
                  content: Text(userAgent),
                ));
                break;
              case _MenuOptions.javascriptChannel:
                await controller.data!.runJavascript('''
var req = new XMLHttpRequest();
req.open('GET', "https://api.ipify.org/?format=json");
req.onload = function() {
  if (req.status == 200) {
    let response = JSON.parse(req.responseText);
    SnackBar.postMessage("IP Address: " + response.ip);
  } else {
    SnackBar.postMessage("Error: " + req.status);
  }
}
req.send();''');
                break;
              // Add from here ...
              case _MenuOptions.clearCookies:
                await _onClearCookies();
                break;
              case _MenuOptions.listCookies:
                await _onListCookies(controller.data!);
                break;
              case _MenuOptions.addCookie:
                await _onAddCookie(controller.data!);
                break;
              case _MenuOptions.setCookie:
                await _onSetCookie(controller.data!);
                break;
              case _MenuOptions.removeCookie:
                await _onRemoveCookie(controller.data!);
                break;
              // ... to here.
            }
          },
          itemBuilder: (context) => [
            const PopupMenuItem<_MenuOptions>(
              value: _MenuOptions.navigationDelegate,
              child: Text('Navigate to YouTube'),
            ),
            const PopupMenuItem<_MenuOptions>(
              value: _MenuOptions.userAgent,
              child: Text('Show user-agent'),
            ),
            const PopupMenuItem<_MenuOptions>(
              value: _MenuOptions.javascriptChannel,
              child: Text('Lookup IP Address'),
            ),
            // Add from here ...
            const PopupMenuItem<_MenuOptions>(
              value: _MenuOptions.clearCookies,
              child: Text('Clear cookies'),
            ),
            const PopupMenuItem<_MenuOptions>(
              value: _MenuOptions.listCookies,
              child: Text('List cookies'),
            ),
            const PopupMenuItem<_MenuOptions>(
              value: _MenuOptions.addCookie,
              child: Text('Add cookie'),
            ),
            const PopupMenuItem<_MenuOptions>(
              value: _MenuOptions.setCookie,
              child: Text('Set cookie'),
            ),
            const PopupMenuItem<_MenuOptions>(
              value: _MenuOptions.removeCookie,
              child: Text('Remove cookie'),
            ),
            // ... to here.
          ],
        );
      },
    );
  }

Exercising the CookieManager

To use all the functionality you have just added to the app, try the following steps:

  1. Select List cookies. It should list the Google Analytics cookies set by flutter.dev.
  2. Select Clear cookies. It should report that the cookies were indeed cleared.
  3. Select Clear cookies again. It should report that no cookies were available to clear.
  4. Select List cookies. It should report that there are no cookies.
  5. Select Add cookie. It should report the cookie as added.
  6. Select Set cookie. It should report the cookie as set.
  7. 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
Resolving dependencies...
  async 2.8.2 (2.9.0 available)
  characters 1.2.0 (1.2.1 available)
  clock 1.1.0 (1.1.1 available)
  fake_async 1.3.0 (1.3.1 available)
+ ffi 2.0.1
+ file 6.1.2
  matcher 0.12.11 (0.12.12 available)
  material_color_utilities 0.1.4 (0.1.5 available)
  meta 1.7.0 (1.8.0 available)
  path 1.8.1 (1.8.2 available)
+ path_provider 2.0.11
+ path_provider_android 2.0.16
+ path_provider_ios 2.0.10
+ path_provider_linux 2.1.7
+ path_provider_macos 2.0.6
+ path_provider_platform_interface 2.0.4
+ path_provider_windows 2.1.0
+ platform 3.1.0
+ process 4.2.4
  source_span 1.8.2 (1.9.0 available)
  string_scanner 1.1.0 (1.1.1 available)
  term_glyph 1.2.0 (1.2.1 available)
  test_api 0.4.9 (0.4.12 available)
+ win32 2.7.0
+ xdg_directories 0.2.0+1
Changed 13 dependencies!

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

  1. Create a new Directory with the name assets in the root folder of your project.
  2. Create a new Directory with the name www in the assets folder.
  3. Create a new Directory with the name styles in the www folder.
  4. Create a new File with the name index.html in the www folder.
  5. Create a new File with the name style.css in the styles folder.

Copy and paste the following code in the index.html file:

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>

For the style.css use the following few lines to set the HTML header style:

assets/www/styles/style.css

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.

lib/src/menu.dart

import 'dart:async';
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:

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

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:

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.

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 cookieManager = CookieManager();

  @override
  Widget build(BuildContext context) {
    return FutureBuilder<WebViewController>(
      future: widget.controller.future,
      builder: (context, controller) {
        return PopupMenuButton<_MenuOptions>(
          onSelected: (value) async {
            switch (value) {
              case _MenuOptions.navigationDelegate:
                await controller.data!.loadUrl('https://youtube.com');
                break;
              case _MenuOptions.userAgent:
                final userAgent = await controller.data!
                    .runJavascriptReturningResult('navigator.userAgent');
                if (!mounted) return;
                ScaffoldMessenger.of(context).showSnackBar(SnackBar(
                  content: Text(userAgent),
                ));
                break;
              case _MenuOptions.javascriptChannel:
                await controller.data!.runJavascript('''
var req = new XMLHttpRequest();
req.open('GET', "https://api.ipify.org/?format=json");
req.onload = function() {
 if (req.status == 200) {
   let response = JSON.parse(req.responseText);
   SnackBar.postMessage("IP Address: " + response.ip);
 } else {
   SnackBar.postMessage("Error: " + req.status);
 }
}
req.send();''');
                break;
              case _MenuOptions.clearCookies:
                await _onClearCookies();
                break;
              case _MenuOptions.listCookies:
                await _onListCookies(controller.data!);
                break;
              case _MenuOptions.addCookie:
                await _onAddCookie(controller.data!);
                break;
              case _MenuOptions.setCookie:
                await _onSetCookie(controller.data!);
                break;
              case _MenuOptions.removeCookie:
                await _onRemoveCookie(controller.data!);
                break;
              // Add from here ...
              case _MenuOptions.loadFlutterAsset:
                await _onLoadFlutterAssetExample(controller.data!, context);
                break;
              case _MenuOptions.loadLocalFile:
                await _onLoadLocalFileExample(controller.data!, context);
                break;
              case _MenuOptions.loadHtmlString:
                await _onLoadHtmlStringExample(controller.data!, context);
                break;
              // ... to here.
            }
          },
          itemBuilder: (context) => [
            const PopupMenuItem<_MenuOptions>(
              value: _MenuOptions.navigationDelegate,
              child: Text('Navigate to YouTube'),
            ),
            const PopupMenuItem<_MenuOptions>(
              value: _MenuOptions.userAgent,
              child: Text('Show user-agent'),
            ),
            const PopupMenuItem<_MenuOptions>(
              value: _MenuOptions.javascriptChannel,
              child: Text('Lookup IP Address'),
            ),
            const PopupMenuItem<_MenuOptions>(
              value: _MenuOptions.clearCookies,
              child: Text('Clear cookies'),
            ),
            const PopupMenuItem<_MenuOptions>(
              value: _MenuOptions.listCookies,
              child: Text('List cookies'),
            ),
            const PopupMenuItem<_MenuOptions>(
              value: _MenuOptions.addCookie,
              child: Text('Add cookie'),
            ),
            const PopupMenuItem<_MenuOptions>(
              value: _MenuOptions.setCookie,
              child: Text('Set cookie'),
            ),
            const PopupMenuItem<_MenuOptions>(
              value: _MenuOptions.removeCookie,
              child: Text('Remove cookie'),
            ),
            // Add from here ...
            const PopupMenuItem<_MenuOptions>(
              value: _MenuOptions.loadFlutterAsset,
              child: Text('Load Flutter Asset'),
            ),
            const PopupMenuItem<_MenuOptions>(
              value: _MenuOptions.loadHtmlString,
              child: Text('Load HTML string'),
            ),
            const PopupMenuItem<_MenuOptions>(
              value: _MenuOptions.loadLocalFile,
              child: Text('Load local file'),
            ),
            // ... to here.
          ],
        );
      },
    );
  }

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.