webview_flutter仓库地址:webview_flutter | Flutter package
github地址:https://github.com/flutter/packages/tree/main/packages/webview_flutter/webview_flutter
要打开非https协议的网页,需要在安卓平台上添加权限:android:usesCleartextTraffic="true"
打开网页demo:
// 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.// ignore_for_file: public_member_api_docsimport 'dart:async';import 'dart:convert';import 'dart:io';import 'dart:typed_data';import 'package:flutter/material.dart';import 'package:webview_flutter/webview_flutter.dart';// #docregion platform_imports// Import for Android features.import 'package:webview_flutter_android/webview_flutter_android.dart';// Import for iOS features.import 'package:webview_flutter_wkwebview/webview_flutter_wkwebview.dart';// #enddocregion platform_importsvoid main() => runApp(const MaterialApp(home: WebViewExample()));const String kNavigationExamplePage = '''<!DOCTYPE html><html><head><title>Navigation Delegate Example</title></head><body><p>The navigation delegate is set to block navigation to the youtube website.</p><ul><ul><a href="https://www.youtube.com/">https://www.youtube.com/</a></ul><ul><a href="https://www.google.com/">https://www.google.com/</a></ul></ul></body></html>''';const String kLocalExamplePage = '''<!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.flutter-io.cn/packages/webview_flutter">Flutter webview</a> plugin.</p></body></html>''';const String kTransparentBackgroundPage = ''' <!DOCTYPE html> <html> <head> <title>Transparent background test</title> </head> <style type="text/css"> body { background: transparent; margin: 0; padding: 0; } #container { position: relative; margin: 0; padding: 0; width: 100vw; height: 100vh; } #shape { background: red; width: 200px; height: 200px; margin: 0; padding: 0; position: absolute; top: calc(50% - 100px); left: calc(50% - 100px); } p { text-align: center; } </style> <body> <div id="container"> <p>Transparent background test</p> <div id="shape"></div> </div> </body> </html>''';const String kLogExamplePage = '''<!DOCTYPE html><html lang="en"><head><title>Load file or HTML string example</title></head><body onload="console.log('Logging that the page is loading.')"><h1>Local demo page</h1><p> This page is used to test the forwarding of console logs to Dart.</p><style> .btn-group button { padding: 24px; 24px; display: block; width: 25%; margin: 5px 0px 0px 0px; }</style><div class="btn-group"> <button onclick="console.error('This is an error message.')">Error</button> <button onclick="console.warn('This is a warning message.')">Warning</button> <button onclick="console.info('This is a info message.')">Info</button> <button onclick="console.debug('This is a debug message.')">Debug</button> <button onclick="console.log('This is a log message.')">Log</button></div></body></html>''';class WebViewExample extends StatefulWidget { const WebViewExample({super.key}); @override State<WebViewExample> createState() => _WebViewExampleState();}class _WebViewExampleState extends State<WebViewExample> { late final WebViewController _controller; @override void initState() { super.initState(); // #docregion platform_features late final PlatformWebViewControllerCreationParams params; if (WebViewPlatform.instance is WebKitWebViewPlatform) { params = WebKitWebViewControllerCreationParams( allowsInlineMediaPlayback: true, mediaTypesRequiringUserAction: const <PlaybackMediaTypes>{}, ); } else { params = const PlatformWebViewControllerCreationParams(); } final WebViewController controller = WebViewController.fromPlatformCreationParams(params); // #enddocregion platform_features controller ..setJavaScriptMode(JavaScriptMode.unrestricted) ..setBackgroundColor(const Color(0x00000000)) ..setNavigationDelegate( NavigationDelegate( onProgress: (int progress) { debugPrint('WebView is loading (progress : $progress%)'); }, onPageStarted: (String url) { debugPrint('Page started loading: $url'); }, onPageFinished: (String url) { debugPrint('Page finished loading: $url'); }, onWebResourceError: (WebResourceError error) { debugPrint('''Page resource error: code: ${error.errorCode} description: ${error.description} errorType: ${error.errorType} isForMainFrame: ${error.isForMainFrame} '''); }, onNavigationRequest: (NavigationRequest request) { if (request.url.startsWith('https://www.youtube.com/')) { debugPrint('blocking navigation to ${request.url}'); return NavigationDecision.prevent; } debugPrint('allowing navigation to ${request.url}'); return NavigationDecision.navigate; }, onUrlChange: (UrlChange change) { debugPrint('url change to ${change.url}'); }, onHttpAuthRequest: (HttpAuthRequest request) { openDialog(request); }, ), ) ..addJavaScriptChannel( 'Toaster', onMessageReceived: (JavaScriptMessage message) { ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text(message.message)), ); }, ) ..loadRequest(Uri.parse('http://192.168.1.171:5173/#/pad?team=red')); // #docregion platform_features if (controller.platform is AndroidWebViewController) { AndroidWebViewController.enableDebugging(true); (controller.platform as AndroidWebViewController) .setMediaPlaybackRequiresUserGesture(false); } // #enddocregion platform_features _controller = controller; } @override Widget build(BuildContext context) { return Scaffold( backgroundColor: Colors.green, body: WebViewWidget(controller: _controller), // floatingActionButton: favoriteButton(), ); } Widget favoriteButton() { return FloatingActionButton( onPressed: () async { final String? url = await _controller.currentUrl(); if (mounted) { ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text('Favorited $url')), ); } }, child: const Icon(Icons.favorite), ); } Future<void> openDialog(HttpAuthRequest httpRequest) async { final TextEditingController usernameTextController = TextEditingController(); final TextEditingController passwordTextController = TextEditingController(); return showDialog( context: context, barrierDismissible: false, builder: (BuildContext context) { return AlertDialog( title: Text('${httpRequest.host}: ${httpRequest.realm ?? '-'}'), content: SingleChildScrollView( child: Column( mainAxisSize: MainAxisSize.min, children: <Widget>[ TextField( decoration: const InputDecoration(labelText: 'Username'), autofocus: true, controller: usernameTextController, ), TextField( decoration: const InputDecoration(labelText: 'Password'), controller: passwordTextController, ), ], ), ), actions: <Widget>[ // Explicitly cancel the request on iOS as the OS does not emit new // requests when a previous request is pending. TextButton( onPressed: () { httpRequest.onCancel(); Navigator.of(context).pop(); }, child: const Text('Cancel'), ), TextButton( onPressed: () { httpRequest.onProceed( WebViewCredential( user: usernameTextController.text, password: passwordTextController.text, ), ); Navigator.of(context).pop(); }, child: const Text('Authenticate'), ), ], ); }, ); }}enum MenuOptions { showUserAgent, listCookies, clearCookies, addToCache, listCache, clearCache, navigationDelegate, doPostRequest, loadLocalFile, loadFlutterAsset, loadHtmlString, transparentBackground, setCookie, logExample, basicAuthentication,}class SampleMenu extends StatelessWidget { SampleMenu({ super.key, required this.webViewController, }); final WebViewController webViewController; late final WebViewCookieManager cookieManager = WebViewCookieManager(); @override Widget build(BuildContext context) { return PopupMenuButton<MenuOptions>( key: const ValueKey<String>('ShowPopupMenu'), onSelected: (MenuOptions value) { switch (value) { case MenuOptions.showUserAgent: _onShowUserAgent(); case MenuOptions.listCookies: _onListCookies(context); case MenuOptions.clearCookies: _onClearCookies(context); case MenuOptions.addToCache: _onAddToCache(context); case MenuOptions.listCache: _onListCache(); case MenuOptions.clearCache: _onClearCache(context); case MenuOptions.navigationDelegate: _onNavigationDelegateExample(); case MenuOptions.doPostRequest: _onDoPostRequest(); case MenuOptions.loadFlutterAsset: _onLoadFlutterAssetExample(); case MenuOptions.loadHtmlString: _onLoadHtmlStringExample(); case MenuOptions.transparentBackground: _onTransparentBackground(); case MenuOptions.setCookie: _onSetCookie(); case MenuOptions.logExample: _onLogExample(); case MenuOptions.basicAuthentication: _promptForUrl(context); case MenuOptions.loadLocalFile: // TODO: Handle this case. } }, itemBuilder: (BuildContext context) => <PopupMenuItem<MenuOptions>>[ const PopupMenuItem<MenuOptions>( value: MenuOptions.showUserAgent, child: Text('Show user agent'), ), const PopupMenuItem<MenuOptions>( value: MenuOptions.listCookies, child: Text('List cookies'), ), const PopupMenuItem<MenuOptions>( value: MenuOptions.clearCookies, child: Text('Clear cookies'), ), const PopupMenuItem<MenuOptions>( value: MenuOptions.addToCache, child: Text('Add to cache'), ), const PopupMenuItem<MenuOptions>( value: MenuOptions.listCache, child: Text('List cache'), ), const PopupMenuItem<MenuOptions>( value: MenuOptions.clearCache, child: Text('Clear cache'), ), const PopupMenuItem<MenuOptions>( value: MenuOptions.navigationDelegate, child: Text('Navigation Delegate example'), ), const PopupMenuItem<MenuOptions>( value: MenuOptions.doPostRequest, child: Text('Post Request'), ), const PopupMenuItem<MenuOptions>( value: MenuOptions.loadHtmlString, child: Text('Load HTML string'), ), const PopupMenuItem<MenuOptions>( value: MenuOptions.loadLocalFile, child: Text('Load local file'), ), const PopupMenuItem<MenuOptions>( value: MenuOptions.loadFlutterAsset, child: Text('Load Flutter Asset'), ), const PopupMenuItem<MenuOptions>( key: ValueKey<String>('ShowTransparentBackgroundExample'), value: MenuOptions.transparentBackground, child: Text('Transparent background example'), ), const PopupMenuItem<MenuOptions>( value: MenuOptions.setCookie, child: Text('Set cookie'), ), const PopupMenuItem<MenuOptions>( value: MenuOptions.logExample, child: Text('Log example'), ), const PopupMenuItem<MenuOptions>( value: MenuOptions.basicAuthentication, child: Text('Basic Authentication Example'), ), ], ); } Future<void> _onShowUserAgent() { // Send a message with the user agent string to the Toaster JavaScript channel we registered // with the WebView. return webViewController.runJavaScript( 'Toaster.postMessage("User Agent: " + navigator.userAgent);', ); } Future<void> _onListCookies(BuildContext context) async { final String cookies = await webViewController .runJavaScriptReturningResult('document.cookie') as String; if (context.mounted) { ScaffoldMessenger.of(context).showSnackBar(SnackBar( content: Column( mainAxisAlignment: MainAxisAlignment.end, mainAxisSize: MainAxisSize.min, children: <Widget>[ const Text('Cookies:'), _getCookieList(cookies), ], ), )); } } Future<void> _onAddToCache(BuildContext context) async { await webViewController.runJavaScript( 'caches.open("test_caches_entry"); localStorage["test_localStorage"] = "dummy_entry";', ); if (context.mounted) { ScaffoldMessenger.of(context).showSnackBar(const SnackBar( content: Text('Added a test entry to cache.'), )); } } Future<void> _onListCache() { return webViewController.runJavaScript('caches.keys()' // ignore: missing_whitespace_between_adjacent_strings '.then((cacheKeys) => JSON.stringify({"cacheKeys" : cacheKeys, "localStorage" : localStorage}))' '.then((caches) => Toaster.postMessage(caches))'); } Future<void> _onClearCache(BuildContext context) async { await webViewController.clearCache(); await webViewController.clearLocalStorage(); if (context.mounted) { ScaffoldMessenger.of(context).showSnackBar(const SnackBar( content: Text('Cache cleared.'), )); } } Future<void> _onClearCookies(BuildContext context) async { final bool hadCookies = await cookieManager.clearCookies(); String message = 'There were cookies. Now, they are gone!'; if (!hadCookies) { message = 'There are no cookies.'; } if (context.mounted) { ScaffoldMessenger.of(context).showSnackBar(SnackBar( content: Text(message), )); } } Future<void> _onNavigationDelegateExample() { final String contentBase64 = base64Encode( const Utf8Encoder().convert(kNavigationExamplePage), ); return webViewController.loadRequest( Uri.parse('data:text/html;base64,$contentBase64'), ); } Future<void> _onSetCookie() async { await cookieManager.setCookie( const WebViewCookie( name: 'foo', value: 'bar', domain: 'httpbin.org', path: '/anything', ), ); await webViewController.loadRequest(Uri.parse( 'https://httpbin.org/anything', )); } Future<void> _onDoPostRequest() { return webViewController.loadRequest( Uri.parse('https://httpbin.org/post'), method: LoadRequestMethod.post, headers: <String, String>{'foo': 'bar', 'Content-Type': 'text/plain'}, body: Uint8List.fromList('Test Body'.codeUnits), ); } Future<void> _onLoadFlutterAssetExample() { return webViewController.loadFlutterAsset('assets/www/index.html'); } Future<void> _onLoadHtmlStringExample() { return webViewController.loadHtmlString(kLocalExamplePage); } Future<void> _onTransparentBackground() { return webViewController.loadHtmlString(kTransparentBackgroundPage); } Widget _getCookieList(String cookies) { if (cookies == '""') { return Container(); } final List<String> cookieList = cookies.split(';'); final Iterable<Text> cookieWidgets = cookieList.map((String cookie) => Text(cookie)); return Column( mainAxisAlignment: MainAxisAlignment.end, mainAxisSize: MainAxisSize.min, children: cookieWidgets.toList(), ); } Future<void> _onLogExample() { webViewController .setOnConsoleMessage((JavaScriptConsoleMessage consoleMessage) { debugPrint( '== JS == ${consoleMessage.level.name}: ${consoleMessage.message}'); }); return webViewController.loadHtmlString(kLogExamplePage); } Future<void> _promptForUrl(BuildContext context) { final TextEditingController urlTextController = TextEditingController(); return showDialog<String>( context: context, builder: (BuildContext context) { return AlertDialog( title: const Text('Input URL to visit'), content: TextField( decoration: const InputDecoration(labelText: 'URL'), autofocus: true, controller: urlTextController, ), actions: <Widget>[ TextButton( onPressed: () { if (urlTextController.text.isNotEmpty) { final Uri? uri = Uri.tryParse(urlTextController.text); if (uri != null && uri.scheme.isNotEmpty) { webViewController.loadRequest(uri); Navigator.pop(context); } } }, child: const Text('Visit'), ), ], ); }, ); }}class NavigationControls extends StatelessWidget { const NavigationControls({super.key, required this.webViewController}); final WebViewController webViewController; @override Widget build(BuildContext context) { return Row( children: <Widget>[ IconButton( icon: const Icon(Icons.arrow_back_ios), onPressed: () async { if (await webViewController.canGoBack()) { await webViewController.goBack(); } else { if (context.mounted) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('No back history item')), ); } } }, ), IconButton( icon: const Icon(Icons.arrow_forward_ios), onPressed: () async { if (await webViewController.canGoForward()) { await webViewController.goForward(); } else { if (context.mounted) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('No forward history item')), ); } } }, ), IconButton( icon: const Icon(Icons.replay), onPressed: () => webViewController.reload(), ), ], ); }}
最后的效果图: