Ajouter WebView à votre application Flutter

1. Présentation

Dernière mise à jour : 19/10/2021

Le plug-in WebView de Flutter permet d'ajouter un widget WebView à votre application Flutter pour Android ou iOS. Sur iOS, le widget WebView repose sur WKWebView, tandis que sur Android, le widget WebView s'appuie sur une instance WebView. Le plug-in peut afficher des widgets Flutter par-dessus la vue Web. Par exemple, il est possible d'afficher un menu déroulant par-dessus la vue Web.

Ce que vous allez faire

Dans cet atelier de programmation par étapes, vous allez créer une application mobile intégrant une instance WebView avec le SDK Flutter. Cette appli pourra :

  • Afficher le contenu Web dans WebView
  • Afficher les widgets Flutter empilés sur WebView
  • Réagir aux événements de progression du chargement des pages
  • Contrôler WebView via WebViewController
  • Bloquer les sites Web à l'aide de NavigationDelegate
  • Évaluer des expressions JavaScript
  • Gérer les rappels à partir de JavaScript avec JavascriptChannels
  • Définir, supprimer, ajouter ou afficher des cookies
  • Charger et afficher le code HTML à partir d'éléments, de fichiers ou de chaînes contenant du code HTML

Points abordés

Dans cet atelier de programmation, vous allez apprendre à utiliser le plug-in webview_flutter de différentes manières, y compris les suivantes :

  • Configuration du plug-in webview_flutter
  • Détection des événements de progression du chargement des pages
  • Contrôle de la navigation sur les pages
  • Contrôle de WebView pour revenir en arrière et avancer dans l'historique
  • Évaluation JavaScript, y compris à l'aide des résultats renvoyés
  • Enregistrement des rappels pour appeler le code Dart à partir de JavaScript
  • Gestion des cookies
  • Chargement et affichage des pages HTML à partir d'éléments, de fichiers ou de chaînes contenant du code HTML

Prérequis

2. Configurer votre environnement de développement Flutter

Pour cet atelier, vous avez besoin de deux logiciels : le SDK Flutter et un éditeur.

Vous pouvez exécuter l'atelier de programmation sur l'un des appareils suivants :

3. Premiers pas

Premiers pas avec Flutter

Vous pouvez créer un projet Flutter de plusieurs façons, en utilisant les outils fournis par Android Studio et Visual Studio Code pour cette tâche. Suivez les procédures associées pour créer un projet, ou exécutez les commandes suivantes dans un terminal de ligne de commande pratique.

$ flutter create --platforms=android,ios webview_in_flutter
Creating project webview_in_flutter...
Running "flutter pub get" in webview_in_flutter...               1,728ms
Wrote 73 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.

Ajouter le plug-in WebView de Flutter en tant que dépendance

Il est facile d'ajouter des fonctionnalités supplémentaires à une application Flutter à l'aide de packages Pub. Dans cet atelier de programmation, vous allez ajouter le plug-in webview_flutter à votre projet. Exécutez les commandes suivantes dans le terminal.

$ cd webview_in_flutter
$ flutter pub add webview_flutter

Si vous inspectez le fichier pubspec.yaml, vous constaterez qu'il comporte désormais une ligne dans la section des dépendances du plug-in webview_flutter.

Configurer la version minimale (minSDK) d'Android

Pour utiliser le plug-in webview_flutter sur Android, vous devez définir minSDK sur 20. Modifiez votre fichier android/app/build.gradle comme suit :

android/app/build.gradle

