flutter 开发笔记(一):webview

552 阅读3分钟

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),
    );
  }
}