I/O #
dart:io is Dart’s built-in library for all input/output operations — reading and writing files, exploring directories, reading terminal input, running external processes, and working with networking. This library is only available on the Dart VM (server, CLI, desktop) — it can’t be used in the browser, which has a different I/O model. Understanding when to use synchronous vs asynchronous operations, and how to handle large files efficiently with streaming, is the core of reliable, responsive I/O.
Synchronous vs Asynchronous — a Choice That Matters #
dart:io provides two versions of almost every operation: synchronous (names ending in Sync) and asynchronous. Choosing the wrong one can make an app unresponsive or even crash.
import 'dart:io';
// SYNCHRONOUS — blocks the thread until done
String isiSync = File('data.txt').readAsStringSync(); // the thread stops here
// ASYNCHRONOUS — the thread is free to continue, the operation runs in the background
String isiAsync = await File('data.txt').readAsString(); // the thread doesn't stop
// ANTI-PATTERN: synchronous in a server or an app that needs responsiveness
// One slow request can block all other requests!
Future<Response> handleRequest(Request req) async {
final data = File('large.csv').readAsStringSync(); // ✗ blocks the event loop!
return Response.ok(data);
}
// CORRECT: always asynchronous for servers and responsive apps
Future<Response> handleRequest(Request req) async {
final data = await File('large.csv').readAsString(); // ✓ non-blocking
return Response.ok(data);
}
USE SYNCHRONOUS when:
✓ Simple CLI scripts running linearly from top to bottom
✓ Startup initialization (before the event loop starts)
✓ Code running on a separate Isolate (doesn't block the main isolate)
USE ASYNCHRONOUS when:
✓ Servers handling many requests
✓ UI apps that must stay responsive
✓ Almost every other case
File Operations #
Reading Files #
import 'dart:io';
// 1. Read the entire content as a String (for small-to-medium files)
final isi = await File('config.json').readAsString(encoding: utf8);
// 2. Read the entire content as bytes
final bytes = await File('gambar.png').readAsBytes();
// 3. Read line by line — already split by Dart
final baris = await File('data.csv').readAsLines();
for (final b in baris) {
print(b);
}
Reading Large Files with Streaming #
For very large files (hundreds of MB or GB), reading everything into memory at once can cause an OutOfMemoryError. Use streaming:
import 'dart:io';
import 'dart:convert';
// Stream line by line — memory efficient for large files
Future<void> prosesFileBesar(String path) async {
final file = File(path);
// openRead() returns a Stream<List<int>> (bytes)
// transform(utf8.decoder) → Stream<String> (text)
// transform(LineSplitter()) → Stream<String> (per line)
final stream = file
.openRead()
.transform(utf8.decoder)
.transform(const LineSplitter());
int nomorBaris = 0;
await for (final baris in stream) {
nomorBaris++;
prosesSetiapBaris(baris, nomorBaris);
// Only one line in memory at a time
}
print('Processed $nomorBaris lines');
}
// ANTI-PATTERN: reading a large file all at once
Future<void> prosesBuruk(String path) async {
final semua = await File(path).readAsString(); // ✗ a 1GB file = 1GB in RAM
for (final baris in semua.split('\n')) {
prosesSetiapBaris(baris, 0);
}
}
Writing Files #
import 'dart:io';
// 1. Write a String (overwrites the file if it exists)
await File('output.txt').writeAsString('New content');
// 2. Write bytes
await File('data.bin').writeAsBytes([0x48, 0x65, 0x6C, 0x6C, 0x6F]);
// 3. Write with FileMode
await File('log.txt').writeAsString(
'New entry\n',
mode: FileMode.append, // append at the end, don't overwrite
flush: true, // flush to disk after writing
);
FileMode — Write Mode Control
#
// Available modes:
FileMode.write // write from the start, create if absent, overwrite if present (default)
FileMode.append // append at the end of the file
FileMode.read // read only
FileMode.writeOnly // write only, can't read
FileMode.writeOnlyAppend // append-write only
Streaming Writes for Large Data #
To write large data incrementally, use IOSink:
import 'dart:io';
Future<void> tulisLaporanBesar(String path, List<Transaksi> data) async {
final sink = File(path).openWrite(mode: FileMode.write);
try {
// Write the header
sink.writeln('ID,Tanggal,Jumlah,Keterangan');
// Write data incrementally — no need for all of it in memory at once
for (final t in data) {
sink.writeln('${t.id},${t.tanggal},${t.jumlah},${t.keterangan}');
}
// Important: flush ensures all data is written to disk
await sink.flush();
} finally {
// Always close the sink, even if there's an error
await sink.close();
}
}
Other File Operations #
final file = File('dokumen.txt');
// Check existence
bool ada = await file.exists();
// File info
FileStat stat = await file.stat();
print(stat.size); // size in bytes
print(stat.modified); // DateTime of last modification
print(stat.type); // FileSystemEntityType
// Copy a file
await file.copy('dokumen_backup.txt');
// Rename / move
await file.rename('dokumen_baru.txt');
// Delete a file
await file.delete();
// Create an empty file if absent
await file.create(recursive: true); // recursive also creates parent directories
// File size
int ukuran = await file.length();
Directory Operations #
import 'dart:io';
// Current directory
Directory sekarang = Directory.current;
print(sekarang.path);
// System temp directory
Directory temp = await Directory.systemTemp.createTemp('dart_temp_');
final dir = Directory('data/laporan');
// Create a directory (recursive creates missing parents)
await dir.create(recursive: true);
// Check existence
bool ada = await dir.exists();
// Delete a directory (recursive also deletes its contents)
await dir.delete(recursive: true);
// Rename
await dir.rename('data/laporan_lama');
Directory Traversal #
import 'dart:io';
// List directory contents — one level
final dir = Directory('lib');
await for (final entitas in dir.list()) {
if (entitas is File) {
print('File: ${entitas.path}');
} else if (entitas is Directory) {
print('Dir: ${entitas.path}');
}
}
// Recursive listing — all contents including subdirectories
await for (final entitas in dir.list(recursive: true)) {
print(entitas.path);
}
// Filter only .dart files
final dartFiles = dir
.list(recursive: true)
.where((e) => e is File && e.path.endsWith('.dart'))
.cast<File>();
await for (final file in dartFiles) {
print(file.path);
}
// Count the total directory size
int totalBytes = 0;
await for (final entitas in dir.list(recursive: true)) {
if (entitas is File) {
totalBytes += await entitas.length();
}
}
print('Total: ${totalBytes ~/ 1024} KB');
Path Manipulation #
dart:io provides Platform.pathSeparator for cross-platform compatible paths, but the path package is far more complete:
dart pub add path
import 'package:path/path.dart' as p;
// Join paths — automatically uses the correct separator (\\ on Windows, / on Unix)
String fullPath = p.join('data', 'laporan', 'q4.csv');
// 'data/laporan/q4.csv' on Unix
// 'data\\laporan\\q4.csv' on Windows
// Path components
print(p.basename('/home/user/file.dart')); // 'file.dart'
print(p.basenameWithoutExtension('/home/user/file.dart')); // 'file'
print(p.extension('/home/user/file.dart')); // '.dart'
print(p.dirname('/home/user/file.dart')); // '/home/user'
// Absolute vs relative paths
print(p.isAbsolute('/home/user')); // true
print(p.isRelative('data/file')); // true
print(p.absolute('data/file')); // absolute path from the current directory
// Normalization
print(p.normalize('/home/user/../user/./file')); // '/home/user/file'
// Relative from one path to another
print(p.relative('/home/user/a', from: '/home/user/b')); // '../a'
Standard I/O — Terminal #
import 'dart:io';
// Write to stdout (without a trailing newline)
stdout.write('Enter your name: ');
// Read input from the keyboard (synchronous — fine for CLI)
String? input = stdin.readLineSync();
print('Hello, $input!');
// Write to stderr (for error messages)
stderr.writeln('Error: file not found');
// stdout/stderr as IOSink — support all IOSink methods
stdout.writeln('Message with newline');
// Read bytes from stdin (for binary data)
// stdin is a Stream<List<int>>
stdin.listen((data) {
print('Data received: $data');
});
Interactive CLI #
import 'dart:io';
void main() {
stdout.writeln('=== Simple Calculator ===');
while (true) {
stdout.write('Enter an expression (or "exit"): ');
final input = stdin.readLineSync()?.trim();
if (input == null || input == 'exit') {
stdout.writeln('Goodbye!');
break;
}
try {
final hasil = hitungEkspresi(input);
stdout.writeln('= $hasil');
} on FormatException {
stderr.writeln('Invalid format: $input');
}
}
}
Environment Variables and Platform #
import 'dart:io';
// Reading environment variables
String? path = Platform.environment['PATH'];
String? home = Platform.environment['HOME'];
String? javaHome = Platform.environment['JAVA_HOME'] ?? '/usr/lib/jvm/java';
// Platform info
print(Platform.operatingSystem); // 'linux', 'macos', 'windows', 'android', 'ios'
print(Platform.operatingSystemVersion); // detailed OS version
print(Platform.localHostname); // hostname
print(Platform.numberOfProcessors); // number of CPU cores
print(Platform.pathSeparator); // '/' or '\\'
print(Platform.isLinux); // bool
print(Platform.isMacOS); // bool
print(Platform.isWindows); // bool
// Command-line arguments
// dart run script.dart arg1 arg2 --flag
print(Platform.executableArguments); // arguments to the Dart VM
// Platform.script = URI of the running script
print(Platform.script.toFilePath()); // absolute script path
Running External Processes #
dart:io lets you run operating system commands through Process:
import 'dart:io';
// Run and wait for completion — capture all output
Future<void> contohProcess() async {
// Way 1: run — wait for completion, capture output
final result = await Process.run('ls', ['-la', '/tmp']);
print('Exit code: ${result.exitCode}');
print('stdout:\n${result.stdout}');
if (result.stderr.isNotEmpty) {
stderr.write(result.stderr);
}
// Way 2: start — stream output in real-time
final process = await Process.start('tail', ['-f', 'server.log']);
process.stdout
.transform(utf8.decoder)
.transform(const LineSplitter())
.listen((baris) => print('[LOG] $baris'));
process.stderr
.transform(utf8.decoder)
.listen((error) => stderr.write('[ERR] $error'));
// Wait for the process to finish
final exitCode = await process.exitCode;
print('Process finished with code: $exitCode');
}
// Run in a shell (for piping, globs, etc.)
Future<String> jalankanShell(String perintah) async {
final result = await Process.run(
'bash', ['-c', perintah],
runInShell: true,
);
if (result.exitCode != 0) {
throw ProcessException('bash', ['-c', perintah],
result.stderr.toString(), result.exitCode as int);
}
return result.stdout.toString().trim();
}
// Usage
void main() async {
final versiGit = await jalankanShell('git --version');
print(versiGit); // git version 2.x.x
final filesCount = await jalankanShell('ls *.dart | wc -l');
print('Number of Dart files: $filesCount');
}
File Watching — Monitoring Changes #
import 'dart:io';
// Watch file/directory changes in real-time
Future<void> pantauPerubahan(String path) async {
final watcher = File(path).watch();
await for (final event in watcher) {
switch (event.type) {
case FileSystemEvent.create:
print('Created: ${event.path}');
case FileSystemEvent.modify:
print('Modified: ${event.path}');
case FileSystemEvent.delete:
print('Deleted: ${event.path}');
case FileSystemEvent.move:
final moveEvent = event as FileSystemMoveEvent;
print('Moved: ${event.path} → ${moveEvent.destination}');
}
}
}
// Watch an entire directory
void pantauDirektori(String path) {
Directory(path).watch(recursive: true).listen((event) {
print('${event.type}: ${event.path}');
});
}
I/O Anti-Patterns #
Not Closing Resources #
// ANTI-PATTERN: IOSink never closed — data may not be flushed
Future<void> tulisBuruk(String path) async {
final sink = File(path).openWrite();
sink.writeln('Important data');
// ✗ the sink is never closed — data may be lost!
}
// CORRECT: always close with try-finally
Future<void> tulisBaik(String path) async {
final sink = File(path).openWrite();
try {
sink.writeln('Important data');
await sink.flush();
} finally {
await sink.close(); // ✓ always executed
}
}
Not Checking File Existence #
// ANTI-PATTERN: reading directly without checking
Future<void> bacaBuruk(String path) async {
final isi = await File(path).readAsString(); // ✗ FileSystemException if absent
proses(isi);
}
// CORRECT: check first or catch the specific exception
Future<void> bacaBaik(String path) async {
final file = File(path);
if (!await file.exists()) {
throw FileSystemException('File not found', path);
}
final isi = await file.readAsString();
proses(isi);
}
// Or: use a specific try-on
Future<void> bacaDenganFallback(String path) async {
try {
final isi = await File(path).readAsString();
proses(isi);
} on FileSystemException catch (e) {
print('Cannot read $path: ${e.message}');
// use a default value or continue without the data
}
}
Building Paths with String Concatenation #
// ANTI-PATTERN: string concatenation for paths — not cross-platform
String path = directoryPath + '/' + filename; // ✗ will be wrong on Windows
// CORRECT: use the path package
import 'package:path/path.dart' as p;
String path = p.join(directoryPath, filename); // ✓ correct on every platform
Summary #
dart:iois only available on the Dart VM — it can’t be used in code compiled to JavaScript (web). Separate I/O code from business logic so it’s easy to test and port.- Asynchronous is almost always better than synchronous — use
readAsString(), notreadAsStringSync(), except in simple CLI scripts or initialization code before the event loop.- Streaming for large files —
openRead()+transform(utf8.decoder)+transform(LineSplitter())reads line by line without loading the entire file into memory.FileMode.appendfor log files and growing data —FileMode.write(default) overwrites existing files.- Always close
IOSinkwithfinally— otherwise, unflushed data can be lost when the program ends.- Use the
pathpackage for path manipulation — string concatenation with/or\isn’t portable across platforms.Platform.environmentfor reading environment variables — a common pattern for app configuration that differs between development and production.Process.runfor short commands with captured output,Process.startfor commands with real-time output streaming (liketail -f).- Catch
FileSystemExceptionspecifically, not a genericcatch (e)— so other errors aren’t masked and stay debuggable.Directory.watch()for hot-reload and file watchers — useful for dev tools that need to detect file changes in real-time.