背景 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' ;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; await androidController.setAllowFileAccess(true ); await androidController.setAllowContentAccess(true ); }
3. 接管文件选择回调(核心) 1 2 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' ;Future<List <String >> _onAndroidShowFileSelector( FileSelectorParams params, ) async { 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(); } 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 String _toWebViewUri(String path) { final trimmed = path.trim(); if (trimmed.startsWith('content://' ) || trimmed.startsWith('file://' )) { return trimmed; } return Uri .file(trimmed).toString(); }
策略分支总览
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 List <String > _normalizedAcceptTypes(List <String > types) { return types .expand((t) => t.split(',' )) .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 { 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)); if (controller.platform is AndroidWebViewController) { final android = controller.platform as AndroidWebViewController; 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 <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" />
注意事项
这是 Android 专项修复 :setOnShowFileSelector 是 AndroidWebViewController 的平台能力。iOS 如果遇到类似问题,需要单独验证 WKWebView 的行为。
当前方案面向图片/视频上传 :如果 H5 要求选择 PDF、文档等非媒体文件,需要换用更通用的文件选择方案(如 file_picker)。
用户取消选择是正常分支 :取消时返回空列表,不要当成错误处理,避免 crash。
为什么要写在 Flutter 层而不是原生 Activity :webview_flutter_android 已经提供了 setOnShowFileSelector,Flutter 层直接接管更贴近业务,不需要额外维护一套原生 WebView 容器。
验证
打开 App 内 H5 页面
点击上传图片 → 应弹出相册 → 选择后 H5 正常收到文件
点击上传视频 → 应弹出视频选择器 → 选择后 H5 正常收到文件
多选模式 → 可以选多张/多个
capture 模式 → 直接拉起相机/摄像
参考