背景

Hybrid App 中 H5 页面经常需要上传图片/视频,通过 <input type="file"> 触发系统选择器。但在 Flutter WebView 中,这个流程默认并不会自动接通——点击上传按钮往往毫无反应。

本文记录一套完整的修复方案:让 Android WebView 正确响应 H5 的文件选择请求,并把图片/视频回传给网页

现象

App 内 H5 页面在 Android WebView 中点击:

  • 上传图片按钮
  • 上传视频按钮
  • <input type="file"> 触发

时,没有弹出系统相册/选择器,或弹出后文件无法回传,H5 上传流程卡死。

这个问题只在 Android WebView 场景明显,原生 Flutter 页面中直接调用 image_picker 不受影响。

根因

问题的核心在于:Flutter webview_flutter 在 Android 侧 默认不会自动接管文件选择器

H5 里的 <input type="file"> 最终会走到 Android WebChromeClient.onShowFileChooser() 回调。如果 Flutter 层没有显式处理这个回调:

  • WebView 不知道如何打开文件选择器
  • H5 请求了图片/视频,但 Flutter 侧没有对应动作
  • 选完的文件也无法回传给网页

也就是说,必须由 Flutter 层主动接管这个原生回调。

解决方案

1. 使用 Android 专属 WebViewController

创建控制器时,针对 Android 构造平台参数:

1
2
3
4
5
6
7
8
9
10
11
12
13
import 'package:webview_flutter_android/webview_flutter_android.dart';

// 创建 WebView 控制器时,先构造 Android 专属参数
PlatformWebViewControllerCreationParams params =
const PlatformWebViewControllerCreationParams();

if (WebView.platform is AndroidWebViewPlatform) {
params = AndroidWebViewControllerCreationParams(
(params as PlatformWebViewControllerCreationParams),
);
}

final controller = WebViewController.fromPlatformCreationParams(params);

这样后续才能安全拿到 AndroidWebViewController 并配置 Android 专属能力。

2. 启用文件访问

1
2
3
4
5
6
7
8
if (controller.platform is AndroidWebViewController) {
final androidController = controller.platform as AndroidWebViewController;

// 允许 WebView 访问 file:// 和 content:// URI
// 否则文件选出来了但网页无法读取
await androidController.setAllowFileAccess(true);
await androidController.setAllowContentAccess(true);
}

3. 接管文件选择回调(核心)

1
2
// 把 Android WebView 的文件选择请求转交给 Flutter 层
await androidController.setOnShowFileSelector(_onAndroidShowFileSelector);

_onAndroidShowFileSelector 是我们自己实现的文件选择逻辑:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
import 'package:image_picker/image_picker.dart';

/// Android WebView 文件选择回调
Future<List<String>> _onAndroidShowFileSelector(
FileSelectorParams params,
) async {
// 1. 解析 H5 请求的 accept 类型
final acceptTypes = _normalizedAcceptTypes(params.acceptTypes);
final bool wantsImage = _isImageType(acceptTypes);
final bool wantsVideo = _isVideoType(acceptTypes);
final bool isMultiple = params.mode == FileSelectorMode.multiple;
final bool isCapture = params.isCapture;

final ImagePicker picker = ImagePicker();

try {
List<XFile> files;

if (wantsImage && !wantsVideo) {
// 只选图片
files = isMultiple
? await picker.pickMultiImage()
: [await picker.pickImage(
source: isCapture ? ImageSource.camera : ImageSource.gallery,
)].whereType<XFile>().toList();
} else if (wantsVideo && !wantsImage) {
// 只选视频
files = isMultiple
? await picker.pickMultiVideo()
: [await picker.pickVideo(
source: isCapture ? ImageSource.camera : ImageSource.gallery,
)].whereType<XFile>().toList();
} else {
// 图片视频都行(混合类型)
files = isMultiple
? await picker.pickMultipleMedia()
: [await picker.pickMedia()].whereType<XFile>().toList();
}

// 2. 把 XFile 转成 WebView 可识别的 URI 数组
return files
.map((f) => _toWebViewUri(f.path))
.whereType<String>()
.toList();
} catch (e) {
// 用户取消选择,返回空列表(不是错误)
return [];
}
}

4. URI 格式转换

