DEV Community

Cover image for File System Management in Dart
Mathieu Kerjouan
Mathieu Kerjouan

Posted on

File System Management in Dart

It's a good time to also investigate a bit the dart I/O interfaces, especially the one related to files. The dart:io package must be imported to deal with files and directories in Dart.

import 'dart:io';
Enter fullscreen mode Exit fullscreen mode

A file in Dart is an object instantiated by the File class. This object can be created by simply invoking the default constructor, where its argument will be a String.

// default constructor can be used
// to create a new object pointing to
// a local file.
File myFile = File("./file1.test");
Enter fullscreen mode Exit fullscreen mode

fromRawPath() is another constructor, it opens a file based on an List<Uint8> (Uint8List), this kind of data is usually generated by utf8.encode or ascii.encode.

// fromRawPath constructor can be used
// to open a file based on a raw path,
// an Uint8list.
File myFile2 = File.fromRawPath(
  ascii.encode("./file2.test");
);
Enter fullscreen mode Exit fullscreen mode

Finally, the fromUri() constructor will open a file based on an Uri object.

// fromUri is another construct that
// can be used to open a file based on
// an Uri.
File myFile3 = File.fromUri(
  Uri.file("./file3.test")
);
Enter fullscreen mode Exit fullscreen mode

Now the file object has been created, many attributes and methods are available to control it. Let check first the attributes. In the previous examples, all objects are using a relative path, when the object is returned, the absolute path attribute is set. It is the absolute path representation of the data previously passed.

print(myFile1.absolute);
print(myFile2.absolute);
print(myFile3.absolute);
Enter fullscreen mode Exit fullscreen mode
$ dart run
File: '/home/user/tmp/cboring/./file1.test'
File: '/home/user/tmp/cboring/./file2.test'
File: '/home/user/tmp/cboring/file3.test'
Enter fullscreen mode Exit fullscreen mode

The original path passed as first argument can be retrieved with the path attribute.

print(myFile1.path);
print(myFile2.path);
print(myFile3.path);
Enter fullscreen mode Exit fullscreen mode
$ dart run
./file1.test
./file2.test
file3.test
Enter fullscreen mode Exit fullscreen mode

The file object can also returns an Uri object with the help of the uri attribute.

// this is a closure helper to show
// the properties from an Uri object and
// return them as a Map.
final toMap = (Uri uri) {
  return {
    "scheme": uri.scheme,
    "host": uri.host,
    "path": uri.path,
    "fragment": uri.fragment,
  };
 };

print(toMap(myFile1.uri));
print(toMap(myFile2.uri));
print(toMap(myFile3.uri));
Enter fullscreen mode Exit fullscreen mode
$ dart run
{scheme: , host: , path: file1.test, fragment: }
{scheme: , host: , path: file2.test, fragment: }
{scheme: , host: , path: file3.test, fragment: }
Enter fullscreen mode Exit fullscreen mode

Another interesting property exposed by the File object is the parent one (usually a directory).

print(myFile1.parent);
print(myFile2.parent);
print(myFile3.parent);
Enter fullscreen mode Exit fullscreen mode
$ dart run
Directory: '.'
Directory: '.'
Directory: '.'
Enter fullscreen mode Exit fullscreen mode

Let creates a small closure to summarize and display all those information into a Map.

final fileToMap = (File file) {
  return {
    'absolute': file.absolute,
    'hashCode': file.hashCode,
    'isAbsolute': file.isAbsolute,
    'parent': file.parent,
    'path': file.path,
    'runtimeType': file.runtimeType,
    'uri': toMap(file.uri),
  };
};

print(fileToMap(myFile1));
print(fileToMap(myFile2));
print(fileToMap(myFile3));
Enter fullscreen mode Exit fullscreen mode
{absolute: File: '/home/user/tmp/cboring/./file1.test', hashCode: 554905050, isAbsolute: false, parent: Directory: '.', path: ./file1.test, runtimeType: _File, uri: {scheme: , host: , path: file1.test, fragment: }}
{absolute: File: '/home/user/tmp/cboring/./file2.test', hashCode: 224769083, isAbsolute: false, parent: Directory: '.', path: ./file2.test, runtimeType: _File, uri: {scheme: , host: , path: file2.test, fragment: }}
{absolute: File: '/home/user/tmp/cboring/file3.test', hashCode: 467382617, isAbsolute: false, parent: Directory: '.', path: file3.test, runtimeType: _File, uri: {scheme: , host: , path: file3.test, fragment: }}
Enter fullscreen mode Exit fullscreen mode

