路径常见问题
Flutter 开发中,路径问题是新手最常遇到的坑之一。不同平台的文件系统差异很大,缓存、数据库、文件各自应该存放在哪里?本文逐一说明。
path_provider 核心目录
path_provider 是 Flutter 官方提供的路径获取插件,几乎所有存储方案都依赖它。
flutter pub add path_provider常用 API 一览
| API | 说明 | 是否会被系统清理 |
|---|---|---|
getTemporaryDirectory() | 临时目录,相当于缓存 | 是 |
getApplicationDocumentsDirectory() | 文档目录,用户可见数据 | 否 |
getApplicationSupportDirectory() | 应用支持目录,用户不可见数据 | 否 |
getExternalStorageDirectory() | 外部存储目录(仅 Android) | 否 |
getDownloadsDirectory() | 下载目录(仅桌面端) | 否 |
getLibraryDirectory() | Library 目录(仅 iOS/macOS) | 否 |
各平台实际路径对照表
| API | Android | iOS | 鸿蒙 | macOS | Windows | Linux |
|---|---|---|---|---|---|---|
| 临时目录 | /data/data/<包名>/cache | NSCachesDirectory | <沙盒>/cache | ~/Library/Caches | %TEMP% | /tmp |
| 文档目录 | /data/data/<包名>/app_flutter | NSDocumentDirectory | <沙盒>/files | ~/Documents | %APPDATA% | ~/.local/share |
| 支持目录 | /data/data/<包名>/files | NSApplicationSupportDirectory | <沙盒>/preferences | ~/Library/Application Support | %APPDATA% | ~/.local/share |
- Android 路径中的
<包名>即pubspec.yaml中的name或android/app/build.gradle中的applicationId,例如com.example.my_app。- 鸿蒙路径中的
<沙盒>指应用沙盒根目录,实际路径形如/data/app/el2/100/base/<包名>/haps/entry/files/,开发者通常无需关心完整路径,通过 API 获取即可。
缓存文件路径
缓存文件应存放在 临时目录(getTemporaryDirectory()),系统可在存储不足时自动清理。
import 'package:path_provider/path_provider.dart';
import 'package:path/path.dart' as p;
Future<String> getCacheFilePath(String filename) async {
final dir = await getTemporaryDirectory();
return p.join(dir.path, filename);
}各方案缓存路径对比
| 方案 | 缓存路径获取方式 | 实际位置(以 Android 为例) |
|---|---|---|
path_provider | getTemporaryDirectory() | /data/data/<包名>/cache |
shared_preferences | 自动管理,无需关心 | /data/data/<包名>/shared_prefs/ |
get_storage | 自动管理,无需关心 | /data/data/<包名>/app_flutter/ |
cached_network_image | 自动管理 | /data/data/<包名>/cache/libCachedImageData/ |
缓存注意事项
- 临时目录会被系统清理:Android 在存储空间不足时可能自动清除缓存;iOS 更激进,系统可能在应用未运行时清理。不要在临时目录存放不可丢失的数据。
- 缓存大小管理:建议自行实现缓存清理逻辑,避免无限增长。
// 清理超过 7 天的缓存文件
Future<void> cleanExpiredCache() async {
final dir = await getTemporaryDirectory();
final now = DateTime.now();
final expireDays = 7;
if (await dir.exists()) {
await for (final entity in dir.list()) {
if (entity is File) {
final stat = await entity.stat();
if (now.difference(stat.modified).inDays > expireDays) {
await entity.delete();
}
}
}
}
}SQLite 数据库文件路径
sqflite 的 getDatabasesPath() 在不同平台上指向不同目录:
import 'package:sqflite/sqflite.dart';
import 'package:path/path.dart' as p;
Future<Database> openMyDatabase() async {
final dbPath = await getDatabasesPath();
return openDatabase(
p.join(dbPath, 'app.db'),
version: 1,
onCreate: (db, version) async {
await db.execute('CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT)');
},
);
}getDatabasesPath() 各平台实际路径
| 平台 | 路径 | 说明 |
|---|---|---|
| Android | /data/data/<包名>/databases/ | 应用私有目录,卸载即删除 |
| iOS | Documents/ 目录下 | 应用沙盒内,卸载即删除 |
| 鸿蒙 | <沙盒>/preferences/rdb/ | 应用沙盒内,卸载即删除 |
| macOS | ~/Library/Application Support/ | - |
| Windows | C:\Users\<用户>\AppData\Roaming\ | - |
| Linux | ~/.local/share/ | - |
Android 外部存储
getDatabasesPath() 返回的是内部存储路径。如果需要将数据库放在外部存储(SD 卡),需手动指定路径,并申请 WRITE_EXTERNAL_STORAGE 权限(Android 10+ 有作用域存储限制,推荐使用内部存储)。
各平台路径差异详解
鸿蒙(HarmonyOS NEXT):应用沙盒
Flutter 从 3.22 版本开始支持鸿蒙平台。鸿蒙 NEXT 采用严格的应用沙盒机制,每个应用只能访问自己沙盒内的目录,与 Android 的内部存储概念类似但更加严格。
鸿蒙沙盒目录结构
| 目录 | 路径(相对于沙盒根) | 说明 | 对应 path_provider API |
|---|---|---|---|
| 沙盒根目录 | /data/app/el2/100/base/<包名>/haps/entry/files/ | 应用私有空间 | - |
| files | <沙盒>/files/ | 应用文件目录 | getApplicationDocumentsDirectory() |
| cache | <沙盒>/cache/ | 缓存目录,系统可清理 | getTemporaryDirectory() |
| preferences | <沙盒>/preferences/ | 偏好设置目录 | getApplicationSupportDirectory() |
| temp | <沙盒>/temp/ | 临时文件目录 | getTemporaryDirectory() |
| rdb | <沙盒>/preferences/rdb/ | 关系型数据库目录 | getDatabasesPath() |
鸿蒙路径注意事项
- 沙盒隔离:鸿蒙应用只能访问自己的沙盒目录,无法像 Android 那样通过权限访问其他应用的目录
- 分布式文件:如需跨设备访问文件,需使用鸿蒙的分布式文件服务,不能直接读写路径
- 用户目录:鸿蒙没有类似 Android 外部存储的"公共目录"概念,用户文件通过系统 Picker 选择
- sqflite 兼容性:鸿蒙平台上
sqflite的getDatabasesPath()返回<沙盒>/preferences/rdb/,数据库文件默认存放在此 - path_provider 支持:Flutter 鸿蒙分支已适配
path_provider,API 用法与其他平台一致
import 'package:path_provider/path_provider.dart';
import 'package:flutter/foundation.dart' show kIsWeb;
// 跨平台获取文档目录(鸿蒙自动映射到沙盒 files 目录)
Future<String> getDataDir() async {
final dir = await getApplicationDocumentsDirectory();
return dir.path;
// Android → /data/data/<包名>/app_flutter/
// iOS → NSDocumentDirectory
// 鸿蒙 → <沙盒>/files/
}鸿蒙与 Android 路径对比
| 场景 | Android | 鸿蒙 |
|---|---|---|
| 应用私有文件 | /data/data/<包名>/files/ | <沙盒>/files/ |
| 缓存 | /data/data/<包名>/cache/ | <沙盒>/cache/ |
| 数据库 | /data/data/<包名>/databases/ | <沙盒>/preferences/rdb/ |
| 偏好设置 | /data/data/<包名>/shared_prefs/ | <沙盒>/preferences/ |
| 外部存储 | /sdcard/Android/data/<包名>/ | 不支持,使用沙盒或系统 Picker |
鸿蒙没有"外部存储"概念,所有应用数据都在沙盒内。如需用户可见的文件,使用系统文件选择器让用户指定保存位置。
Android:内部存储 vs 外部存储
Android 的文件存储是最容易混淆的,搞清以下概念:
| 概念 | 路径 | 特点 | 需要权限 |
|---|---|---|---|
| 内部存储 | /data/data/<包名>/ | 应用私有,卸载删除 | 否 |
| 外部存储-应用专属 | /sdcard/Android/data/<包名>/ | 卸载删除,用户可见 | Android 10+ 不需要 |
| 外部存储-公共目录 | /sdcard/Download/ 等 | 卸载不删除,所有应用共享 | Android 10+ 需 MediaStore API |
import 'package:path_provider/path_provider.dart';
// 内部存储(应用私有)
final internalDir = await getApplicationDocumentsDirectory();
// → /data/data/<包名>/app_flutter/
// 外部存储-应用专属目录(Android 独有)
final externalDir = await getExternalStorageDirectory();
// → /sdcard/Android/data/<包名>/files/Android 10+ 作用域存储
从 Android 10 开始引入作用域存储(Scoped Storage),限制了对公共目录的直接访问。推荐做法:
- 应用私有数据 → 内部存储(
getApplicationDocumentsDirectory) - 需要用户访问的文件 → 使用 MediaStore API 或系统文件选择器
- 避免直接读写
/sdcard/根目录
iOS:沙盒机制
iOS 应用运行在沙盒中,每个应用只能访问自己的目录:
| 目录 | API | 说明 |
|---|---|---|
| Documents | getApplicationDocumentsDirectory() | 用户数据,iTunes 备份 |
| Library/Caches | getTemporaryDirectory() | 缓存,不备份,系统可清理 |
| Library/Application Support | getApplicationSupportDirectory() | 持久数据,iTunes 备份 |
| tmp | 系统临时目录 | 随时可清理 |
iOS 数据备份
- 需要 iTunes/iCloud 备份的数据 →
Documents或Application Support - 可重新下载的缓存 →
Caches或tmp - 大文件(如视频缓存)务必放
Caches,避免备份膨胀
Web:无本地文件系统
Web 平台没有传统文件系统,path_provider 的大多数 API 不可用:
| API | Web 支持 |
|---|---|
getTemporaryDirectory() | ❌ |
getApplicationDocumentsDirectory() | ❌ |
getApplicationSupportDirectory() | ❌ |
Web 平台的替代方案:
import 'package:flutter/foundation.dart' show kIsWeb;
if (kIsWeb) {
// Web: 使用 SharedPreferences(底层是 localStorage)
// 或使用 indexedDB(sqflite 的 Web 实现)
} else {
// 移动端/桌面端: 正常使用文件系统
}桌面端(Windows/macOS/Linux)
桌面端路径遵循各操作系统的惯例,path_provider 提供了完整支持。需要注意:
- macOS 沙盒:如果启用了 App Sandbox,文件访问受限,需要在
com.apple.security.files.user-selected.*声明权限 - Windows:路径使用反斜杠
\,建议始终使用path包的join()拼接路径,避免手动拼接
常见坑与解决方案
1. 手动拼接路径导致跨平台问题
// ❌ 错误:Windows 上分隔符是 \
final path = dir.path + '/data.json';
// ✅ 正确:使用 path 包的 join
import 'package:path/path.dart' as p;
final path = p.join(dir.path, 'data.json');2. 目录不存在导致写入失败
// ❌ 错误:直接写入可能不存在的子目录
final file = File(p.join(dir.path, 'cache', 'data.json'));
await file.writeAsString('{}'); // NoSuchDirectoryException
// ✅ 正确:先创建目录
final cacheDir = Directory(p.join(dir.path, 'cache'));
if (!await cacheDir.exists()) {
await cacheDir.create(recursive: true);
}
final file = File(p.join(cacheDir.path, 'data.json'));
await file.writeAsString('{}');3. Android 上获取外部存储路径返回 null
getExternalStorageDirectory() 在以下情况可能返回 null:
- 模拟器没有挂载 SD 卡
- Android 10+ 作用域存储限制
- 设备没有外部存储
// 安全做法
final externalDir = await getExternalStorageDirectory();
if (externalDir != null) {
// 使用外部存储
} else {
// 回退到内部存储
final internalDir = await getApplicationDocumentsDirectory();
}4. iOS 缓存被清理后应用崩溃
getTemporaryDirectory() 的内容可能被系统清理,每次读取前务必检查文件是否存在:
Future<String?> readCacheFile(String filename) async {
final dir = await getTemporaryDirectory();
final file = File(p.join(dir.path, filename));
if (await file.exists()) {
return await file.readAsString();
}
return null; // 缓存已清理,重新从网络获取
}5. 数据库文件和缓存文件混放
// ❌ 错误:数据库文件放在临时目录
final dbPath = await getTemporaryDirectory(); // 可能被清理!
// ✅ 正确:使用 getDatabasesPath() 或应用文档目录
final dbPath = await getDatabasesPath();
// 或
final dbPath = (await getApplicationDocumentsDirectory()).path;6. Web 平台直接使用文件 API 报错
import 'package:flutter/foundation.dart' show kIsWeb;
Future<void> saveData(String key, String value) async {
if (kIsWeb) {
// Web: 使用 SharedPreferences 或 sembast_web
final prefs = await SharedPreferences.getInstance();
await prefs.setString(key, value);
} else {
// 移动端/桌面端: 使用文件系统
final dir = await getApplicationDocumentsDirectory();
final file = File(p.join(dir.path, '$key.dat'));
await file.writeAsString(value);
}
}7. 鸿蒙平台使用 Android 路径逻辑报错
鸿蒙平台的文件系统与 Android 不同,不要在鸿蒙上硬编码 Android 路径:
// ❌ 错误:硬编码 Android 路径
final dbPath = '/data/data/$packageName/databases/';
// ✅ 正确:使用 path_provider API,自动适配各平台
final dbPath = await getDatabasesPath();
// Android → /data/data/<包名>/databases/
// 鸿蒙 → <沙盒>/preferences/rdb/鸿蒙平台特有的注意事项:
- 不支持
getExternalStorageDirectory(),调用会返回null - 不支持直接访问"公共目录"(如 Download、Pictures),需使用系统 Picker
SharedPreferences底层映射到<沙盒>/preferences/目录
路径选择速查表
| 数据类型 | 推荐 API | 是否备份 | 是否可被清理 |
|---|---|---|---|
| 用户文档、数据库 | getApplicationDocumentsDirectory() | 是 | 否 |
| 应用配置、隐藏数据 | getApplicationSupportDirectory() | 是 | 否 |
| 图片/文件缓存 | getTemporaryDirectory() | 否 | 是 |
| SQLite 数据库 | getDatabasesPath() | 是 | 否 |
| SharedPreferences | 自动管理 | 是 | 否 |
| Android 外部文件 | getExternalStorageDirectory() | 否 | 否 |
简单原则:不可丢失的数据放文档目录,可重新获取的放缓存目录,数据库用
getDatabasesPath()。鸿蒙没有外部存储概念,所有数据都在沙盒内。