android {
    //...

    defaultConfig {
        applicationId "com.example.webview_in_flutter"
        minSdkVersion 20                           // MODIFY
        targetSdkVersion flutter.targetSdkVersion
        versionCode flutterVersionCode.toInteger()
        versionName flutterVersionName
    }

4. Ajouter le widget WebView à l'application Flutter

À cette étape, vous allez ajouter WebView à votre application. Les WebViews sont des vues natives hébergées, et en tant que développeur d'applications, vous pouvez choisir comment les héberger dans votre application. Sur Android, vous pouvez opter pour des affichages virtuels, actuellement définis par défaut pour Android et pour le mode mixte. Toutefois, iOS utilise toujours le mode mixte.

Pour une présentation détaillée des différences entre les écrans virtuels et le mode mixte, consultez la documentation sur l'hébergement des vues natives Android et iOS dans votre application Flutter avec les vues de la plate-forme.

Afficher une instance WebView à l'écran

Remplacez le contenu du bloc lib/main.dart par le code suivant :

lib/main.dart

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

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

Exécuter cette commande sur iOS ou Android affiche une instance WebView dans une fenêtre de navigateur à fond perdu sur votre appareil. Le navigateur s'affiche donc en plein écran sur votre appareil, sans aucune forme de bordure ni de marge. En faisant défiler la page, vous remarquerez peut-être que certaines parties de celle-ci peuvent sembler un peu bizarres. En effet, JavaScript est actuellement désactivé alors qu'il est nécessaire pour afficher flutter.dev correctement.

Exécuter l'application

Exécutez l'application Flutter sur iOS ou Android pour afficher une vue de la plate-forme qui comporte le site Web flutter.dev. Vous pouvez également exécuter l'application dans un émulateur Android ou un simulateur iOS. N'hésitez pas à remplacer l'URL WebView initiale par votre site Web, par exemple.

$ flutter run

Si vous avez installé le simulateur ou l'émulateur approprié ou si vous avez connecté un appareil physique, vous devriez obtenir ce qui suit après avoir compilé et déployé l'application :

5. Détecter les événements de chargement de pages

Grâce au widget WebView, votre application peut détecter plusieurs événements de progression du chargement de la page. Au cours du cycle de chargement de la page WebView, trois événements de chargement de page différents se déclenchent : onPageStarted, onProgress et onPageFinished. À cette étape, vous allez implémenter un indicateur de chargement de page. En bonus, vous pouvez afficher le contenu Flutter dans la zone de contenu WebView.

Ajouter des événements de chargement de pages à votre application

Créez un fichier source à l'adresse lib/src/web_view_stack.dart et renseignez-le comme suit :

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

Ce code a encapsulé le widget WebView dans Stack, superposant de manière conditionnelle WebView avec LinearProgressIndicator lorsque le pourcentage de chargement de la page est de moins de 100 %. Puisque cet état de programme change au fil du temps, vous l'avez stocké dans une classe State associée à StatefulWidget.

Pour utiliser ce nouveau widget WebViewStack, modifiez le fichier lib/main.dart comme suit :

lib/main.dart

import 'package:flutter/material.dart';

import 'src/web_view_stack.dart';

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

Lorsque vous exécutez l'application, suivant l'état de votre réseau et si le navigateur a mis en cache la page que vous consultez, un indicateur de chargement de page s'affiche en superposition sur la zone de contenu WebView.

6. Utiliser WebViewController

Accéder à WebViewController à partir du widget WebView

Le widget WebView permet un contrôle programmatique à l'aide de WebViewController. Ce contrôleur est mis à disposition après la construction du widget WebView via un rappel. Le caractère asynchrone de ce contrôleur en fait un candidat idéal pour la classe asynchrone Completer<T> de Dart.

Mettez à jour lib/src/web_view_stack.dart comme indiqué ci-dessous :

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

Le widget WebViewStack utilise maintenant un contrôleur créé dans le widget où il se trouve. Cela permet de partager facilement le contrôleur de WebViewWidget avec d'autres parties de l'application.

Créer des commandes de navigation

Disposer d'un WebView fonctionnel est une chose, mais il pourrait également être utile de pouvoir parcourir les pages ou l'historique, de revenir en arrière et d'actualiser la page. Heureusement, avec WebViewController, vous pouvez ajouter cette fonctionnalité à votre application.

Créez un fichier source à l'adresse lib/src/navigation_controls.dart et renseignez-le comme suit :

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

Ce widget utilise WebViewController, qui a été partagé avec lui lors de sa création pour permettre aux utilisateurs de contrôler WebView avec une série de IconButton.

Ajouter des commandes de navigation à AppBar

Il est temps d'assembler WebViewStack qui vient d'être mis à jour et NavigationControls qui vient d'être créé dans une nouvelle version de WebViewApp. C'est à cet endroit que nous avons créé l'instance WebViewController partagée. Avec WebViewApp dans la partie supérieure de l'arborescence des widgets de cette application, il est judicieux d'effectuer la création à ce niveau.

Mettez à jour le fichier lib/main.dart comme suit :

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

En exécutant l'application, vous devriez voir une page Web contenant les commandes :

7. Suivre la navigation avec la fonctionnalité NavigationDelegate

WebView fournit NavigationDelegate, à votre application pour lui permettre de suivre et de contrôler la navigation sur les pages du widget WebView. Quand une navigation est initiée par WebView,, par exemple lorsqu'un utilisateur clique sur un lien, NavigationDelegate est appelé. Le rappel NavigationDelegate peut être utilisé pour contrôler si WebView poursuit la navigation.

Enregistrer une version personnalisée de NavigationDelegate

À cette étape, vous allez enregistrer un rappel NavigationDelegate afin de bloquer la navigation vers YouTube.com. Notez que cette implémentation simplifiée bloque également le contenu YouTube intégré, qui apparaît dans différentes pages de documentation de l'API Flutter.

Mettez à jour lib/src/web_view_stack.dart comme suit :

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

À l'étape suivante, vous allez ajouter un élément de menu pour activer le test de votre NavigationDelegate à l'aide de la classe WebViewController. Il revient au lecteur de développer la logique du rappel afin de ne bloquer que la navigation en pleine page sur YouTube.com et de continuer à autoriser le contenu YouTube intégré dans la documentation de l'API.

8. Ajouter un bouton de menu à AppBar

Lors des prochaines étapes, vous allez créer un bouton de menu dans le widget AppBar, qui permet d'évaluer JavaScript, d'appeler des canaux JavaScript et de gérer les cookies. Dans l'ensemble, un menu bien utile.

Créez un fichier source à l'adresse lib/src/menu.dart et renseignez-le comme suit :

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

Quand l'utilisateur sélectionne l'option de menu Accéder à YouTube, la méthode loadRequest de WebViewController s'exécute. Cette navigation va être bloquée par le rappel navigationDelegate que vous avez créé à l'étape précédente.

Pour ajouter le menu à l'écran de WebViewApp, modifiez lib/main.dart comme suit :

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

Exécutez votre application et appuyez sur l'élément de menu Accéder à YouTube. Vous devriez recevoir un message SnackBar vous informant que la manette de navigation a bloqué la navigation sur YouTube.

9. Évaluer JavaScript

WebViewController peut évaluer des expressions JavaScript dans le contexte de la page actuelle. Pour évaluer JavaScript, vous avez le choix entre deux méthodes : pour le code JavaScript qui ne renvoie pas de valeur, utilisez runJavaScript et pour le code JavaScript renvoyant une valeur, utilisez runJavaScriptReturningResult.

Pour activer JavaScript, vous devez configurer WebViewController avec la propriété javaScriptMode définie sur JavascriptMode.unrestricted. Par défaut, javascriptMode est défini sur JavascriptMode.disabled.

Mettez à jour la classe _WebViewStackState en ajoutant le paramètre javascriptMode comme suit :

lib/src/web_view_stack.dart

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;
          },
        ),
      )
      ..setJavaScriptMode(JavaScriptMode.unrestricted);
  }

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

