ZetCode

Dart File Class

last modified May 30, 2026

The File class in Dart provides a rich API for working with files on the filesystem. It supports reading, writing, copying, renaming, deleting, and inspecting file metadata. The class is part of Dart's dart:io library, which is available for server-side and command-line Dart applications.

File operations come in both asynchronous (Future-based) and synchronous forms. Asynchronous methods are preferred in production code because they do not block the event loop. For large files, Dart also provides stream-based I/O via openRead() and openWrite().

Basic Definition

A File object is a lightweight reference to a filesystem path. Constructing one with File('path/to/file.txt') does not open or create anything on disk; the actual I/O happens when you call a method such as readAsString() or writeAsString().

Key capabilities of the File class:

Reading a Text File

readAsString() reads the entire file content as a single String. The optional encoding parameter defaults to UTF-8. The method is asynchronous and returns a Future<String>.

read_text.dart
import 'dart:io';

void main() async {
  final file = File('words.txt');

  try {
    final contents = await file.readAsString();
    print(contents);
  } on FileSystemException catch (e) {
    print('Could not read file: ${e.message}');
  }
}

A File object is created for words.txt. The await keyword pauses execution until readAsString() resolves, after which the full text is available in contents. A FileSystemException is thrown when the file does not exist or is not readable, so it is caught explicitly rather than using the general Exception type.

$ dart read_text.dart
sky
blue
warm
falcon

Reading File Lines

readAsLines() splits the file on newlines and returns a Future<List<String>>. Each element is one line without the trailing newline character. This is convenient for processing structured text files such as CSV or configuration files.

read_lines.dart
import 'dart:io';

void main() async {
  final file = File('words.txt');

  try {
    final lines = await file.readAsLines();
    print('Lines: ${lines.length}');
    for (final (i, line) in lines.indexed) {
      print('${i + 1}: $line');
    }
  } on FileSystemException catch (e) {
    print('Could not read file: ${e.message}');
  }
}

readAsLines() returns a List<String> where each entry is a single line. The example uses Dart 3's Iterable.indexed to get both the zero-based index and the line value in the for loop, producing a numbered listing of every line.

$ dart read_lines.dart
Lines: 4
1: sky
2: blue
3: warm
4: falcon

Writing to a File

writeAsString() writes a string to a file and returns a Future<File>. The default FileMode.write truncates and replaces any existing content. Pass FileMode.append to add text to the end of the file without erasing it.

write_file.dart
import 'dart:io';

void main() async {
  final file = File('log.txt');

  try {
    // Overwrite the file with an initial header
    await file.writeAsString('--- Application Log ---\n');

    // Append individual log entries
    await file.writeAsString('INFO  app started\n',  mode: FileMode.append);
    await file.writeAsString('DEBUG loaded config\n', mode: FileMode.append);
    await file.writeAsString('INFO  ready\n',         mode: FileMode.append);

    print(await file.readAsString());
  } on FileSystemException catch (e) {
    print('File error: ${e.message}');
  }
}

The first writeAsString() call uses the default FileMode.write, which creates (or truncates) log.txt and writes the header line. Each subsequent call uses FileMode.append so the new lines accumulate rather than replacing one another. The file is then read back to confirm the final result.

$ dart write_file.dart
--- Application Log ---
INFO  app started
DEBUG loaded config
INFO  ready

Writing Bytes

writeAsBytes() writes raw binary data from a List<int> (commonly a Uint8List). The same FileMode options apply. This is the correct method when working with binary formats such as images, compressed archives, or custom binary protocols.

write_bytes.dart
import 'dart:io';
import 'dart:typed_data';

void main() async {
  // Minimal valid GIF87a image (1x1 white pixel)
  final gifBytes = Uint8List.fromList([
    0x47, 0x49, 0x46, 0x38, 0x37, 0x61, // GIF87a signature
    0x01, 0x00, 0x01, 0x00, 0x80, 0x00, // logical screen descriptor
    0x00, 0xFF, 0xFF, 0xFF, 0x00, 0x00, // global colour table
    0x00, 0x2C, 0x00, 0x00, 0x00, 0x00, // image descriptor
    0x01, 0x00, 0x01, 0x00, 0x00, 0x02, // image data header
    0x02, 0x44, 0x01, 0x00, 0x3B,       // image data + trailer
  ]);

  final file = File('pixel.gif');
  await file.writeAsBytes(gifBytes);
  print('Written ${gifBytes.length} bytes to ${file.path}');

  // Read back and verify the GIF signature
  final read = await file.readAsBytes();
  final sig = String.fromCharCodes(read.sublist(0, 6));
  print('Signature: $sig');
}

The Uint8List holds the raw bytes that form a minimal GIF image. After writing, the file is read back and its 6-byte signature is decoded to confirm the write succeeded. The same pattern applies whenever you need to persist or verify a binary file format.

$ dart write_bytes.dart
Written 35 bytes to pixel.gif
Signature: GIF87a

Checking File Existence

exists() returns a Future<bool> that resolves to true when the path refers to a file that is present on disk. Use it as a guard before reading or writing to give users a clear error message rather than a raw FileSystemException.

check_exists.dart
import 'dart:io';

Future<void> processFile(String path) async {
  final file = File(path);

  if (!await file.exists()) {
    print('File not found: $path');
    return;
  }

  final lines = await file.readAsLines();
  print('Processing $path (${lines.length} lines)...');
  for (final line in lines) {
    print('  $line');
  }
}

void main() async {
  await processFile('words.txt');
  await processFile('missing.txt');
}

