跳转到正文
YGJoon 技术博客

Flutter 文件操作详解

发布于:
·1.6k 字 · 约 4 分钟·作者:
path_provider 拿文档目录、临时目录、外部存储:文本读写、二进制读写、复制移动删除,配 try-catch 别让 IO 异常崩了应用
Flutter 文件操作详解文章封面
目录

简介

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();
  }
}

最佳实践

  1. 写文件先写临时文件、成功后 rename 覆盖:App 在 writeAsString 中途被杀,直接写目标文件会留下半截文件;临时文件加原子重命名,保证”要么是旧文件、要么是完整新文件”,没有中间态。
  2. 读文件前先 exists() 判断:首次启动文件不存在是正常分支,不是异常,别指望 try-catch 兜一切——把”文件不存在”当异常打日志,日志里全是噪音。
  3. 几 MB 以上的大文件读写用异步 API,别碰 readAsBytesSync 这类同步版本:同步 API 会卡住 UI 线程,用户看到的就是界面冻住几秒。
  4. 缓存和用户数据分目录:临时文件放 getTemporaryDirectory(系统缺空间时会清),用户文档放 getApplicationDocumentsDirectory;放反了,轻则缓存占着备份空间,重则系统把你的重要文件清掉。

注意事项

  1. Android 10+ 分区存储:getExternalStorageDirectory 拿到的是应用专属目录,无需申请存储权限;想写公共相册或 Download 目录得走 MediaStore/SAF,别再按老教程申请 WRITE_EXTERNAL_STORAGE——新系统上它已经名存实亡。
  2. iOS 上 Documents 目录默认进 iCloud 备份:几十 MB 的缓存、日志别放这里,会撑爆用户 iCloud 空间,审核也可能被问;大缓存放 Library/Caches。
  3. 并发写同一文件会互相覆盖:两个地方同时 writeAsString 同一个文件,结果不可预期;有并发写的场景就收敛到一个 FileManager 队列里串行。
  4. 外部存储(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,可以实现各种文件存储需求。在使用时需要注意异常处理和性能优化。


分享这篇文章:


相关文章

上一篇
Flutter 主题切换实现详解
下一篇
Flutter 单元测试详解