Maintenant que WebViewWidget peut exécuter JavaScript, vous pouvez ajouter une option au menu pour utiliser la méthode runJavaScriptReturningResult.

À l'aide de votre éditeur ou d'un raccourci clavier, convertissez la classe Menu en StatefulWidget. Modifiez lib/src/menu.dart comme suit :

lib/src/menu.dart

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

enum _MenuOptions {
  navigationDelegate,
  userAgent,
}

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'));
            break;
          case _MenuOptions.userAgent:
            final userAgent = await widget.controller
                .runJavaScriptReturningResult('navigator.userAgent');
            if (!mounted) return;
            ScaffoldMessenger.of(context).showSnackBar(SnackBar(
              content: Text('$userAgent'),
            ));
            break;
        }
      },
      itemBuilder: (context) => [
        const PopupMenuItem<_MenuOptions>(
          value: _MenuOptions.navigationDelegate,
          child: Text('Navigate to YouTube'),
        ),
        const PopupMenuItem<_MenuOptions>(
          value: _MenuOptions.userAgent,
          child: Text('Show user-agent'),
        ),
      ],
    );
  }
}

Lorsque vous appuyez sur l'option de menu "Afficher le user-agent", le résultat de l'exécution de l'expression JavaScript navigator.userAgent est affiché dans Snackbar. Lors de l'exécution de l'application, vous remarquerez peut-être que la page Flutter.dev s'affiche différemment. C'est le résultat de l'exécution avec JavaScript activé.

10. Utiliser les canaux JavaScript

Les canaux JavaScript permettent à votre application d'enregistrer des gestionnaires de rappel dans le contexte JavaScript de WebViewWidget, qui peuvent être appelés pour transmettre les valeurs au code Dart de l'application. À cette étape, vous allez enregistrer un canal SnackBar qui sera appelé avec le résultat de XMLHttpRequest.