The next part is to investigate around the methods available. One of the most important is probably the exists() method, which returns if a file exist or not. Another one called existsSync() is also available, and will return a boolean instead of a Future because of its synchronous nature. In fact, most of the following methods will also have their synchronous function if needed. Let creates two of those files first, file1.test and file2.test.

$ touch file{1,2}.test

$ dd if=/dev/random of=file1.test count=1024 bs=8
1024+0 records in
1024+0 records out
8192 bytes (8.2 kB, 8.0 KiB) copied, 0.00502157 s, 1.6 MB/s

$ dd if=/dev/random bs=8 count=1024 \
   | perl -ane '
     BEGIN{@s}; 
     map {push(@s, $_)} 
         grep {$_=~/^[a-zA-Z0-9]$/}
         split(//, $_); 
     if (@s>64) {
       $line = join("", @s[0..64]);
       print $line."\n";
       @s=(); 
     }' \
   > file2.test
1024+0 records in
1024+0 records out
8192 bytes (8.2 kB, 8.0 KiB) copied, 0.0112288 s, 730 kB/s
Enter fullscreen mode Exit fullscreen mode

The previous command is maybe a bit hard to understand for someone without Perl experience. In short, we are filtering the output from /dev/random to extract only characters matching the alphabets or numbers. Using other commands would have do the work as well, but I wanted to do a bit of Perl today.

print(myFile1.existsSync());
print(await myFile1.exists());

print(myFile2.existsSync());
print(await myFile2.exists());

print(myFile3.existsSync());
print(await myFile3.exists());
Enter fullscreen mode Exit fullscreen mode
$ dart run
true
true
true
true
false
false
Enter fullscreen mode Exit fullscreen mode

Now few files have been created, one using binaries and another one using text-like random data, it will be interesting to check the next methods. Let check the length() method, it should return the size of a file.

final fileSize = (File file) async {
  try {
    final size = await file.length();
    print("$file.path: $size");
  }
  catch (e) {
    print("$file.path: $e");
  }
};
fileSize(myFile1);
fileSize(myFile2);
fileSize(myFile3);
Enter fullscreen mode Exit fullscreen mode
$ dart run
File: './file1.test'.path: 8192
File: './file2.test'.path: 1122
File: 'file3.test'.path: PathNotFoundException: Cannot retrieve length of file, path = 'file3.test' (OS Error: No such file or directory, errno = 2)
Enter fullscreen mode Exit fullscreen mode

When a file does not exist, the function throw a PathNotFoundException exception. Then, it's important to check the existence of the file before executing these methods. Let do the same test with the stat() method. It should return a FileStat object.

final fileStat = (File file) async {
  try {
    final stat = await file.stat();
    print({
      'accessed': stat.accessed,
      'changed': stat.changed,
      'mode': stat.mode,
      'modified': stat.modified,
      'size': stat.size,
      'type': stat.type,
    });
  }
  catch (e) {
    print("$file.path: $e");
  }
};
fileStat(myFile1);
fileStat(myFile2);
fileStat(myFile3);
Enter fullscreen mode Exit fullscreen mode
$ dart run
{accessed: 2026-08-31 14:15:14.163, changed: 2026-08-31 14:15:14.163, mode: 33204, modified: 2026-08-31 14:15:14.163, size: 8192, type: file}
{accessed: 2026-08-31 14:15:40.139, changed: 2026-08-31 14:15:40.139, mode: 33204, modified: 2026-08-31 14:15:40.139, size: 1122, type: file}
{accessed: 1970-01-01 00:00:00.000Z, changed: 1970-01-01 00:00:00.000Z, mode: 0, modified: 1970-01-01 00:00:00.000Z, size: -1, type: notFound}
Enter fullscreen mode Exit fullscreen mode

Interesting. When executing the stat() method, no exceptions are thrown. I would expect the same behavior than the length() method, but it seems it's not the case. More investigation is required to know why it does that, but the object returned got a notFound type. My guess is, if you are lazy to check for the exceptions, using the stat() looks to be the right way. Our file3.test still does not exist, let create it using the create() method and check what will happen for the 2 others files already created.

final fileCreate = (File file) async {
  try {
    final created = await file.create();
    print(created);
  }
  catch (e) {
    print("$file.path: $e");
  }
};

fileCreate(myFile1);
fileCreate(myFile2);
fileCreate(myFile3);
Enter fullscreen mode Exit fullscreen mode
$ dart run
File: './file1.test'
File: './file2.test'
File: 'file3.test'
Enter fullscreen mode Exit fullscreen mode

When a file is already present on the file system, calling the create() does nothing apparently. Now all these files are created and contains some data (or not), it's the moment to open() them.

print(await myFile1.open());
print(await myFile2.open());
print(await myFile3.open());
Enter fullscreen mode Exit fullscreen mode
$ dart run
Instance of '_RandomAccessFile'
Instance of '_RandomAccessFile'
Instance of '_RandomAccessFile'
Enter fullscreen mode Exit fullscreen mode

It returns a RandomAccessFile object, we will talk about it later, but with this object, a file can be closed via the close(). A good practice is to always ensure an opened file is closed in some way when it is not used anymore to avoid file descriptor exhaustion. Anyway, this returned object is important if you have operations to execute in a long living process. How to deal with this object will be the topic of another article. For now, reading or writing data in one single step will be enough.

The readAsBytes(), readAsLines(https://api.dart.dev/dart-io/FileSystemEntity/readAsLines.html) and readAsString() have been created for this purpose, read a whole file, return its content and automatically close it.

The readAsBytes() method returns a List<Uint8>, perfect to deal with myFile1 containing binary data. The readAsString() method returns a String, perfect to read the content of myFile2 containing alphanumerical characters. The readAsLines() method can also be used, it will return a List<String>, each lines being separated by \n.

print("myFile1:");
print((await myFile1.readAsBytes()).take(10));
print("myFile2:");
print((await myFile2.readAsString()).substring(0,10));
print("myFile2:");
print((await myFile2.readAsLines()).take(2));
Enter fullscreen mode Exit fullscreen mode
$ dart run
myFile1:
(193, 10, 109, 2, 103, 107, 37, 10, 146, 214)
myFile2:
n2ZBmrSIql
myFile2:
(n2ZBmrSIqlF6M0P0JEMN3SKpR4kn1vGXKBH4KVNJK8x6cfvzZ8o9oc6f70kpmCNQn, X86yRGL4f2xJi0o4sbIWOWWZikO09dTNwC9Pt9ltOHLqByFLc847YeGk6gXlSDuGc)
Enter fullscreen mode Exit fullscreen mode

The same methods exist to write data to a file, with the writeAsBytes() and writeAsString() ones. To avoid overwriting the data present in myFile1 and myFile2, some data will be added to myFile3, the only empty file.

await myFile3.writeAsBytes(ascii.encode("new data added"));
print(await myFile3.readAsString());

await myFile3.writeAsBytes([0x41, 0x42, 0x43]);
print(await myFile3.readAsString());

await myFile3.writeAsString("this is a string");
print(await myFile3.readAsString());
Enter fullscreen mode Exit fullscreen mode
$ dart run
new data added
ABC
this is a string
Enter fullscreen mode Exit fullscreen mode

A file can also be copied with the copy() method by defining the path of a new file as first argument.

File myFile1Copy = await myFile1.copy(myFile1.path + ".copy");
print(fileToMap(myFile1Copy));
fileStat(myFile1Copy);

File myFile2Copy = await myFile2.copy(myFile2.path + ".copy");
print(fileToMap(myFile2Copy));
fileStat(myFile2Copy);

File myFile3Copy = await myFile3.copy(myFile3.path + ".copy");
print(fileToMap(myFile3Copy));
fileStat(myFile3Copy);
Enter fullscreen mode Exit fullscreen mode
$ dart run
{absolute: File: '/home/user/tmp/cboring/./file1.test.copy', hashCode: 934827110, isAbsolute: false, parent: Directory: '.', path: ./file1.test.copy, runtimeType: _File, uri: {scheme: , host: , path: file1.test.copy, fragment: }}
{accessed: 2026-09-01 03:57:26.553, changed: 2026-09-01 03:58:34.207, mode: 33204, modified: 2026-09-01 03:58:34.207, size: 8192, type: file}
{absolute: File: '/home/user/tmp/cboring/./file2.test.copy', hashCode: 1050809529, isAbsolute: false, parent: Directory: '.', path: ./file2.test.copy, runtimeType: _File, uri: {scheme: , host: , path: file2.test.copy, fragment: }}
{accessed: 2026-09-01 03:57:26.553, changed: 2026-09-01 03:58:34.235, mode: 33204, modified: 2026-09-01 03:58:34.235, size: 1122, type: file}
{absolute: File: '/home/user/tmp/cboring/file3.test.copy', hashCode: 604940007, isAbsolute: false, parent: Directory: '.', path: file3.test.copy, runtimeType: _File, uri: {scheme: , host: , path: file3.test.copy, fragment: }}
{accessed: 2026-09-01 03:57:26.585, changed: 2026-09-01 03:58:34.235, mode: 33204, modified: 2026-09-01 03:58:34.235, size: 16, type: file}

$ ls -l *.test *.copy
-rw-rw-r-- 1 user user 8192 Aug 31 14:15 file1.test
-rw-rw-r-- 1 user user 8192 Sep  1 03:58 file1.test.copy
-rw-rw-r-- 1 user user 1122 Aug 31 14:15 file2.test
-rw-rw-r-- 1 user user 1122 Sep  1 03:58 file2.test.copy
-rw-rw-r-- 1 user user   16 Aug 31 18:32 file3.test
-rw-rw-r-- 1 user user   16 Sep  1 03:58 file3.test.copy
Enter fullscreen mode Exit fullscreen mode

Renaming a file is also possible by using the rename() method by setting the new path of the file in the first argument.

File myFile1Rename = await myFile1Copy.rename(myFile1.path + ".new");
print(fileToMap(myFile1Rename));
fileStat(myFile1Rename);

File myFile2Rename = await myFile2Copy.rename(myFile2.path + ".new");
print(fileToMap(myFile2Rename));
fileStat(myFile2Rename);

File myFile3Rename = await myFile3Copy.rename(myFile3.path + ".new");
print(fileToMap(myFile3Rename));
fileStat(myFile3Rename);
Enter fullscreen mode Exit fullscreen mode
$ dart run
{absolute: File: '/home/user/tmp/cboring/./file1.test.new', hashCode: 913418505, isAbsolute: false, parent: Directory: '.', path: ./file1.test.new, runtimeType: _File, uri: {scheme: , host: , path: file1.test.new, fragment: }}
{absolute: File: '/home/user/tmp/cboring/./file2.test.new', hashCode: 515199967, isAbsolute: false, parent: Directory: '.', path: ./file2.test.new, runtimeType: _File, uri: {scheme: , host: , path: file2.test.new, fragment: }}
{accessed: 2026-09-01 03:57:26.553, changed: 2026-09-01 04:02:28.533, mode: 33204, modified: 2026-09-01 04:02:28.533, size: 8192, type: file}
{accessed: 2026-09-01 03:57:26.553, changed: 2026-09-01 04:02:28.533, mode: 33204, modified: 2026-09-01 04:02:28.533, size: 1122, type: file}
{absolute: File: '/home/user/tmp/cboring/file3.test.new', hashCode: 783500576, isAbsolute: false, parent: Directory: '.', path: file3.test.new, runtimeType: _File, uri: {scheme: , host: , path: file3.test.new, fragment: }}
{accessed: 2026-09-01 03:57:26.585, changed: 2026-09-01 04:02:28.533, mode: 33204, modified: 2026-09-01 04:02:28.533, size: 16, type: file}

$ ls -l *.test *.copy *.new
ls: cannot access '*.copy': No such file or directory
-rw-rw-r-- 1 user user 8192 Aug 31 14:15  file1.test
-rw-rw-r-- 1 user user 8192 Sep  1 04:02  file1.test.new
-rw-rw-r-- 1 user user 1122 Aug 31 14:15  file2.test
-rw-rw-r-- 1 user user 1122 Sep  1 04:02  file2.test.new
-rw-rw-r-- 1 user user   16 Aug 31 18:32  file3.test
-rw-rw-r-- 1 user user   16 Sep  1 04:02  file3.test.new
Enter fullscreen mode Exit fullscreen mode

When a file is not needed anymore, it is also possible to remove it from the filesystem with the delete() method. It will return a FileSystemEntity object (a superclass of File). If the File object to remove is a directory, the recursive parameter can be set to remove all files recursively present in it.

print(await myFile1Rename.delete());
print(await myFile2Rename.delete());
print(await myFile3Rename.delete());
Enter fullscreen mode Exit fullscreen mode
$ dart run
File: './file1.test.new'
File: './file2.test.new'
File: 'file3.test.new'

$ ls -l *.test *.copy *.new
ls: cannot access '*.copy': No such file or directory
ls: cannot access '*.new': No such file or directory
-rw-rw-r-- 1 user user 8192 Aug 31 14:15  file1.test
-rw-rw-r-- 1 user user 1122 Aug 31 14:15  file2.test
-rw-rw-r-- 1 user user   16 Aug 31 18:32  file3.test
Enter fullscreen mode Exit fullscreen mode

Finally, the last method to see is the watch() method. This one is probably one of the most interesting to investigate. The idea is to subscribe to the filesystem to follow the activity on a file, when a change happens, the Stream object receives an event from it . On Linux, it uses the inotify API interface.

final timeout = () {
  Duration delay = Duration(seconds: 10);
  final action = () => exit(1);
  Future.delayed(delay, action);
};
timeout();

File toWatch = File("./watcher");
toWatch.create();
Stream watcher = await toWatch.watch();
watcher.listen((event) {
  print(event);
});
Enter fullscreen mode Exit fullscreen mode

The code below is a bit more complex than usual. A timeout closure is defined, it will stop the application after 10 seconds by calling the exit() function. When watching a file in the main thread, the program will simply wait for event forever, the timeout closure has been created to avoid being stuck in this loop.

Then, the ./watcher file is created, and the watcher Stream is attached to it. Because it's a standard Stream, one can listen() on event on it and do some actions.

It's now possible to modify the file from another shell to see the changes happening while the application is running.

$ echo test > watch.test

$ echo test > watch.test

$ echo test > watch.test

$ rm watcher
Enter fullscreen mode Exit fullscreen mode
$ dart run
FileSystemModifyEvent('./watcher', isDirectory=false, contentChanged=true)
FileSystemModifyEvent('./watcher', isDirectory=false, contentChanged=true)
FileSystemModifyEvent('./watcher', isDirectory=false, contentChanged=true)
FileSystemModifyEvent('./watcher', isDirectory=false, contentChanged=true)
FileSystemModifyEvent('./watcher', isDirectory=false, contentChanged=true)
FileSystemModifyEvent('./watcher', isDirectory=false, contentChanged=true)
FileSystemModifyEvent('./watcher', isDirectory=false, contentChanged=false)
FileSystemDeleteEvent('./watcher')
Enter fullscreen mode Exit fullscreen mode

The FileSystemEvent object returned identify the event applied to the file. It can be set to create, delete, modify or move. Few of them will be only displayed when following the activity on a directory. For example, removing a file while watching it, and then recreate it will not work, but this action can be seen when watching a directory.

Conclusion

At first, this post was planned to be a simple, quick and dirty introduction to file management in Dart in another article. In fine, it became a really long explanation about the whole file system API. The abstraction offered by Dart is coherent, simple and stable. In few lines of code, one can easily create, rename or delete files, but also read or write data from them. The support for the iNotify API is a really good thing. As usual, a list of references if you want to dig a bit more:

Happy hacking and have fun!


Cover Image by Kelly Sikkema on Unsplash

Top comments (0)