WebView 不能直接吃裸文件路径,必须转成标准 URI 格式:

1
2
3
4
5
6
7
8
9
10
11
/// 把本地文件路径转成 WebView 能识别的 URI
/// content://... → 原样返回
/// file://... → 原样返回
/// /data/xxx → file:///data/xxx
String _toWebViewUri(String path) {
final trimmed = path.trim();
if (trimmed.startsWith('content://') || trimmed.startsWith('file://')) {
return trimmed;
}
return Uri.file(trimmed).toString(); // → file:///...
}

策略分支总览

H5 请求类型 单选/多选 capture 调用的 API
仅图片 单选 pickImage(ImageSource.gallery)
仅图片 单选 pickImage(ImageSource.camera)
仅图片 多选 pickMultiImage()
仅视频 单选 pickVideo(ImageSource.gallery)
仅视频 单选 pickVideo(ImageSource.camera)
仅视频 多选 pickMultiVideo()
混合(图片+视频) 单选 - pickMedia()
混合(图片+视频) 多选 - pickMultipleMedia()

解析 H5 的 acceptTypes

H5 传来的 accept 字符串可能很花,需要归一化:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/// 归一化 accept 字符串
List<String> _normalizedAcceptTypes(List<String> types) {
return types
.expand((t) => t.split(',')) // "image/*,video/*" 拆开
.map((t) => t.trim().toLowerCase())
.toList();
}

/// 判断是否为图片类型
bool _isImageType(List<String> types) {
return types.any((t) => t.contains('image'));
}

/// 判断是否为视频类型
bool _isVideoType(List<String> types) {
return types.any((t) => t.contains('video'));
}

完整 WebView 初始化代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
Future<WebViewController> _createWebViewController() async {
// 1. Android 专属参数
PlatformWebViewControllerCreationParams params =
const PlatformWebViewControllerCreationParams();
if (WebView.platform is AndroidWebViewPlatform) {
params = AndroidWebViewControllerCreationParams(
(params as PlatformWebViewControllerCreationParams),
);
}

final controller = WebViewController.fromPlatformCreationParams(params)
..setJavaScriptMode(JavaScriptMode.unrestricted)
..loadRequest(Uri.parse(url));

// 2. Android 专项配置
if (controller.platform is AndroidWebViewController) {
final android = controller.platform as AndroidWebViewController;

// 允许读取文件和内容 URI
await android.setAllowFileAccess(true);
await android.setAllowContentAccess(true);

// 接管文件选择器
await android.setOnShowFileSelector(_onAndroidShowFileSelector);
}

return controller;
}

依赖与权限

pubspec.yaml

1
2
3
4
dependencies:
webview_flutter: ^4.13.1
webview_flutter_android: ^4.10.14
image_picker: ^1.0.0

AndroidManifest.xml

1
2
3
4
5
6
7
8
9
10
<!-- 读取媒体文件(Android 13+ 用 READ_MEDIA_*) -->
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES" />
<uses-permission android:name="android.permission.READ_MEDIA_VIDEO" />

<!-- 旧版兼容 -->
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"
android:maxSdkVersion="32" />

<!-- 拍照/录视频 -->
<uses-permission android:name="android.permission.CAMERA" />

注意事项

  1. 这是 Android 专项修复setOnShowFileSelectorAndroidWebViewController 的平台能力。iOS 如果遇到类似问题,需要单独验证 WKWebView 的行为。

  2. 当前方案面向图片/视频上传:如果 H5 要求选择 PDF、文档等非媒体文件,需要换用更通用的文件选择方案(如 file_picker)。

  3. 用户取消选择是正常分支:取消时返回空列表,不要当成错误处理,避免 crash。

  4. 为什么要写在 Flutter 层而不是原生 Activitywebview_flutter_android 已经提供了 setOnShowFileSelector,Flutter 层直接接管更贴近业务,不需要额外维护一套原生 WebView 容器。

验证

  1. 打开 App 内 H5 页面
  2. 点击上传图片 → 应弹出相册 → 选择后 H5 正常收到文件
  3. 点击上传视频 → 应弹出视频选择器 → 选择后 H5 正常收到文件
  4. 多选模式 → 可以选多张/多个
  5. capture 模式 → 直接拉起相机/摄像

参考