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';
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");
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");
);
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")
);
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);
$ dart run
File: '/home/user/tmp/cboring/./file1.test'
File: '/home/user/tmp/cboring/./file2.test'
File: '/home/user/tmp/cboring/file3.test'
The original path passed as first argument can be retrieved with the path attribute.
print(myFile1.path);
print(myFile2.path);
print(myFile3.path);
$ dart run
./file1.test
./file2.test
file3.test
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));
$ dart run
{scheme: , host: , path: file1.test, fragment: }
{scheme: , host: , path: file2.test, fragment: }
{scheme: , host: , path: file3.test, fragment: }
Another interesting property exposed by the File object is the parent one (usually a directory).
print(myFile1.parent);
print(myFile2.parent);
print(myFile3.parent);
$ dart run
Directory: '.'
Directory: '.'
Directory: '.'
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));
{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: }}
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
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());
$ dart run
true
true
true
true
false
false
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);
$ 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)
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);
$ 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}
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);
$ dart run
File: './file1.test'
File: './file2.test'
File: 'file3.test'
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());
$ dart run
Instance of '_RandomAccessFile'
Instance of '_RandomAccessFile'
Instance of '_RandomAccessFile'
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));
$ dart run
myFile1:
(193, 10, 109, 2, 103, 107, 37, 10, 146, 214)
myFile2:
n2ZBmrSIql
myFile2:
(n2ZBmrSIqlF6M0P0JEMN3SKpR4kn1vGXKBH4KVNJK8x6cfvzZ8o9oc6f70kpmCNQn, X86yRGL4f2xJi0o4sbIWOWWZikO09dTNwC9Pt9ltOHLqByFLc847YeGk6gXlSDuGc)
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());
$ dart run
new data added
ABC
this is a string
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);
$ 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
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);
$ 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
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());
$ 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
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);
});
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
$ 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')
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:
File Handling in Dart by Dart Tutorial on Medium;
Dart Lesson 17: file operations and command-line tool by @jige2025;
Using Dart to create custom directories and files for your new Flutter project by @tobacodes;
Learn Dart #5: Read and Write files in under 30 seconds by @creativ_bracket.
Happy hacking and have fun!
Cover Image by Kelly Sikkema on Unsplash
Top comments (0)