WebView 在 App 中是一个常用的功能,本文结合日常开发 App 时遇到的需求,探索对应的解决方法。以下是详细记录
安装
flutter 使用的是 webview_flutter,需要在 pubspec.yaml
文件中添加 webview_flutter
依赖,最新的版本可以在 pub.dev/packages/we… 中查询
dependencies:
flutter:
sdk: flutter
webview_flutter: ^4.10.0
另外,还需要在 android 的 AndroidManifest.xml
中添加网络权限,若不添加,则会遇到 net:ERR_CACHE_MISS
问题
<uses-permission android:name="android.permission.INTERNET" />
使用
使用的话,需要建立一个 _controller
,它是 WebViewController
的一个实例,它用于控制和管理 WebView 的行为和状态。具体来说,_controller
允许你执行各种操作,例如加载 URL、运行 JavaScript 代码、导航历史记录等
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:webview_flutter/webview_flutter.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
// This widget is the root of your application.
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
useMaterial3: true,
),
home: const WebViewExample(),
);
}
}
class WebViewExample extends StatefulWidget {
const WebViewExample({super.key});
@override
WebViewExampleState createState() => WebViewExampleState();
}
class WebViewExampleState extends State<WebViewExample> {
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(
actions: [
IconButton(
icon: const Icon(Icons.arrow_back_rounded),
onPressed: () {
_controller.goBack();
},
),
IconButton(
icon: const Icon(Icons.arrow_forward_rounded),
onPressed: () {
_controller.goForward();
},
),
IconButton(
icon: const Icon(Icons.refresh),
onPressed: () {
_controller.reload();
},
),
],
title: const Text('Webview'),
),
body: WebViewWidget(controller: _controller),
);
}
}
代码渲染了 flutter 的官方网址,支持前进、回退和刷新操作
交互
与 webview 渲染的网页交互一直都是很常见的功能,同样地,需要利用上述的 _controller
,在 WebViewExampleState 中创建一个方法,交互的信息同样由两方决定
void _sendMessageToWeb(String message) {
_controller.runJavaScript("window.flutter_injected_code('$message');");
}
如果需要的话,就调用这个方法
_sendMessageToWeb('Hello from flutter');
接收消息,则需要改造 _controller
,加上 addJavaScriptChannel
的实现
_controller = WebViewController()
..setJavaScriptMode(JavaScriptMode.unrestricted)
..addJavaScriptChannel('FlutterChannel', onMessageReceived: (javaScriptMessage) {
print(javaScriptMessage.message);
})
..loadRequest(Uri.parse('http://192.168.111.141:5173/')); //
那么网页端该如何接收呢,以 vue3 为例
<template>
<div id="app">
<h1>{{ message }}</h1>
<button @click="sendMessageToFlutter">Send Message to Flutter</button>
</div>
</template>
<script lang="ts" setup>
import { ref, onMounted } from 'vue';
const message = ref<string>('Hello, Vue 3!');
function sendMessageToFlutter() {
if (window.FlutterChannel && window.FlutterChannel.postMessage) {
window.FlutterChannel.postMessage('Hello from Vue 3');
} else {
message.value = 'FlutterChannel is not available';
}
}
function flutterInjectedCode(msg: string) {
message.value = msg;
}
// 当组件挂载时注册全局函数
onMounted(() => {
window.flutter_injected_code = flutterInjectedCode;
});
</script>
<style scoped>
#app {
font-family: Avenir, Helvetica, Arial, sans-serif;
text-align: center;
color: #2c3e50;
margin-top: 60px;
}
</style>
以下是最终的 WebViewExampleState 代码
class WebViewExampleState extends State<WebViewExample> {
late final WebViewController _controller;
@override
void initState() {
super.initState();
_controller = WebViewController()
..setJavaScriptMode(JavaScriptMode.unrestricted)
..addJavaScriptChannel('FlutterChannel',
onMessageReceived: (javaScriptMessage) {
print(javaScriptMessage.message);
})
..loadRequest(Uri.parse('http://192.168.111.141:5173/'));
}
void _sendMessageToWeb(String message) {
_controller.runJavaScript("window.flutter_injected_code('$message');");
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
actions: [
IconButton(
icon: const Icon(Icons.send),
onPressed: () {
_sendMessageToWeb('Hello from Flutter');
},
),
],
title: const Text('Webview'),
),
body: WebViewWidget(controller: _controller),
);
}
}
渲染富文本
渲染富文本也是常见的需求,同样需要改造 _controller
,加上 loadHtmlString
的实现
class WebViewExampleState extends State<WebViewExample> {
late WebViewController _controller;
@override
void initState() {
super.initState();
_controller = WebViewController()
..setJavaScriptMode(JavaScriptMode.unrestricted)
..loadHtmlString(_loadHtml());
}
String _loadHtml() {
return '''
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
<title>Rich Text Example</title>
</head>
<body>
<h1>Rich Text Example</h1>
<p>This is an example of <strong>bold</strong> text, <em>italic</em> text, and <u>underlined</u> text.</p>
<p>Here is a list:</p>
<ul>
<li>Item 1</li>
<li>Item 2</li>
<li>Item 3</li>
</ul>
<p>Here is a <a href="https://flutter.dev">link</a>.</p>
<img src="https://w.wallhaven.cc/full/73/wallhaven-7318pv.png" alt="Flutter" width="200">
</body>
</html>
''';
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Webview'),
),
body: WebViewWidget(controller: _controller),
);
}
}
有时,webview 并无法占满一个版面,它可能需要与 flutter 的组件共存,结合上面的 addJavaScriptChannel
,可以禁止 html 的滑动(设置 body 的 overflow 为 hidden),然后把 height 传递给 flutter 方再设置高度;flutter 代码则设置了 SizedBox 包裹 WebViewWidget ,以供动态设置高度
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:webview_flutter/webview_flutter.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Rich Text Demo',
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
useMaterial3: true,
),
home: Scaffold(
appBar: AppBar(
title: const Text('Webview with Additional Content'),
),
body: const Column(
children: [
WebViewExample(),
Divider(),
Padding(
padding: EdgeInsets.all(16.0),
child: Text(
'This is additional text below the WebView.',
style: TextStyle(fontSize: 16, color: Colors.black87),
),
),
],
),
),
);
}
}
class WebViewExample extends StatefulWidget {
const WebViewExample({super.key});
@override
WebViewExampleState createState() => WebViewExampleState();
}
class WebViewExampleState extends State<WebViewExample> {
late WebViewController _controller;
double _height = 0;
@override
void initState() {
super.initState();
_controller = WebViewController()
..setJavaScriptMode(JavaScriptMode.unrestricted)
..addJavaScriptChannel('FlutterChannel',
onMessageReceived: (javaScriptMessage) {
final message = jsonDecode(javaScriptMessage.message);
if (message['height'] != null) {
double newHeight = message['height'].toDouble();
if (_height != newHeight) {
setState(() {
_height = message['height'].toDouble();
});
}
}
})
..loadHtmlString(_loadHtml());
}
String _loadHtml() {
return '''
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
<title>Rich Text Example</title>
<style>
body {
overflow: hidden;
}
</style>
<script>
function sendHeightToFlutter() {
const height = document.body.scrollHeight;
if (window.FlutterChannel) {
window.FlutterChannel.postMessage(JSON.stringify({ height: height }));
}
}
window.onload = sendHeightToFlutter;
window.onresize = sendHeightToFlutter;
</script>
</head>
<body>
<h1>Rich Text Example</h1>
<p>This is an example of <strong>bold</strong> text, <em>italic</em> text, and <u>underlined</u> text.</p>
<p>Here is a list:</p>
<ul>
<li>Item 1</li>
<li>Item 2</li>
<li>Item 3</li>
</ul>
<p>Here is a <a href="https://flutter.dev">link</a>.</p>
<img src="https://w.wallhaven.cc/full/73/wallhaven-7318pv.png" alt="Flutter" width="200">
</body>
</html>
''';
}
@override
Widget build(BuildContext context) {
return SizedBox(
height: _height,
child: WebViewWidget(controller: _controller),
);
}
}