一、path_provider

path_provider 是 flutter 提供的用于进行文件存储的 package

本质上是封装了 Android 和 iOS Native 方法去进行读写磁盘,读写分为两种场景:

  • 临时文件夹
  • 应用的Documents 目录

临时文件夹在执行系统的清空缓存时会清空掉该文件夹,documents 目录则是随着应用创建而创建,只有在删除应用时,才会清空这个目录。

临时文件夹在 iOS 上对用的是 NSCachesDirectory 在 Android 对用的是 getCacheDir()

而 Documents 目录在 iOS 对应的是 NSDocumentDirectory ,在 Android 上对应的是 AppData 目录

本身 package_provider 提供了三个方法分别获取不同的文件路径

1、getTemporaryDirectory

获取设备上的临时目录,也就是上面提到的 临时文件夹

2、getApplicationDocumentsDirectory

获取应用的 document 目录

3、getExternalStorageDirectory

这个方法可以修改顶级存储也就是系统级别的存储路径,但是只有在 android 上生效,因为 iOS 的 sandbox Application 执行特性,不允许修改顶级存储。

因此如果在 iOS 上使用会抛出异常

使用这个方法需要额外注意

二、使用 path_provider 的 getApplicationDocumentsDirectory

无论是获取临时 cache 路径还是 documents 文件夹,都是异步的方法,并且返回的都是 Directory 类型

Directory 是一个抽象类,其中实现了 get 属性 path:

@pragma("vm:entry-point")
abstract class Directory implements FileSystemEntity {
  /**
   * Gets the path of this directory.
   */
  String get path;

因此返回的 Directory 需要通过 Directory.path 拿到具体的路径

1、获取文件夹路径

最终我们要在 document 中访问一个文件,首先需要知道路径是什么,因此每次我们都需要调用 await getApplicationDocumentsDirectory() 拿到文件夹的路径。

这里通过 一个 get 属性 _localPath 获取,本质上还是每次调用方法获取数据,当然可以通过属性判断是否存在值,还是异步方法返回

  Future<String> get _localPath async {
    final directory = await getApplicationDocumentsDirectory();
    print(directory.path);
    return directory.path;
  }

2、获取文件实例

一个文件总是通过 File() 创建出实例来,File 的工厂方法如下:

  @pragma("vm:entry-point")
  factory File(String path) {
    final IOOverrides overrides = IOOverrides.current;
    if (overrides == null) {
      return new _File(path);
    }
    return overrides.createFile(path);
  }

但我们拿到文件路径之后,就可以通过 File 拿到文件实例,然后通过文件实例做其他事情。

下面代码中,首先通过一个 _file存储数据,如果 _file 并不是 File 类型,在通过 File('$path/counter.txt') 实例化文件实例

  Future<File> get _localFile async {
    if (_file is File) {
      return _file;
    } else {
      final String path = await _localPath;
      _file = File('$path/counter.txt');
      return _file;
    }
  }

3、读取文件内容

读取文件的内容。首先需要拿到文件实例,然后 File 提供了两种读取形式,分别是 readAsBytesreadAsString,表示字节读取和字符串读取。

每种读取形式包含了异步和同步方法,分别是:

  • readAsBytes
  • readAsBytesSync
  • readAsString
  • readAsStringSync

下面的方法中,读取内容作为字符串,并且将其转成 int 类型:

  _initCounter() async {
    final File file = await _localFile;
    final String res = await file.readAsString();
    setState(() {
      _counter = int.parse(res ?? 0);
    });
  }

4、写入文件内容

同样的写入文件内容也提供了 字节和字符串两种形式,并且提供了同步和异步

  • writeAsBytes
  • writeAsBytesSync
  • writeAsString
  • writeAsStringSync

下面方法中,讲一个 int 类型数据,转成 String 然后写入到文件中

  _saveCounter() async {
    final File file = await _localFile;
    file.writeAsString(_counter.toString());
  }

三、文件读写实践

基于文件存储,创建一个计数应用

1、首页进行路由导航

如果没有任何持久化数据的策略,每次进去 counter 都是置空,这并不是我们期望的结果,这里刻意做了个首页的导航

class HomeDemo extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Center(
      child: OutlineButton(
        child: Text('go to counter'),
        onPressed: () => {
          Navigator.of(context).push(MaterialPageRoute(builder:(context) => CounterDemo()))
        },
      ),
    );
  }
}

2、一个页面承载 counter 计数示例

class CounterDemo extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('counter')
      ),
      body: FileIODemo()
    );
  }
}

3、最终持久化计数示例的实现

class FileIODemo extends StatefulWidget {
  FileIODemo({Key key}) : super(key: key);

  _FileIODemoState createState() => _FileIODemoState();
}

class _FileIODemoState extends State<FileIODemo> {
  int _counter = 0;
  File _file;

  _FileIODemoState() {
    _initFile();
  }
  @override
  void initState() {
    super.initState();
    _initCounter();
  }

  _initCounter() async {
    final File file = await _localFile;
    final String res = await file.readAsString();
    setState(() {
      _counter = int.parse(res ?? 0);
    });
  }

  _initFile() async {
    _file = await _localFile;
  }

  Future<String> get _localPath async {
    final directory = await getApplicationDocumentsDirectory();
    print(directory.path);
    return directory.path;
  }

  Future<File> get _localFile async {
    if (_file is File) {
      return _file;
    } else {
      final String path = await _localPath;
      _file = File('$path/counter.txt');
      return _file;
    }
  }
  _saveCounter() async {
    final File file = await _localFile;
    file.writeAsString(_counter.toString());
  }


  @override
  Widget build(BuildContext context) {
    return Column(
      children: <Widget>[
        SizedBox(height: 20),
        Row(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            Text(_counter.toString()),
          ],
        ),
        SizedBox(height: 20),
        Row(
          mainAxisAlignment: MainAxisAlignment.spaceAround,
          children: <Widget>[
            RaisedButton(
              child: Text('-'),
              onPressed: () {
                setState(() {
                  _counter -= 1;
                  _saveCounter();
                });
              },
            ),
            RaisedButton(
              child: Text('+'),
              onPressed: () {
                setState(() {
                  _counter += 1;
                  _saveCounter();
                });
              },
            ),
          ],
        )
      ],
    );
  }
}

四、效果

11111.gif

五、在线代码

https://github.com/postbird/FlutterHelloWorldDemo/blob/master/demo1/lib/bak/main.55-io%20%E6%96%87%E4%BB%B6%E8%AF%BB%E5%86%99.dart