Flutter InAppWebView本地服务器指向外部存储目录问题求助
如何让InAppWebView本地服务器指向外部存储目录?
问题描述
需要将InAppWebView的本地服务器根目录设置为外部存储的/storage/emulated/0/Download/mediplus-lite,而非默认的assets目录,但当前配置无法生效。
解决方案
1. 申请并获取外部存储权限
Android系统对外部存储访问有严格权限控制,必须先完成权限申请:
- 在
android/app/src/main/AndroidManifest.xml中添加权限声明:<!-- Android 12及以下适配 --> <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" /> <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" /> <!-- Android 13及以上适配 --> <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_MEDIA_AUDIO" /> - 运行时动态请求权限,可借助
permission_handler包简化流程:import 'package:permission_handler/permission_handler.dart'; Future<bool> requestStoragePermission() async { PermissionStatus status; if (Platform.isAndroid) { status = await (await AndroidInfo.version >= 13) ? Permission.photos.request() : Permission.storage.request(); } else { status = await Permission.storage.request(); } return status.isGranted; }
2. 正确配置InAppLocalhostServer
- 避免硬编码路径,用系统API获取外部存储目录保证兼容性:
import 'package:path_provider/path_provider.dart'; Future<String> getTargetDirectory() async { Directory? baseDir; if (Platform.isAndroid) { baseDir = await getExternalStorageDirectory(); baseDir = Directory('${baseDir?.path}/Download/mediplus-lite'); } return baseDir?.path ?? ''; } - 启动服务器前先验证权限和路径有效性,处理启动异常。
3. 修改后的完整代码示例
import 'dart:io'; import 'package:flutter/material.dart'; import 'package:flutter_inappwebview/flutter_inappwebview.dart'; import 'package:permission_handler/permission_handler.dart'; import 'package:path_provider/path_provider.dart'; class InAppTest extends StatefulWidget { const InAppTest({super.key}); @override State<InAppTest> createState() => _InAppTestState(); } class _InAppTestState extends State<InAppTest> { final GlobalKey webviewKey = GlobalKey(); InAppWebViewController? webViewController; InAppWebViewGroupOptions options = InAppWebViewGroupOptions( crossPlatform: InAppWebViewOptions( useShouldOverrideUrlLoading: true, mediaPlaybackRequiresUserGesture: false, ), android: AndroidInAppWebViewOptions(useHybridComposition: true)); late PullToRefreshController pullToRefreshController; String url = 'google.com'; final urlContrller = TextEditingController(); InAppLocalhostServer? localhostServer; @override void initState() { super.initState(); _initLocalServer(); pullToRefreshController = PullToRefreshController( options: PullToRefreshOptions(color: Colors.blue), onRefresh: () async { if (Platform.isAndroid) webViewController?.reload(); }, ); } Future<void> _initLocalServer() async { // 请求存储权限 bool hasPermission = await requestStoragePermission(); if (!hasPermission) { if (mounted) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('需要存储权限才能访问本地文件')), ); } return; } // 获取目标目录并验证存在性 String docRoot = await getTargetDirectory(); Directory targetDir = Directory(docRoot); if (!await targetDir.exists()) { if (mounted) { ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text('目标目录不存在:$docRoot')), ); } return; } // 启动本地服务器 localhostServer = InAppLocalhostServer(documentRoot: docRoot); try { await localhostServer?.start(); if (mounted) setState(() {}); } catch (e) { print('服务器启动失败:$e'); if (mounted) { ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text('服务器启动失败:$e')), ); } } } Future<bool> requestStoragePermission() async { PermissionStatus status; if (Platform.isAndroid) { status = await (await AndroidInfo.version >= 13) ? Permission.photos.request() : Permission.storage.request(); } else { status = await Permission.storage.request(); } return status.isGranted; } Future<String> getTargetDirectory() async { Directory? baseDir; if (Platform.isAndroid) { baseDir = await getExternalStorageDirectory(); baseDir = Directory('${baseDir?.path}/Download/mediplus-lite'); } return baseDir?.path ?? ''; } @override void dispose() { localhostServer?.stop(); super.dispose(); } @override Widget build(BuildContext context) { return Scaffold( body: SafeArea( child: Column( children: [ SizedBox( width: 200, height: 200, child: Stack( children: [ localhostServer != null ? InAppWebView( key: webviewKey, initialUrlRequest: URLRequest( url: Uri.parse('http://127.0.0.1:8080/index.html')), initialOptions: options, pullToRefreshController: pullToRefreshController, onWebViewCreated: (controller) { webViewController = controller; controller.zoomBy(zoomFactor: 1); controller.injectCSSCode(source: 'body{transform: scale(1.0);}'); }, onLoadStart: (controller, url) { setState(() { this.url = url.toString(); urlContrller.text = this.url; }); }, androidOnPermissionRequest: (controller, origin, resources) async { return PermissionRequestResponse( resources: resources, action: PermissionRequestResponseAction.GRANT, ); }, ) : const Center(child: Text('服务器初始化中...')) ], )), TextButton( onPressed: () => webViewController?.zoomBy(zoomFactor: 0.1), child: const Text('Press'), ), ], )), ); } }
错误排查要点
- 确认目标文件夹
/storage/emulated/0/Download/mediplus-lite存在,且包含index.html文件。 - 检查权限是否已成功授予,可通过
Permission.storage.status或对应媒体权限状态验证。 - 查看控制台日志,确认服务器启动时是否有路径不存在、权限不足等异常信息。
内容的提问来源于stack exchange,提问作者jonathan Cunha 4Cubo
相关产品推荐
相关产品推荐

