Introifca perché una WebView
Non tutto ciò che mostriamo in un'app deve essere ricostruito con widget nativi. A volte serve visualizzare una pagina web esistente, un contenuto HTML dinamico, i termini di servizio, una dashboard di terze parti o completare un flusso OAuth che avviene interamente nel browser. In questi casi la WebView è lo strumento giusto.
Il pacchetto ufficiale mantenuto dal team Flutter è webview_flutter, che a partire dalla versione 4 espone un'API unificata e moderna basata su WebViewController.
Installazione e configurazione
Aggiungi la dipendenza:
dependencies:
webview_flutter: ^4.10.0
Su Android non serve alcuna configurazione particolare per contenuti HTTPS. Se devi caricare contenuti HTTP in chiaro, ricorda di abilitare il cleartext traffic nel AndroidManifest.xml.
Su iOS non è necessario aggiungere permessi per il semplice caricamento di pagine, ma se la WebView deve accedere a fotocamera o microfono dovrai dichiarare le relative NSCameraUsageDescription nel file Info.plist.
Una WebView di base
La struttura minima ruota attorno a un WebViewController configurato in initState e passato al widget WebViewWidget:
import 'package:flutter/material.dart';
import 'package:webview_flutter/webview_flutter.dart';
class SimpleWebView extends StatefulWidget {
const SimpleWebView({super.key});
@override
State<SimpleWebView> createState() => _SimpleWebViewState();
}
class _SimpleWebViewState extends State<SimpleWebView> {
late final WebViewController _controller;
@override
void initState() {
super.initState();
_controller = WebViewController()
..setJavaScriptMode(JavaScriptMode.unrestricted)
..loadRequest(Uri.parse('https://flutter.dev'));
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('WebView')),
body: WebViewWidget(controller: _controller),
);
}
}
Con JavaScriptMode.unrestricted abilitiamo l'esecuzione di JavaScript, indispensabile per la maggior parte dei siti moderni.
Monitorare la navigazione e il caricamento
Uno dei bisogni più frequenti è mostrare un indicatore di progresso e intercettare la navigazione. Il NavigationDelegate permette di reagire a ogni evento del ciclo di vita:
_controller
..setJavaScriptMode(JavaScriptMode.unrestricted)
..setNavigationDelegate(
NavigationDelegate(
onProgress: (progress) {
setState(() => _progress = progress / 100);
},
onPageStarted: (url) => debugPrint('Caricamento: $url'),
onPageFinished: (url) => debugPrint('Completato: $url'),
onWebResourceError: (error) {
debugPrint('Errore: ${error.description}');
},
onNavigationRequest: (request) {
if (request.url.startsWith('https://blocked.com')) {
return NavigationDecision.prevent;
}
return NavigationDecision.navigate;
},
),
)
..loadRequest(Uri.parse('https://flutter.dev'));
Il callback onNavigationRequest è particolarmente utile per intercettare i redirect in un flusso OAuth: quando il provider reindirizza al tuo redirect_uri puoi bloccare la navigazione, estrarre il codice di autorizzazione dalla query e chiudere la WebView.
Comunicazione tra Dart e JavaScript
La WebView non è una scatola chiusa: possiamo far dialogare il codice web con quello Dart in entrambe le direzioni.
Da Dart a JavaScript
Per eseguire codice JavaScript e leggere il risultato:
final result = await _controller.runJavaScriptReturningResult(
'document.title',
);
debugPrint('Titolo della pagina: $result');
Da JavaScript a Dart
Registrando un JavaScriptChannel esponiamo un canale che la pagina web può invocare:
_controller.addJavaScriptChannel(
'FlutterBridge',
onMessageReceived: (JavaScriptMessage message) {
debugPrint('Messaggio da JS: ${message.message}');
},
);
Dal lato JavaScript, la pagina invia dati così:
FlutterBridge.postMessage('Utente ha cliccato il pulsante');
Questo meccanismo è perfetto per contenuti HTML ibridi in cui la parte web deve notificare eventi all'app nativa.
Caricare HTML locale o inline
Oltre agli URL remoti puoi caricare stringhe HTML direttamente o file dagli asset:
_controller.loadHtmlString('''
<html>
<body style="font-family: sans-serif; padding: 24px">
<h1>Contenuto locale</h1>
<button onclick="FlutterBridge.postMessage('ciao')">Invia</button>
</body>
</html>
''');
Per file negli asset, ricordati di dichiararli nel pubspec.yaml e usa loadFlutterAsset('assets/pagina.html').
Gestire il pulsante indietro
Un dettaglio spesso trascurato è il tasto indietro di Android: per impostazione predefinita chiude la schermata invece di navigare indietro nella cronologia della WebView. Usa PopScope per gestirlo:
PopScope(
canPop: false,
onPopInvokedWithResult: (didPop, result) async {
if (didPop) return;
if (await _controller.canGoBack()) {
_controller.goBack();
} else {
if (mounted) Navigator.of(context).pop();
}
},
child: WebViewWidget(controller: _controller),
)
Buone pratiche
- Limita l'ambito: usa la WebView solo dove serve davvero, preferendo widget nativi per prestazioni e coerenza dell'interfaccia.
- Valida gli URL in
onNavigationRequestper evitare che l'utente navighi fuori dal dominio previsto. - Attenzione ai
JavaScriptChannel: rappresentano una superficie di sicurezza, non esporre operazioni sensibili a pagine di cui non hai il controllo. - Gestisci lo stato offline mostrando un fallback quando
onWebResourceErrorsegnala l'assenza di rete.
Conclusione
webview_flutter è la soluzione ufficiale e affidabile per integrare contenuti web nelle app Flutter. Con WebViewController, NavigationDelegate e i canali JavaScript hai tutto il necessario per costruire esperienze ibride solide: dalla semplice visualizzazione di una pagina a complessi flussi bidirezionali tra web e nativo.