In some cases, it can be handy to read and write files to disk.This can be used to persist data across app launches,or to download data from the internet and save it for later offline use.

In order to save files to disk, you’ll need to combine thepath_provider plugin withthe dart:iolibrary.

Directions

  • Find the correct local path
  • Create a reference to the file location
  • Write data to the file
  • Read data from the file

1. Find the correct local path

In this example, you’ll display a counter. When the counter changes, you’llwrite data on disk so you can read it again when the app loads.Therefore, you must wonder: Where should I store this data?

The path_provider pluginprovides a platform-agnostic way to access commonly used locations on thedevice’s filesystem. The plugin currently supports access to two filesystemlocations:

  • Temporary directory: A temporary directory (cache) that the system canclear at any time. On iOS, this corresponds to the value thatNSTemporaryDirectory()returns. On Android, this is the value thatgetCacheDir())returns.
  • Documents directory: A directory for the app to store files that onlyit can access. The system clears the directory only when the app is deleted.On iOS, this corresponds to NSDocumentDirectory. On Android, this is theAppData directory.In this case, you’ll want to store information in the documents directory.You can find the path to the documents directory as follows:
  1. Future<String> get _localPath async {
  2. final directory = await getApplicationDocumentsDirectory();
  3. return directory.path;
  4. }

2. Create a reference to the file location

Once you know where to store the file, you’ll need to create a reference to thefile’s full location. You can use theFileclass from the dart:iolibrary to achieve this.

  1. Future<File> get _localFile async {
  2. final path = await _localPath;
  3. return File('$path/counter.txt');
  4. }

3. Write data to the file

Now that you have a File to work with, use it to read and write data.First, write some data to the file. Since you’re working with a counter,you’ll simply store the integer as a String.

  1. Future<File> writeCounter(int counter) async {
  2. final file = await _localFile;
  3. // Write the file
  4. return file.writeAsString('$counter');
  5. }

4. Read data from the file

Now that you have some data on disk, you can read it.Once again, use the File class.

  1. Future<int> readCounter() async {
  2. try {
  3. final file = await _localFile;
  4. // Read the file
  5. String contents = await file.readAsString();
  6. return int.parse(contents);
  7. } catch (e) {
  8. // If encountering an error, return 0
  9. return 0;
  10. }
  11. }

Testing

In order to test code that interacts with files, you’ll need to Mock calls tothe MethodChannel. The MethodChannel is the class that Flutter uses tocommunicate with the host platform.

In these tests, you can’t interact with the filesystem on a device.You’ll need to interact with the test environment’s filesystem.

To mock the method call, provide a setupAll function in the test file.This function runs before the tests are executed.

  1. setUpAll(() async {
  2. // Create a temporary directory to work with
  3. final directory = await Directory.systemTemp.createTemp();
  4. // Mock out the MethodChannel for the path_provider plugin
  5. const MethodChannel('plugins.flutter.io/path_provider')
  6. .setMockMethodCallHandler((MethodCall methodCall) async {
  7. // If you're getting the apps documents directory, return the path to the
  8. // temp directory on the test environment instead.
  9. if (methodCall.method == 'getApplicationDocumentsDirectory') {
  10. return directory.path;
  11. }
  12. return null;
  13. });
  14. });

Complete example

  1. import 'dart:async';
  2. import 'dart:io';
  3. import 'package:flutter/foundation.dart';
  4. import 'package:flutter/material.dart';
  5. import 'package:path_provider/path_provider.dart';
  6. void main() {
  7. runApp(
  8. MaterialApp(
  9. title: 'Reading and Writing Files',
  10. home: FlutterDemo(storage: CounterStorage()),
  11. ),
  12. );
  13. }
  14. class CounterStorage {
  15. Future<String> get _localPath async {
  16. final directory = await getApplicationDocumentsDirectory();
  17. return directory.path;
  18. }
  19. Future<File> get _localFile async {
  20. final path = await _localPath;
  21. return File('$path/counter.txt');
  22. }
  23. Future<int> readCounter() async {
  24. try {
  25. final file = await _localFile;
  26. // Read the file
  27. String contents = await file.readAsString();
  28. return int.parse(contents);
  29. } catch (e) {
  30. // If encountering an error, return 0
  31. return 0;
  32. }
  33. }
  34. Future<File> writeCounter(int counter) async {
  35. final file = await _localFile;
  36. // Write the file
  37. return file.writeAsString('$counter');
  38. }
  39. }
  40. class FlutterDemo extends StatefulWidget {
  41. final CounterStorage storage;
  42. FlutterDemo({Key key, @required this.storage}) : super(key: key);
  43. @override
  44. _FlutterDemoState createState() => _FlutterDemoState();
  45. }
  46. class _FlutterDemoState extends State<FlutterDemo> {
  47. int _counter;
  48. @override
  49. void initState() {
  50. super.initState();
  51. widget.storage.readCounter().then((int value) {
  52. setState(() {
  53. _counter = value;
  54. });
  55. });
  56. }
  57. Future<File> _incrementCounter() {
  58. setState(() {
  59. _counter++;
  60. });
  61. // Write the variable as a string to the file.
  62. return widget.storage.writeCounter(_counter);
  63. }
  64. @override
  65. Widget build(BuildContext context) {
  66. return Scaffold(
  67. appBar: AppBar(title: Text('Reading and Writing Files')),
  68. body: Center(
  69. child: Text(
  70. 'Button tapped $_counter time${_counter == 1 ? '' : 's'}.',
  71. ),
  72. ),
  73. floatingActionButton: FloatingActionButton(
  74. onPressed: _incrementCounter,
  75. tooltip: 'Increment',
  76. child: Icon(Icons.add),
  77. ),
  78. );
  79. }
  80. }