简介
Flutter 提供了完整的文件操作 API,可以实现文件的读写、复制、移动、删除等操作。本文详细介绍文件操作的各种用法。
基本配置
添加依赖
dependencies:
path_provider: ^2.1.1
获取路径
// 获取应用文档目录
final directory = await getApplicationDocumentsDirectory();
// 获取临时目录
final tempDir = await getTemporaryDirectory();
// 获取外部存储目录(仅 Android)
final extDir = await getExternalStorageDirectory();
基本操作
文件读写
Future<void> writeFile(String content) async {
final directory = await getApplicationDocumentsDirectory();
final file = File('${directory.path}/my_file.txt');
try {
await file.writeAsString(content);
print('File written successfully');
} catch (e) {
print('Error writing file: $e');
}
}
Future<String> readFile() async {
final directory = await getApplicationDocumentsDirectory();
final file = File('${directory.path}/my_file.txt');
try {
String contents = await file.readAsString();
return contents;
} catch (e) {
print('Error reading file: $e');
return '';
}
}
二进制操作
Future<void> writeBinaryFile(List<int> bytes) async {
final directory = await getApplicationDocumentsDirectory();
final file = File('${directory.path}/binary_file');
try {
await file.writeAsBytes(bytes);
print('Binary file written successfully');
} catch (e) {
print('Error writing binary file: $e');
}
}
Future<List<int>> readBinaryFile() async {
final directory = await getApplicationDocumentsDirectory();
final file = File('${directory.path}/binary_file');
try {
List<int> bytes = await file.readAsBytes();
return bytes;
} catch (e) {
print('Error reading binary file: $e');
return [];
}
}
完整示例
class FileManager {
static final FileManager _instance = FileManager._internal();
factory FileManager() => _instance;
FileManager._internal();
Future<String> get _localPath async {
final directory = await getApplicationDocumentsDirectory();
return directory.path;
}
Future<File> _localFile(String filename) async {
final path = await _localPath;
return File('$path/$filename');
}
// 写入文本文件
Future<void> writeTextFile(
String filename,
String content,
) async {
try {
final file = await _localFile(filename);
await file.writeAsString(content);
} catch (e) {
throw Exception('Could not write file: $e');
}
}
// 读取文本文件
Future<String> readTextFile(String filename) async {
try {
final file = await _localFile(filename);
return await file.readAsString();
} catch (e) {
throw Exception('Could not read file: $e');
}
}
// 写入二进制文件
Future<void> writeBinaryFile(
String filename,
List<int> bytes,
) async {
try {
final file = await _localFile(filename);
await file.writeAsBytes(bytes);
} catch (e) {
throw Exception('Could not write binary file: $e');
}
}
// 读取二进制文件
Future<List<int>> readBinaryFile(String filename) async {
try {
final file = await _localFile(filename);
return await file.readAsBytes();
} catch (e) {
throw Exception('Could not read binary file: $e');
}
}
// 检查文件是否存在
Future<bool> exists(String filename) async {
try {
final file = await _localFile(filename);
return await file.exists();
} catch (e) {
return false;
}
}
// 删除文件
Future<void> deleteFile(String filename) async {
try {
final file = await _localFile(filename);
if (await file.exists()) {
await file.delete();
}
} catch (e) {
throw Exception('Could not delete file: $e');
}
}
// 复制文件
Future<void> copyFile(
String sourceFilename,
String targetFilename,
) async {
try {
final sourceFile = await _localFile(sourceFilename);
final targetFile = await _localFile(targetFilename);
await sourceFile.copy(targetFile.path);
} catch (e) {
throw Exception('Could not copy file: $e');
}
}
// 获取文件大小
Future<int> getFileSize(String filename) async {
try {
final file = await _localFile(filename);
return await file.length();
} catch (e) {
throw Exception('Could not get file size: $e');
}
}
}
// 使用示例
class FileOperationDemo extends StatefulWidget {
@override
_FileOperationDemoState createState() => _FileOperationDemoState();
}
class _FileOperationDemoState extends State<FileOperationDemo> {
final fileManager = FileManager();
final textController = TextEditingController();
String _content = '';
Future<void> _saveContent() async {
if (textController.text.isEmpty) return;
try {
await fileManager.writeTextFile(
'notes.txt',
textController.text,
);
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Content saved successfully')),
);
} catch (e) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Error saving content: $e')),
);
}
}
Future<void> _loadContent() async {
try {
final content = await fileManager.readTextFile('notes.txt');
setState(() {
_content = content;
textController.text = content;
});
} catch (e) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Error loading content: $e')),
);
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('File Operations')),
body: Padding(
padding: EdgeInsets.all(16.0),
child: Column(
children: [
TextField(
controller: textController,
maxLines: 5,
decoration: InputDecoration(
border: OutlineInputBorder(),
hintText: 'Enter content to save',
),
),
SizedBox(height: 16),
Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
ElevatedButton(
onPressed: _saveContent,
child: Text('Save'),
),
ElevatedButton(
onPressed: _loadContent,
child: Text('Load'),
),
],
),
SizedBox(height: 16),
Text('Loaded content:'),
SizedBox(height: 8),
Text(_content),
],
),
),
);
}
@override
void dispose() {
textController.dispose();
super.dispose();
}
}
最佳实践
- 写文件先写临时文件、成功后
rename覆盖:App 在writeAsString中途被杀,直接写目标文件会留下半截文件;临时文件加原子重命名,保证”要么是旧文件、要么是完整新文件”,没有中间态。 - 读文件前先
exists()判断:首次启动文件不存在是正常分支,不是异常,别指望try-catch兜一切——把”文件不存在”当异常打日志,日志里全是噪音。 - 几 MB 以上的大文件读写用异步 API,别碰
readAsBytesSync这类同步版本:同步 API 会卡住 UI 线程,用户看到的就是界面冻住几秒。 - 缓存和用户数据分目录:临时文件放
getTemporaryDirectory(系统缺空间时会清),用户文档放getApplicationDocumentsDirectory;放反了,轻则缓存占着备份空间,重则系统把你的重要文件清掉。
注意事项
- Android 10+ 分区存储:
getExternalStorageDirectory拿到的是应用专属目录,无需申请存储权限;想写公共相册或 Download 目录得走 MediaStore/SAF,别再按老教程申请WRITE_EXTERNAL_STORAGE——新系统上它已经名存实亡。 - iOS 上 Documents 目录默认进 iCloud 备份:几十 MB 的缓存、日志别放这里,会撑爆用户 iCloud 空间,审核也可能被问;大缓存放
Library/Caches。 - 并发写同一文件会互相覆盖:两个地方同时
writeAsString同一个文件,结果不可预期;有并发写的场景就收敛到一个FileManager队列里串行。 - 外部存储(SD 卡)可能被拔出:读写前检查目录可用性,别假设路径永远存在——
exists()返回 false 时给用户一句人话提示,而不是直接崩。
实战案例:路径拼接别手拼字符串
flutter_brightness 是个 Windows 桌面应用,依赖里有 path 包。一开始写日志和缓存路径时,我图省事直接字符串拼接:'$dir/logs/app.log'。在 Windows 上跑了几个月都没事,直到有次目录名里多了层嵌套,我才意识到隐患:手拼字符串默认是 /,Windows API 大多能兼容,但一旦路径要传给串口库、注册表写入或外部进程,斜杠方向就可能出幺蛾子。改成 path 包的 join() 之后,跨平台的拼接逻辑就不用自己操心了。trade-off 很简单:path 包零成本、无侵入,但”手拼字符串在 Windows 上大部分时候能跑”这个错觉,会让你在排查诡异 bug 时多花一下午。我的原则是:凡是落盘的路径,一律走 path_provider 取目录 + path 包拼接,哪怕当时只跑 Windows。桌面端和移动端最大的区别就是用户环境千奇百怪,路径这种基础东西别赌运气。
总结
Flutter 提供了完整的文件操作 API,通过合理使用这些 API,可以实现各种文件存储需求。在使用时需要注意异常处理和性能优化。