Mettez à jour la classe WebViewStack comme suit :

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

Pour chaque canal JavaScript dans Set, un objet de canal est disponible dans le contexte JavaScript en tant que propriété de fenêtre portant le même nom que le canal JavaScript name. L'utilisation de ce code depuis le contexte JavaScript implique d'appeler postMessage sur le canal JavaScript pour envoyer un message transmis au gestionnaire de rappel onMessageReceived nommé JavascriptChannel.

Pour utiliser le canal JavaScript ajouté ci-dessous, spécifiez un autre élément de menu qui exécute XMLHttpRequest dans le contexte JavaScript et renvoie les résultats à l'aide du canal JavaScript SnackBar.

Maintenant que WebViewWidget connaît notre canal JavaScript, vous pouvez ajouter un exemple pour étendre l'application. Pour cela, ajoutez une autre instance de PopupMenuItem à la classe Menu, puis ajoutez les fonctionnalités supplémentaires.

Mettez à jour _MenuOptions avec l'option de menu supplémentaire, en ajoutant la valeur d'énumération javascriptChannel et en ajoutant une implémentation à la classe Menu comme suit :

lib/src/menu.dart

enum _MenuOptions {
  navigationDelegate,
  userAgent,
  javascriptChannel,
}

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'));
            break;
          case _MenuOptions.userAgent:
            final userAgent = await widget.controller
                .runJavaScriptReturningResult('navigator.userAgent');
            if (!mounted) return;
            ScaffoldMessenger.of(context).showSnackBar(SnackBar(
              content: Text('$userAgent'),
            ));
            break;
          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();''');
            break;
        }
      },
      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'),
        ),
      ],
    );
  }
}

Ce code JavaScript s'exécute lorsque l'utilisateur choisit l'option de menu Exemple de canal JavaScript.

var req = new XMLHttpRequest();
req.open('GET', "https://api.ipify.org/?format=json");
req.onload = function() {
  if (req.status == 200) {
    SnackBar.postMessage(req.responseText);
  } else {
    SnackBar.postMessage("Error: " + req.status);
  }
}
req.send();

Ce code envoie une requête GET à une API d'adresse IP publique, en renvoyant l'adresse IP de l'appareil. Ce résultat s'affiche dans SnackBar en appelant postMessage sur le SnackBar JavascriptChannel.

11. Gérer les cookies

Votre application peut gérer les cookies dans WebView avec la classe CookieManager. À cette étape, vous allez afficher une liste de cookies, l'effacer, supprimer les cookies et en définir de nouveaux. Ajoutez des entrées à _MenuOptions pour chacun des cas d'utilisation de cookies comme suit :

lib/src/menu.dart

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

Les autres modifications de cette étape sont axées sur la classe Menu, y compris la conversion de la classe Menu de "sans état" à "avec état". Cette modification est importante, car Menu doit posséder CookieManager, et l'état modifiable dans les widgets sans état est une mauvaise combinaison.

Ajoutez CookieManager à la classe State obtenue, comme suit :

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

La classe _MenuState contient le code précédemment ajouté dans la classe Menu, ainsi que la classe CookieManager que vous venez d'ajouter. Dans les sections suivantes, vous ajouterez des fonctions d'assistance à _MenuState, qui seront ensuite appelées par les éléments de menu à ajouter.

Obtenir la liste de tous les cookies

Vous allez utiliser JavaScript pour obtenir la liste de tous les cookies. Pour ce faire, ajoutez une méthode d'assistance à la fin de la classe _MenuState, appelée _onListCookies. À l'aide de la méthode runJavaScriptReturningResult, votre méthode d'assistance exécute document.cookie dans le contexte JavaScript et renvoie une liste de tous les cookies.

Ajoutez les éléments suivants à la classe _MenuState :

lib/src/menu.dart

Future<void> _onListCookies(WebViewController controller) async {
  final String cookies = await controller
      .runJavaScriptReturningResult('document.cookie') as String;
  if (!mounted) return;
  ScaffoldMessenger.of(context).showSnackBar(
    SnackBar(
      content: Text(cookies.isNotEmpty ? cookies : 'There are no cookies.'),
    ),
  );
}

Effacer tous les cookies

Pour effacer tous les cookies dans l'instance WebView, utilisez la méthode clearCookies de la classe CookieManager. La méthode renvoie Future<bool> qui renvoie vers true si CookieManager a effacé les cookies et false si aucun cookie n'a été supprimé.

Ajoutez les éléments suivants à la classe _MenuState :

lib/src/menu.dart

Future<void> _onClearCookies() async {
  final hadCookies = await cookieManager.clearCookies();
  String message = 'There were cookies. Now, they are gone!';
  if (!hadCookies) {
    message = 'There were no cookies to clear.';
  }
  if (!mounted) return;
  ScaffoldMessenger.of(context).showSnackBar(
    SnackBar(
      content: Text(message),
    ),
  );
}

Vous pouvez ajouter un cookie en appelant JavaScript. L'API qui permet d'ajouter un cookie à un document JavaScript est décrite en détail sur MDN.

Ajoutez les éléments suivants à la classe _MenuState :

lib/src/menu.dart

Future<void> _onAddCookie(WebViewController controller) async {
  await controller.runJavaScript('''var date = new Date();
  date.setTime(date.getTime()+(30*24*60*60*1000));
  document.cookie = "FirstName=John; expires=" + date.toGMTString();''');
  if (!mounted) return;
  ScaffoldMessenger.of(context).showSnackBar(
    const SnackBar(
      content: Text('Custom cookie added.'),
    ),
  );
}

Vous pouvez également définir les cookies à l'aide de CookieManager comme suit.

Ajoutez les éléments suivants à la classe _MenuState :

lib/src/menu.dart

Future<void> _onSetCookie(WebViewController controller) async {
  await cookieManager.setCookie(
    const WebViewCookie(name: 'foo', value: 'bar', domain: 'flutter.dev'),
  );
  if (!mounted) return;
  ScaffoldMessenger.of(context).showSnackBar(
    const SnackBar(
      content: Text('Custom cookie is set.'),
    ),
  );
}

Pour supprimer un cookie, vous devez ajouter un cookie avec une date d'expiration déjà passée.

Ajoutez les éléments suivants à la classe _MenuState :

lib/src/menu.dart

Future<void> _onRemoveCookie(WebViewController controller) async {
  await controller.runJavaScript(
      'document.cookie="FirstName=John; expires=Thu, 01 Jan 1970 00:00:00 UTC" ');
  if (!mounted) return;
  ScaffoldMessenger.of(context).showSnackBar(
    const SnackBar(
      content: Text('Custom cookie removed.'),
    ),
  );
}

Ajouter les éléments du menu CookieManager

Il vous suffit d'ajouter les options de menu et de les lier aux méthodes d'assistance que vous venez d'ajouter. Mettez à jour la classe _MenuState comme suit :

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

Exécuter CookieManager

Pour utiliser toutes les fonctionnalités que vous venez d'ajouter à l'application, procédez comme suit :

  1. Sélectionnez Répertorier les cookies. Cette option doit répertorier les cookies Google Analytics définis par flutter.dev.
  2. Sélectionnez Effacer les cookies. Cette option doit indiquer que les cookies ont bien été supprimés.
  3. Sélectionnez à nouveau Effacer les cookies. Cette option doit indiquer qu'il n'y a aucun cookie à supprimer.
  4. Sélectionnez Répertorier les cookies. Cette option doit indiquer qu'il n'y a pas de cookie.
  5. Sélectionnez Ajouter un cookie. Cette option doit indiquer que le cookie a été ajouté.
  6. Sélectionnez Définir des cookies. Cette option doit indiquer que le cookie a été défini.
  7. Sélectionnez Répertorier les cookies, puis, en dernier lieu, sélectionnez Supprimer les cookies.

12. Charger des éléments, des fichiers et des chaînes HTML Flutter dans WebView

Votre application peut charger des fichiers HTML à l'aide de différentes méthodes et les afficher dans l'instance WebView. À cette étape, vous allez charger un élément Flutter spécifié dans le fichier pubspec.yaml, charger un fichier situé au chemin spécifié et charger une page à l'aide d'une chaîne HTML.

Si vous souhaitez charger un fichier situé à un chemin spécifié, vous devez ajouter path_provider à pubspec.yaml. Il s'agit d'un plug-in Flutter permettant de trouver des emplacements couramment utilisés dans le système de fichiers.

Dans la ligne de commande, exécutez la commande suivante :

$ flutter pub add path_provider

Pour charger l'élément, vous devez spécifier son chemin dans pubspec.yaml. Dans pubspec.yaml, ajoutez les lignes suivantes :

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.

Pour ajouter les éléments à votre projet procédez comme suit :

  1. Créez un répertoire nommé assets dans le dossier racine de votre projet.
  2. Créez un répertoire nommé www dans le dossier assets.
  3. Créez un répertoire nommé styles dans le dossier www.
  4. Créez un fichier nommé index.html dans le dossier www.
  5. Créez un fichier nommé style.css dans le dossier styles.

Copiez et collez le code suivant dans le fichier index.html :

assets/www/index.html

<!DOCTYPE html>
<!-- Copyright 2013 The Flutter Authors. All rights reserved.
Use of this source code is governed by a BSD-style license that can be
found in the LICENSE file. -->
<html lang="en">
<head>
<title>Load file or HTML string example</title>
<link rel="stylesheet" href="styles/style.css" />
</head>
<body>

<h1>Local demo page</h1>
<p>
 This is an example page used to demonstrate how to load a local file or HTML
 string using the <a href="https://pub.dev/packages/webview_flutter">Flutter
 webview</a> plugin.
</p>

</body>
</html>

Pour définir le style d'en-tête HTML, utilisez le code suivant pour les styles.css :

assets/www/styles/style.css

h1 {
   color: blue;
}

Maintenant que les éléments sont définis et prêts à être utilisés, vous pouvez mettre en œuvre les méthodes nécessaires pour charger et afficher les éléments, les fichiers ou les chaînes HTML Flutter.

Charger l'élément Flutter

Pour charger l'élément que vous venez de créer, il vous suffit d'appeler la méthode loadFlutterAsset à l'aide de WebViewController et d'indiquer le chemin d'accès à l'élément en tant que paramètre. Ajoutez la méthode suivante à la fin de votre code :

lib/src/menu.dart

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

Charger un fichier local

Pour charger un fichier sur votre appareil, vous pouvez ajouter une méthode qui utilisera la méthode loadFile, à nouveau à l'aide de WebViewController qui redirige String contenant le chemin d'accès vers le fichier.

Vous devez d'abord créer un fichier contenant le code HTML. Pour ce faire, vous pouvez simplement ajouter le code HTML en tant que chaîne en haut de votre code dans le fichier menu.dart, juste en dessous des importations.

lib/src/menu.dart

import 'dart:io';                                   // Add this line,
import 'package:flutter/material.dart';
import 'package:path_provider/path_provider.dart';  // And this one.
import 'package:webview_flutter/webview_flutter.dart';

// Add from here ...
const String kExamplePage = '''
<!DOCTYPE html>
<html lang="en">
<head>
<title>Load file or HTML string example</title>
</head>
<body>

