Apprenez à convertir une PWA en une application Flutter à l’aide du plug-in InAppWebView 6 de Flutter
Dans cet article, nous allons convertir un PWA (Application Web Progressive) dans une application mobile Flutter pour Android et iOS en utilisant la dernière version 6 du flutter_inappwebview
brancher.
Progressive Web App est un terme qui fait référence aux applications Web qui sont développées et chargées comme des pages Web ordinaires, mais qui se comportent de la même manière que les applications natives lorsqu’elles sont utilisées sur un appareil mobile.
Ils sont construits et améliorés avec des API modernes pour offrir des capacités, une fiabilité et une installabilité améliorées tout en atteignant n’importe qui, n’importe où, sur n’importe quel appareil avec une seule base de code. Les applications Web progressives exploitent ce dynamisme du nouveau Web ainsi que des technologies telles que les service workers et les manifestes pour offrir une expérience utilisateur de type application native qui fonctionne même lorsque l’utilisateur est hors ligne.
Les développeurs peuvent publier l’application Web en ligne, s’assurer qu’elle répond aux exigences d’installation de base et les utilisateurs peuvent ajouter l’application à leur écran d’accueil. La publication de l’application sur des systèmes de distribution numérique comme Apple App Store ou Google Play est facultative.
Les applications hybrides sont des applications qui combinent les fonctionnalités des applications natives et des applications Web. Ils courent à l’intérieur d’un conteneur, dans ce cas, un WebView
.
Ils sont disponibles via les magasins d’applications, peuvent accéder aux API natives et aux composants matériels de votre téléphone, et sont installés sur votre appareil, tout comme une application native.
Je n’expliquerai pas les avantages et les inconvénients entre les PWA, les applications natives et les applications hybrides, car cela dépasse le cadre de cet article. Vous pouvez déjà le rechercher sur le Web.
Comme exemple de PWA, nous utiliserons (dépôt GitHub : js13kpwa), qui est une PWA entièrement fonctionnelle avec prise en charge hors ligne.
js13kpwa est une liste d’entrées A-Frame soumises au concours js13kGames 2017, utilisée comme exemple pour les articles MDN sur les applications Web progressives. Le js13kPWA a la structure du shell de l’application, fonctionne hors ligne avec le service worker, est installable grâce au fichier manifeste et à la fonction Ajouter à l’écran d’accueil, et peut être réactivé à l’aide des notifications et du push.
De plus, pour ce cas d’utilisation, nous ajouterons une simple communication bidirectionnelle entre JavaScript et Flutter/Dart.
Travailleurs des services
Les travailleurs des services sont un élément fondamental d’une PWA. Ils permettent un chargement rapide (quel que soit le réseau), un accès hors ligne, des notifications push et d’autres fonctionnalités.
Vérifier pour la disponibilité de l’API JavaScript Service Worker en fonction de la version de WebView/Browser.
Les Service Workers sont disponibles sur Android à partir de « Android 5–6.x WebView : Chromium 107 » et sur iOS à partir de iOS 14.0+.
Sur iOS, l’activation de l’API Service Worker nécessite une configuration supplémentaire à l’aide de domaines liés à l’application (lisez le WebKit — Domaines liés aux applications article pour plus de détails).
La fonctionnalité App-Bound Domains prend des mesures pour préserver la confidentialité des utilisateurs en limitant les domaines sur lesquels une application peut utiliser des API puissantes pour suivre les utilisateurs lors de la navigation dans l’application.
Vous pouvez spécifier jusqu’à dix domaines « liés à l’application » à l’aide de la Info.plist
clé WKAppBoundDomains
.
Nous devons donc y ajouter le domaine de notre PWA. Sinon, l’API Service Worker ne fonctionnera pas. Pour notre cas d’utilisation, nous devons ajouter le mdn.github.io
domaine. Voici un exemple de ios/Runner/Info.plist
dossier:
<dict>
<!-- ... -->
<key>WKAppBoundDomains</key>
<array>
<string>mdn.github.io</string>
</array>
<!-- ... -->
</dict>
Détection de réseau Internet
Détecter si le téléphone mobile de l’utilisateur est connecté à Internet est important pour WebView
pour charger la PWA depuis le cache au lieu de demander les ressources en ligne.
Pour vérifier s’il existe une connexion valide, c’est-à-dire un réseau cellulaire ou Wi-Fi, nous utiliserons le connectivity_plus
brancher. Au lieu de cela, pour tester si le réseau est connecté à Internet, nous pouvons essayer de rechercher l’adresse d’un hôte, tel que https://exemple.com/.
Voici la détection complète du code :
Future<bool> isNetworkAvailable() async {
// check if there is a valid network connection
final connectivityResult = await Connectivity().checkConnectivity();
if (connectivityResult != ConnectivityResult.mobile &&
connectivityResult != ConnectivityResult.wifi) {
return false;
}// check if the network is really connected to Internet
try {
final result = await InternetAddress.lookup('example.com');
if (result.isEmpty || result[0].rawAddress.isEmpty) {
return false;
}
} on SocketException catch (_) {
return false;
}
return true;
}
Paramètres de base InAppWebView
Pour faire le InAppWebView
fonctionnent correctement, nous devons définir quelques paramètres de base :
InAppWebViewSettings(
// enable opening windows support
supportMultipleWindows: true,
javaScriptCanOpenWindowsAutomatically: true,// useful for identifying traffic, e.g. in Google Analytics.
applicationNameForUserAgent: 'My PWA App Name',
// Override the User Agent, otherwise some external APIs, such as Google and Facebook logins, will not work
// because they recognize the default WebView User Agent.
userAgent:
'Mozilla/5.0 (Linux; Android 13) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/107.0.5304.105 Mobile Safari/537.36',
disableDefaultErrorPage: true,
// enable iOS service worker feature limited to defined App Bound Domains
limitsNavigationsToAppBoundDomains: true
);
Modifiez-le en fonction de vos besoins.
Dans cet exemple, nous activons la prise en charge de plusieurs fenêtres au cas où nous voudrions ouvrir une fenêtre contextuelle WebView
les fenêtres.
Dans certains cas, vous devrez peut-être également remplacer l’agent utilisateur par une valeur différente de celle par défaut pour pouvoir utiliser certaines API externes, telles que les connexions Google et Facebook. Sinon, ils ne fonctionneront pas car ils reconnaissent et bloquent la valeur par défaut WebView
agent utilisateur.
De plus, vous devez définir le limitsNavigationsToAppBoundDomains
mise à true
activer l’API Service Worker sur iOS.
Prise en charge HTTP (non HTTPS)
À partir d’Android 9 (API niveau 28), la prise en charge du texte en clair est désactivée par défaut :
Sur iOS, vous devez désactiver Sécurité des transports Apple (ATS). Il y a deux options :
- Désactiver ATS pour un domaine spécifique uniquement (Wiki officiel): (ajoutez le code suivant à votre
Info.plist
dossier)
<key>NSAppTransportSecurity</key>
<dict>
<key>NSExceptionDomains</key>
<dict>
<key>www.yourserver.com</key>
<dict>
<!-- add this key to enable subdomains such as sub.yourserver.com -->
<key>NSIncludesSubdomains</key>
<true/>
<!-- add this key to allow standard HTTP requests, thus negating the ATS -->
<key>NSTemporaryExceptionAllowsInsecureHTTPLoads</key>
<true/>
<!-- add this key to specify the minimum TLS version to accept -->
<key>NSTemporaryExceptionMinimumTLSVersion</key>
<string>TLSv1.1</string>
</dict>
</dict>
</dict>
- Désactivez complètement l’ATS (Wiki officiel). Ajoutez le code suivant à votre
Info.plist
dossier:
<key>NSAppTransportSecurity</key>
<dict>
<key>NSAllowsArbitraryLoads</key><true/>
</dict>
Autre utile Info.plist
les propriétés sont :
NSAllowsLocalNetworking
: valeur booléenne indiquant s’il faut autoriser le chargement des ressources locales (Wiki officiel)NSAllowsArbitraryLoadsInWebContent
: valeur booléenne indiquant si toutes les restrictions App Transport Security sont désactivées pour les requêtes effectuées à partir de vues Web (Wiki officiel)
Aussi, nous allons utiliser le WidgetsBindingObserver
pour Android, utile pour savoir quand le système place l’application en arrière-plan ou renvoie l’application au premier plan.
Avec lui, nous pouvons arrêter et reprendre l’exécution de JavaScript et tout traitement pouvant être interrompu en toute sécurité, comme les vidéos, l’audio et les animations.
Voici une implémentation simple de didChangeAppLifecycleState
:
@override
void didChangeAppLifecycleState(AppLifecycleState state) {
if (!kIsWeb) {
if (webViewController != null &&
defaultTargetPlatform == TargetPlatform.android) {
if (state == AppLifecycleState.paused) {
pauseAll();
} else {
resumeAll();
}
}
}
}void pauseAll() {
if (defaultTargetPlatform == TargetPlatform.android) {
webViewController?.pause();
}
webViewController?.pauseTimers();
}
void resumeAll() {
if (defaultTargetPlatform == TargetPlatform.android) {
webViewController?.resume();
}
webViewController?.resumeTimers();
}
Pour détecter les clics sur le bouton de retour d’Android, nous enveloppons notre principal Scaffold
application widget dans un WillPopScope
widget et implémentez le onWillPop
méthode pour remonter dans l’histoire de WebView
.
Voici un exemple d’implémentation :
@override
Widget build(BuildContext context) {
return WillPopScope(
onWillPop: () async {
// detect Android back button click
final controller = webViewController;
if (controller != null) {
if (await controller.canGoBack()) {
controller.goBack();
return false;
}
}
return true;
},
child: Scaffold(
appBar: AppBar(
// remove the toolbar
toolbarHeight: 0,
),
body: // ...
),
);
}
Avant de charger l’URL PWA dans le InAppWebView
wrapper, nous vérifions si la connexion Internet est disponible à l’aide de l’utilitaire défini précédemment et, en fonction de celui-ci, nous devons définir le mode et la politique de cache pour Android et iOS de cette manière :
// Android-only
final cacheMode = networkAvailable
? CacheMode.LOAD_DEFAULT
: CacheMode.LOAD_CACHE_ELSE_NETWORK;// iOS-only
final cachePolicy = networkAvailable
? URLRequestCachePolicy.USE_PROTOCOL_CACHE_POLICY
: URLRequestCachePolicy.RETURN_CACHE_DATA_ELSE_LOAD;
La cacheMode
seront utilisés dans le initialSettings
la propriété et la cachePolicy
seront utilisés dans le URLRequest
de la initialUrlRequest
propriété.
Cette logique nous permet de charger les données mises en cache si une connexion Internet n’est pas disponible.
Pour limiter la navigation à l’hôte PWA uniquement, nous implémentons le shouldOverrideUrlLoading
pour vérifier si une requête HTTP spécifique pour le cadre principal ne correspond pas à l’hôte PWA, nous allons donc ouvrir cette requête dans des applications tierces à l’aide de la url_launcher
brancher:
shouldOverrideUrlLoading:
(controller, navigationAction) async {
// restrict navigation to target host, open external links in 3rd party apps
final uri = navigationAction.request.url;
if (uri != null &&
navigationAction.isForMainFrame &&
uri.host != kPwaHost &&
await canLaunchUrl(uri)) {
launchUrl(uri);
return NavigationActionPolicy.CANCEL;
}
return NavigationActionPolicy.ALLOW;
},
Pour détecter si la PWA a été « installée » correctement la première fois, nous implémentons le onLoadStop
WebView
méthode pour vérifier la disponibilité de la connexion internet et si la PWA a déjà été installée :
onLoadStop: (controller, url) async {
if (await isNetworkAvailable() && !(await isPWAInstalled())) {
// if network is available and this is the first time
setPWAInstalled();
}
},
Les deux utilitaires, isPWAInstalled
et setPWAInstalled
pourrait être implémenté comme suit en utilisant le shared_preferences
plugin pour obtenir et enregistrer le statut d’installation de PWA :
Future<bool> isPWAInstalled() async {
final prefs = await SharedPreferences.getInstance();
return prefs.getBool('isInstalled') ?? false;
}void setPWAInstalled({bool installed = true}) async {
final prefs = await SharedPreferences.getInstance();
await prefs.setBool('isInstalled', installed);
}
Tous ces utilitaires nous permettent de détecter la disponibilité du réseau et l’état d’installation de la PWA afin que nous puissions implémenter une page d’erreur personnalisée, comme ci-dessous :
onReceivedError: (controller, request, error) async {
final isForMainFrame = request.isForMainFrame ?? true;
if (isForMainFrame && !(await isNetworkAvailable())) {
if (!(await isPWAInstalled())) {
await controller.loadData(
data: kHTMLErrorPageNotInstalled);
}
}
},
où kHTMLErrorPageNotInstalled
est une chaîne contenant notre code HTML personnalisé.
Si vous avez besoin de soutenir le API JavaScript de notification Web, malheureusement, WebView natif Android et WKWebView natif iOS ne prennent pas en charge cette fonctionnalité de manière native, nous devons donc l’implémenter nous-mêmes ! Pour un exemple d’implémentation, vous pouvez vérifier le Exemple de projet de notification Web. Il utilise un UserScript
pour injecter du code JavaScript personnalisé au démarrage de la page Web afin d’implémenter l’API de notification Web.
Le code JavaScript injecté essaie de créer un « polyfill » pour le Notification
objet fenêtre et communiquer avec le côté Flutter/Dart en utilisant Gestionnaires JavaScript pour gérer et mettre en œuvre l’interface utilisateur de notification correspondante, par exemple, lorsque vous demandez l’autorisation avec Notification.requestPermission()
ou lorsque vous souhaitez afficher une notification.
De plus, si vous devez prendre en charge l’utilisation de la caméra et du microphone (par exemple, une application WebRTC), vous devez implémenter le onPermissionRequest
événement et demander des autorisations en utilisant, par exemple, le permission_handler
brancher. Pour plus de détails, visitez le guide WebRTC officiel et le Exemple de projet WebRTC.
Pour gérer les demandes qui ouvrent une nouvelle fenêtre à l’aide de JavaScript (window.open()
) ou par l’attribut cible dans un lien (tel que target="_blank"
), nous devons implémenter onCreateWindow
événement et retour true
pour déclarer que nous traitons la demande. Voici un exemple simple :
onCreateWindow: (controller, createWindowAction) async {
showDialog(
context: context,
builder: (context) {
final popupWebViewSettings =
sharedSettings.copy();
popupWebViewSettings.supportMultipleWindows =
false;
popupWebViewSettings
.javaScriptCanOpenWindowsAutomatically =
false;return WebViewPopup(
createWindowAction: createWindowAction,
popupWebViewSettings: popupWebViewSettings);
},
);
return true;
},
La WebViewPopup
est un autre InAppWebView
instance à l’intérieur d’un AlertDialog
widget qui prend en entrée le createWindowAction
pour obtenir le windowId
à utiliser pour le nouveau WebView
. La windowId
est un identifiant utilisé du côté natif pour obtenir le droit WebView
référence que Flutter doit montrer. Le WebView Popup implémentera également le onCloseWindow
pour écouter quand la popup doit être fermée et supprimée de l’arborescence Widget :
onCloseWindow: (controller) {
Navigator.pop(context);
},
Vérifiez également le Exemple de projet de fenêtre contextuelle pour un exemple de réalisation.
Pour implémenter notre communication bidirectionnelle entre JavaScript et Flutter/Dart, nous utiliserons le Gestionnaires JavaScript caractéristique.
Pour notre cas d’utilisation, nous voulons écouter les clics sur l’élément HTML du bouton « Demander des notifications factices » avec l’identifiant notifications
et montrer un SnackBar
avec le texte aléatoire généré par JavaScript.
Pour ce faire, nous créons un simple Scénario utilisateur et injectez-le après le chargement de la page :
initialUserScripts: UnmodifiableListView<UserScript>([
UserScript(
source: """
document.getElementById('notifications').addEventListener('click', function(event) {
var randomText = Math.random().toString(36).slice(2, 7);
window.flutter_inappwebview.callHandler('requestDummyNotification', randomText);
});
""",
injectionTime:
UserScriptInjectionTime.AT_DOCUMENT_END)
]),
Ensuite, nous ajoutons le gestionnaire JavaScript correspondant juste après lorsque le WebView
instance est créée :
onWebViewCreated: (controller) {
webViewController = controller;controller.addJavaScriptHandler(
handlerName: 'requestDummyNotification',
callback: (arguments) {
final String randomText =
arguments.isNotEmpty ? arguments[0] : '';
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(randomText)));
},
);
},
Voici le résultat :
L’exemple de projet de code complet est disponible sur https://github.com/pichillilorenzo/flutter_inappwebview_examples/tree/main/pwa_to_flutter_app
C’est tout pour aujourd’hui!
Utilisez-vous ce plug-in ? Soumettez votre application via le Soumettre l’application page et suivez les instructions. Vérifier la Vitrine page pour voir qui l’utilise déjà !
Ce projet suit la tous les contributeurs spécification (contributeurs). Je tiens à remercier toutes les personnes qui soutiennent le projet de quelque manière que ce soit. Merci beaucoup à vous tous ! 💙