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 onNavigationRequest per 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 onWebResourceError segnala 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.