processFile calls exists() first and returns early with a descriptive message when the file is absent. Only when the file exists does it proceed to read and process the lines. The second call demonstrates the not-found path.

$ dart check_exists.dart
Processing words.txt (4 lines)...
  sky
  blue
  warm
  falcon
File not found: missing.txt

Reading File Metadata

The stat() method returns a Future<FileStat> with information about the file: size in bytes, last-modified and last-accessed timestamps, and the FileSystemEntityType (file, directory, or link).

file_stat.dart
import 'dart:io';

void main() async {
  final file = File('words.txt');

  try {
    final stat = await file.stat();
    print('Type:     ${stat.type}');
    print('Size:     ${stat.size} bytes');
    print('Modified: ${stat.modified}');
    print('Accessed: ${stat.accessed}');
    print('Mode:     ${stat.modeString()}');
  } on FileSystemException catch (e) {
    print('Could not stat file: ${e.message}');
  }
}

FileStat.type distinguishes between a regular file, a directory, a symbolic link, and other entity types. modeString() returns a POSIX-style permission string (e.g. rw-r--r--) reflecting the file's access permissions on the current platform.

$ dart file_stat.dart
Type:     file
Size:     24 bytes
Modified: 2025-04-04 10:30:45.000
Accessed: 2025-04-04 10:35:12.000
Mode:     rw-r--r--

Copying, Renaming, and Deleting

copy(newPath) creates a duplicate of the file at the given path. rename(newPath) moves or renames the file without making a copy — when source and destination are on the same filesystem this is a cheap metadata-only operation. delete() permanently removes the file.

manage_files.dart
import 'dart:io';

void main() async {
  final original = File('words.txt');

  // Copy to a backup
  final backup = await original.copy('words.bak');
  print('Copied  → ${backup.path}');

  // Rename the backup (original path no longer exists afterwards)
  final renamed = await backup.rename('words_backup.txt');
  print('Renamed → ${renamed.path}');

  // Verify the renamed file exists, then delete it
  if (await renamed.exists()) {
    await renamed.delete();
    print('Deleted   ${renamed.path}');
  }
}

copy() returns a new File reference pointing to the copy. rename() returns a File for the new path — the original path no longer exists afterwards. The existence check before delete() is defensive; in production code you might instead catch the FileSystemException that would be thrown if the file had already been removed by another process.

$ dart manage_files.dart
Copied  → words.bak
Renamed → words_backup.txt
Deleted   words_backup.txt

Reading Binary Data

readAsBytes() loads the entire file into a Uint8List. The example below reads a PNG file and validates its 8-byte magic number, which every valid PNG file starts with, and then extracts the image dimensions from the IHDR chunk.

read_bytes.dart
import 'dart:io';

// Every valid PNG file starts with these 8 bytes.
const List<int> pngMagic = [137, 80, 78, 71, 13, 10, 26, 10];

void main() async {
  final file = File('image.png');

  try {
    final bytes = await file.readAsBytes();
    print('Read ${bytes.length} bytes');

    // Verify PNG magic number
    final header = bytes.sublist(0, 8);
    final isPng = List.generate(8, (i) => header[i] == pngMagic[i])
        .every((match) => match);
    print('Valid PNG: $isPng');

    // IHDR chunk starts at offset 8; width and height are big-endian int32
    int w = (bytes[16] << 24) | (bytes[17] << 16) | (bytes[18] << 8) | bytes[19];
    int h = (bytes[20] << 24) | (bytes[21] << 16) | (bytes[22] << 8) | bytes[23];
    print('Dimensions: ${w}x${h} px');
  } on FileSystemException catch (e) {
    print('Could not read file: ${e.message}');
  }
}

The first 8 bytes are compared element-by-element against the known PNG magic number. If they match, the example also extracts the image width and height from the IHDR chunk, which begins at byte offset 16. Bit-shifting combines four bytes into a 32-bit big-endian integer — the format PNG uses for all multi-byte integers.

$ dart read_bytes.dart
Read 4096 bytes
Valid PNG: true
Dimensions: 200x150 px

Reading Large Files with Streams

openRead() returns a Stream<List<int>> that delivers the file in byte chunks. Piping it through utf8.decoder and LineSplitter lets you process one line at a time without loading the entire file into memory — essential for large log files or data exports.

read_stream.dart
import 'dart:io';
import 'dart:convert';

void main() async {
  final file = File('words.txt');
  int lineCount = 0;

  try {
    final stream = file
        .openRead()
        .transform(utf8.decoder)
        .transform(const LineSplitter());

    await for (final line in stream) {
      lineCount++;
      print('$lineCount: $line');
    }
    print('Total lines: $lineCount');
  } on FileSystemException catch (e) {
    print('Could not open file: ${e.message}');
  }
}

openRead() opens the file and starts streaming raw bytes. utf8.decoder converts each byte chunk into a String. LineSplitter reassembles the stream into complete lines, handling the case where a chunk boundary falls in the middle of a line. The await for loop processes each line as soon as it arrives, keeping memory usage constant regardless of file size.

$ dart read_stream.dart
1: sky
2: blue
3: warm
4: falcon
Total lines: 4

Best Practices

Source

Dart File Documentation

This tutorial covered Dart's File class with practical examples demonstrating reading and writing text and binary data, checking existence, inspecting metadata, managing files with copy/rename/delete, and streaming large files efficiently.

Author

My name is Jan Bodnar, and I am a passionate programmer with extensive programming experience. I have been writing programming articles since 2007. To date, I have authored over 1,400 articles and 8 e-books. I possess more than ten years of experience in teaching programming.

List all Dart tutorials.