I/O

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);
  }
}
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:io hanya 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(), bukan readAsStringSync(), kecuali di script CLI sederhana atau kode inisialisasi sebelum event loop.
  • Streaming untuk file besaropenRead() + transform(utf8.decoder) + transform(LineSplitter()) membaca baris per baris tanpa memuat seluruh file ke memori.
  • FileMode.append untuk log file dan data yang terus bertambah — FileMode.write (default) akan menimpa file yang sudah ada.
  • Selalu tutup IOSink dengan finally — jika tidak, data yang belum di-flush bisa hilang saat program berakhir.
  • Gunakan package path untuk manipulasi path — string concatenation dengan / atau \ tidak portabel lintas platform.
  • Platform.environment untuk membaca environment variable — pola umum untuk konfigurasi aplikasi yang berbeda di development vs production.
  • Process.run untuk command yang pendek dengan output tertangkap, Process.start untuk command dengan streaming output real-time (seperti tail -f).
  • Tangkap FileSystemException secara spesifik, bukan catch (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.

← Sebelumnya: Multi Threading   Berikutnya: Socket →

About | Author | Content Scope | Editorial Policy | Privacy Policy | Disclaimer | Contact