<h1>Local demo page</h1>
<p>
 This is an example page used to demonstrate how to load a local file or HTML
 string using the <a href="https://pub.dev/packages/webview_flutter">Flutter
 webview</a> plugin.
</p>

</body>
</html>
''';
// ... to here.

Pour créer File et écrire la chaîne HTML dans le fichier, vous allez ajouter deux méthodes. _onLoadLocalFileExample charge le fichier en fournissant le chemin d'accès sous forme de chaîne, qui est renvoyée par la méthode _prepareLocalFile(). Ajoutez les méthodes suivantes à votre 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;
}

Charger une chaîne HTML

Il est plutôt simple d'afficher une page en fournissant une chaîne HTML. WebViewController utilise une méthode appelée loadHtmlString, qui vous permet d'utiliser la chaîne HTML comme argument. WebView affiche alors la page HTML fournie. Ajoutez la méthode suivante à votre 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.

Ajouter les éléments du menu

Maintenant que les éléments sont définis et prêts à être utilisés, et que les méthodes comprenant toutes les fonctionnalités ont été créées, le menu peut être mis à jour. Ajoutez les entrées suivantes à l'énumération _MenuOptions :

lib/src/menu.dart

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

Maintenant que l'énumération est mise à jour, vous pouvez ajouter les options de menu et les associer aux méthodes d'assistance que vous venez d'ajouter. Mettez à jour la classe _MenuState comme suit :

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

Tester les éléments, le fichier et la chaîne HTML

Pour vérifier si le code précédemment mis en œuvre fonctionne correctement, vous pouvez l'exécuter sur votre appareil et cliquer sur l'un des éléments de menu nouvellement ajouté. Vous remarquerez que _onLoadFlutterAssetExample utilise style.css que nous avons ajouté pour remplacer l'en-tête du fichier HTML par la couleur bleue.

13. Terminé !

Félicitations ! Vous avez terminé l'atelier de programmation. Vous trouverez le code final de cet atelier de programmation dans le dossier atelier de programmation.

Pour aller plus loin, suivez les autres ateliers de programmation Flutter.