I/O #
dart:io adalah library bawaan Dart untuk semua operasi input/output — membaca dan menulis file, menjelajahi direktori, membaca input dari terminal, menjalankan proses eksternal, dan bekerja dengan jaringan. Library ini hanya tersedia di Dart VM (server, CLI, desktop) — tidak bisa digunakan di browser yang memiliki model I/O berbeda. Pemahaman tentang kapan menggunakan operasi sinkron vs asinkron, dan bagaimana menangani file besar secara efisien dengan streaming, adalah inti dari I/O yang andal dan responsif.
Sinkron vs Asinkron — Pilihan yang Menentukan #
dart:io menyediakan dua versi untuk hampir setiap operasi: sinkron (nama diakhiri Sync) dan asinkron. Memilih yang salah bisa membuat aplikasi tidak responsif atau bahkan crash.
import 'dart:io';
// SINKRON — memblokir thread sampai selesai
String isiSync = File('data.txt').readAsStringSync(); // thread berhenti di sini
// ASINKRON — thread bebas melanjutkan, operasi berjalan di background
String isiAsync = await File('data.txt').readAsString(); // thread tidak berhenti
// ANTI-PATTERN: sinkron di server atau aplikasi yang butuh responsivitas
// Satu request yang lama bisa memblokir semua request lain!
Future<Response> handleRequest(Request req) async {
final data = File('large.csv').readAsStringSync(); // ✗ blokir event loop!
return Response.ok(data);
}
// BENAR: asinkron selalu untuk server dan aplikasi responsif
Future<Response> handleRequest(Request req) async {
final data = await File('large.csv').readAsString(); // ✓ non-blocking
return Response.ok(data);
}
GUNAKAN SINKRON ketika:
✓ Script CLI sederhana yang berjalan linear dari atas ke bawah
✓ Inisialisasi saat startup (sebelum event loop dimulai)
✓ Kode yang berjalan di Isolate terpisah (tidak memblokir main isolate)
GUNAKAN ASINKRON ketika:
✓ Server yang menangani banyak request
✓ Aplikasi UI yang harus tetap responsif
✓ Hampir semua kasus lainnya
Operasi File #
Membaca File #
import 'dart:io';
// 1. Baca seluruh isi sebagai String (untuk file kecil-sedang)
final isi = await File('config.json').readAsString(encoding: utf8);
// 2. Baca seluruh isi sebagai bytes
final bytes = await File('gambar.png').readAsBytes();
// 3. Baca per baris — sudah di-split oleh Dart
final baris = await File('data.csv').readAsLines();
for (final b in baris) {
print(b);
}
Membaca File Besar dengan Streaming #
Untuk file yang sangat besar (ratusan MB atau GB), membaca sekaligus ke memori bisa menyebabkan OutOfMemoryError. Gunakan streaming:
import 'dart:io';
import 'dart:convert';
// Stream baris per baris — memori efisien untuk file besar
Future<void> prosesFileBesar(String path) async {
final file = File(path);
// openRead() mengembalikan Stream<List<int>> (bytes)
// transform(utf8.decoder) → Stream<String> (teks)
// transform(LineSplitter()) → Stream<String> (per baris)
final stream = file
.openRead()
.transform(utf8.decoder)
.transform(const LineSplitter());
int nomorBaris = 0;
await for (final baris in stream) {
nomorBaris++;
prosesSetiapBaris(baris, nomorBaris);
// Hanya satu baris di memori pada satu waktu
}
print('Diproses $nomorBaris baris');
}
// ANTI-PATTERN: baca file besar sekaligus
Future<void> prosesBuruk(String path) async {
final semua = await File(path).readAsString(); // ✗ file 1GB = 1GB di RAM
for (final baris in semua.split('\n')) {
prosesSetiapBaris(baris, 0);
}
}
Menulis File #
import 'dart:io';
// 1. Tulis String (overwrite file jika sudah ada)
await File('output.txt').writeAsString('Konten baru');
// 2. Tulis bytes
await File('data.bin').writeAsBytes([0x48, 0x65, 0x6C, 0x6C, 0x6F]);
// 3. Tulis dengan FileMode
await File('log.txt').writeAsString(
'Entry baru\n',
mode: FileMode.append, // tambah di akhir, jangan timpa
flush: true, // flush ke disk setelah tulis
);
FileMode — Kontrol Mode Penulisan
#
// Mode yang tersedia:
FileMode.write // tulis dari awal, buat file jika belum ada, timpa jika ada (default)
FileMode.append // tambah di akhir file
FileMode.read // hanya baca
FileMode.writeOnly // tulis saja, tidak bisa baca
FileMode.writeOnlyAppend // tulis append saja
Streaming Penulisan untuk Data Besar #
Untuk menulis data besar secara bertahap, gunakan IOSink:
import 'dart:io';
Future<void> tulisLaporanBesar(String path, List<Transaksi> data) async {
final sink = File(path).openWrite(mode: FileMode.write);
try {
// Tulis header
sink.writeln('ID,Tanggal,Jumlah,Keterangan');
// Tulis data bertahap — tidak perlu semua di memori sekaligus
for (final t in data) {
sink.writeln('${t.id},${t.tanggal},${t.jumlah},${t.keterangan}');
}
// Penting: flush memastikan semua data tertulis ke disk
await sink.flush();
} finally {
// Selalu tutup sink, bahkan jika ada error
await sink.close();
}
}
Operasi File Lainnya #
final file = File('dokumen.txt');
// Cek keberadaan
bool ada = await file.exists();
// Informasi file
FileStat stat = await file.stat();
print(stat.size); // ukuran dalam bytes
print(stat.modified); // DateTime terakhir dimodifikasi
print(stat.type); // FileSystemEntityType
// Salin file
await file.copy('dokumen_backup.txt');
// Rename / pindahkan
await file.rename('dokumen_baru.txt');
// Hapus file
await file.delete();
// Buat file kosong jika belum ada
await file.create(recursive: true); // recursive membuat direktori parent juga
// Ukuran file
int ukuran = await file.length();
Operasi Direktori #
import 'dart:io';
// Direktori saat ini
Directory sekarang = Directory.current;
print(sekarang.path);
// Direktori sementara sistem
Directory temp = await Directory.systemTemp.createTemp('dart_temp_');
final dir = Directory('data/laporan');
// Buat direktori (recursive membuat parent yang belum ada)
await dir.create(recursive: true);
// Cek keberadaan
bool ada = await dir.exists();
// Hapus direktori (recursive menghapus isi juga)
await dir.delete(recursive: true);
// Rename
await dir.rename('data/laporan_lama');
Traversal Direktori #
import 'dart:io';
// List isi direktori — satu 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}');
}
}
// List rekursif — semua isi termasuk subdirektori
await for (final entitas in dir.list(recursive: true)) {
print(entitas.path);
}
// Filter hanya file .dart
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);
}
// Hitung total ukuran direktori
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');
Manipulasi Path #
dart:io menyediakan Platform.pathSeparator untuk path yang kompatibel lintas platform, tapi package path jauh lebih lengkap:
dart pub add path
import 'package:path/path.dart' as p;
// Join path — otomatis gunakan separator yang benar (\\ di Windows, / di Unix)
String fullPath = p.join('data', 'laporan', 'q4.csv');
// 'data/laporan/q4.csv' di Unix
// 'data\\laporan\\q4.csv' di Windows
// Komponen path
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'
// Path absolut vs relatif
print(p.isAbsolute('/home/user')); // true
print(p.isRelative('data/file')); // true
print(p.absolute('data/file')); // path absolut dari direktori saat ini
// Normalisasi
print(p.normalize('/home/user/../user/./file')); // '/home/user/file'
// Relatif dari satu path ke path lain
print(p.relative('/home/user/a', from: '/home/user/b')); // '../a'
Standard I/O — Terminal #
import 'dart:io';
// Menulis ke stdout (tanpa newline di akhir)
stdout.write('Masukkan nama: ');
// Membaca input dari keyboard (sinkron — ok untuk CLI)
String? input = stdin.readLineSync();
print('Halo, $input!');
// Menulis ke stderr (untuk pesan error)
stderr.writeln('Error: file tidak ditemukan');
// stdout/stderr sebagai IOSink — mendukung semua method IOSink
stdout.writeln('Pesan dengan newline');
// Membaca bytes dari stdin (untuk data biner)
// stdin adalah Stream<List<int>>
stdin.listen((data) {
print('Data diterima: $data');
});
CLI Interaktif #
import 'dart:io';
void main() {
stdout.writeln('=== Kalkulator Sederhana ===');
while (true) {
stdout.write('Masukkan ekspresi (atau "keluar"): ');
final input = stdin.readLineSync()?.trim();
if (input == null || input == 'keluar') {
stdout.writeln('Sampai jumpa!');
break;
}
try {
final hasil = hitungEkspresi(input);
stdout.writeln('= $hasil');
} on FormatException {
stderr.writeln('Format tidak valid: $input');
}
}
}
Environment Variables dan Platform #
import 'dart:io';
// Membaca environment variable
String? path = Platform.environment['PATH'];
String? home = Platform.environment['HOME'];
String? javaHome = Platform.environment['JAVA_HOME'] ?? '/usr/lib/jvm/java';
// Informasi platform
print(Platform.operatingSystem); // 'linux', 'macos', 'windows', 'android', 'ios'
print(Platform.operatingSystemVersion); // versi OS detail
print(Platform.localHostname); // nama host
print(Platform.numberOfProcessors); // jumlah CPU core
print(Platform.pathSeparator); // '/' atau '\\'
print(Platform.isLinux); // bool
print(Platform.isMacOS); // bool
print(Platform.isWindows); // bool
// Argumen command-line
// dart run script.dart arg1 arg2 --flag
print(Platform.executableArguments); // argumen ke Dart VM
// Platform.script = URI dari script yang berjalan
print(Platform.script.toFilePath()); // path absolut script
Menjalankan Proses Eksternal #
dart:io memungkinkan menjalankan perintah sistem operasi melalui Process:
import 'dart:io';
// Jalankan dan tunggu selesai — tangkap semua output
Future<void> contohProcess() async {
// Cara 1: run — tunggu selesai, tangkap 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);
}
// Cara 2: start — streaming output secara 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'));
// Tunggu proses selesai
final exitCode = await process.exitCode;
print('Proses selesai dengan kode: $exitCode');
}
// Jalankan di shell (untuk piping, glob, dll.)
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();
}
// Penggunaan
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('Jumlah file Dart: $filesCount');
}
File Watching — Memantau Perubahan #
import 'dart:io';
// Pantau perubahan file/direktori secara 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('Dibuat: ${event.path}');
case FileSystemEvent.modify:
print('Dimodifikasi: ${event.path}');
case FileSystemEvent.delete:
print('Dihapus: ${event.path}');
case FileSystemEvent.move:
final moveEvent = event as FileSystemMoveEvent;
print('Dipindahkan: ${event.path} → ${moveEvent.destination}');
}
}
}
// Pantau seluruh direktori
void pantauDirektori(String path) {
Directory(path).watch(recursive: true).listen((event) {
print('${event.type}: ${event.path}');
});
}
Anti-Pattern I/O #
Tidak Menutup Resource #
// ANTI-PATTERN: IOSink tidak ditutup — data mungkin tidak ter-flush
Future<void> tulisBuruk(String path) async {
final sink = File(path).openWrite();
sink.writeln('Data penting');
// ✗ sink tidak pernah di-close — data mungkin hilang!
}
// BENAR: selalu tutup dengan try-finally
Future<void> tulisBaik(String path) async {
final sink = File(path).openWrite();
try {
sink.writeln('Data penting');
await sink.flush();
} finally {
await sink.close(); // ✓ selalu dieksekusi
}
}
Tidak Memeriksa Keberadaan File #
// ANTI-PATTERN: langsung baca tanpa cek
Future<void> bacaBuruk(String path) async {
final isi = await File(path).readAsString(); // ✗ FileSystemException jika tidak ada
proses(isi);
}
// BENAR: cek dulu atau tangkap exception spesifik
Future<void> bacaBaik(String path) async {
final file = File(path);
if (!await file.exists()) {
throw FileSystemException('File tidak ditemukan', path);
}
final isi = await file.readAsString();
proses(isi);
}
// Atau: gunakan try-on yang spesifik
Future<void> bacaDenganFallback(String path) async {
try {
final isi = await File(path).readAsString();
proses(isi);
} on FileSystemException catch (e) {
print('Tidak bisa membaca $path: ${e.message}');
// gunakan nilai default atau lanjutkan tanpa data
}
}
Membangun Path dengan String Concatenation #
// ANTI-PATTERN: string concatenation untuk path — tidak lintas platform
String path = directoryPath + '/' + filename; // ✗ akan salah di Windows
// BENAR: gunakan package path
import 'package:path/path.dart' as p;
String path = p.join(directoryPath, filename); // ✓ benar di semua platform
Ringkasan #
dart:iohanya tersedia di Dart VM — tidak bisa digunakan di kode yang dikompilasi ke JavaScript (web). Pisahkan kode I/O dari logika bisnis agar mudah di-test dan di-port.- Asinkron hampir selalu lebih baik dari sinkron — gunakan
readAsString(), bukanreadAsStringSync(), kecuali di script CLI sederhana atau kode inisialisasi sebelum event loop.- Streaming untuk file besar —
openRead()+transform(utf8.decoder)+transform(LineSplitter())membaca baris per baris tanpa memuat seluruh file ke memori.FileMode.appenduntuk log file dan data yang terus bertambah —FileMode.write(default) akan menimpa file yang sudah ada.- Selalu tutup
IOSinkdenganfinally— jika tidak, data yang belum di-flush bisa hilang saat program berakhir.- Gunakan package
pathuntuk manipulasi path — string concatenation dengan/atau\tidak portabel lintas platform.Platform.environmentuntuk membaca environment variable — pola umum untuk konfigurasi aplikasi yang berbeda di development vs production.Process.rununtuk command yang pendek dengan output tertangkap,Process.startuntuk command dengan streaming output real-time (sepertitail -f).- Tangkap
FileSystemExceptionsecara spesifik, bukancatch (e)generik — sehingga error lain tidak tertutup dan tetap bisa di-debug.Directory.watch()untuk hot-reload dan file watcher — berguna untuk dev tools yang perlu mendeteksi perubahan file secara real-time.