You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.06 07:33:27