Typed Data

Typed Data #

dart:typed_data menyediakan array bertype dengan performa tinggi untuk data numerik dan biner — berbeda dari List<int> biasa yang menyimpan object boxed, typed list menyimpan nilai langsung dalam memori yang contigious (bersebelahan), seperti array di C. Ini menjadikannya jauh lebih efisien untuk pemrosesan gambar, protokol jaringan, kriptografi, audio processing, dan semua kasus yang melibatkan data biner dalam jumlah besar.

Mengapa Typed Data? #

// List<int> biasa — setiap elemen adalah Dart object (boxed)
// Overhead: pointer (8 byte) + header object + nilai integer
List<int> listBiasa = [1, 2, 3, 4, 5];

// Uint8List — array byte langsung di memori, tidak ada boxing
// Efisien: setiap elemen hanya 1 byte, tersusun bersebelahan
Uint8List typedList = Uint8List.fromList([1, 2, 3, 4, 5]);

// Perbedaan ukuran untuk 1 juta integer:
// List<int>: ~8MB (pointer) + overhead per elemen
// Int32List: 4MB (4 byte per elemen, flat)
// Uint8List: 1MB (1 byte per elemen, flat)

Tipe Typed List #

KelasTipe ElemenUkuran/ElemenRentang Nilai
Uint8ListInteger unsigned1 byte0 – 255
Int8ListInteger signed1 byte-128 – 127
Uint16ListInteger unsigned2 byte0 – 65.535
Int16ListInteger signed2 byte-32.768 – 32.767
Uint32ListInteger unsigned4 byte0 – 4.294.967.295
Int32ListInteger signed4 byte-2.147.483.648 – 2.147.483.647
Uint64ListInteger unsigned8 byte0 – 2⁶⁴-1
Int64ListInteger signed8 byte-2⁶³ – 2⁶³-1
Float32ListFloating point4 byteIEEE 754 single
Float64ListFloating point8 byteIEEE 754 double

Uint8List — Paling Umum Digunakan #

Uint8List adalah tipe yang paling sering dipakai — merepresentasikan raw bytes:

import 'dart:typed_data';

// Membuat Uint8List
final zeros = Uint8List(10);              // [0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
final dariList = Uint8List.fromList([72, 101, 108, 108, 111]); // 'Hello'
final view = Uint8List.view(buffer);     // view ke ByteBuffer yang sudah ada

// Akses dan modifikasi — seperti List<int>
final bytes = Uint8List(5);
bytes[0] = 255;   // ✓ 0-255
bytes[1] = 128;
bytes[2] = 0;
// bytes[0] = 256; // ✗ nilai di-clamp: menjadi 0 (overflow silently!)
// bytes[0] = -1;  // ✗ menjadi 255 (wrapping)

// Properti
print(bytes.length);          // 5
print(bytes.lengthInBytes);   // 5 (sama untuk Uint8List)
print(bytes.elementSizeInBytes); // 1 byte per elemen
print(bytes.buffer);          // ByteBuffer yang mendasarinya

// Slice (view, bukan copy)
final slice = bytes.sublist(1, 3); // elemen indeks 1 dan 2

Konversi String ↔ Bytes #

import 'dart:convert';
import 'dart:typed_data';

// String → Uint8List (UTF-8)
String teks = 'Halo, Dunia! 🌍';
Uint8List bytesUtf8 = utf8.encode(teks) as Uint8List;
// atau:
Uint8List bytesUtf8b = Uint8List.fromList(utf8.encode(teks));

// Uint8List → String (UTF-8)
String decoded = utf8.decode(bytesUtf8);
print(decoded); // 'Halo, Dunia! 🌍'

// Sebagai List<int> biasa
List<int> asList = bytesUtf8.toList();

// Ke hex string (berguna untuk debugging)
String toHex(Uint8List bytes) {
  return bytes.map((b) => b.toRadixString(16).padLeft(2, '0')).join(' ');
}
print(toHex(Uint8List.fromList([72, 101, 108, 111]))); // '48 65 6c 6f'

// Dari hex string
Uint8List fromHex(String hex) {
  final cleaned = hex.replaceAll(' ', '').replaceAll(':', '');
  return Uint8List.fromList([
    for (int i = 0; i < cleaned.length; i += 2)
      int.parse(cleaned.substring(i, i + 2), radix: 16),
  ]);
}

ByteData — Akses Multi-Type ke Buffer #

ByteData memungkinkan membaca dan menulis berbagai tipe numerik ke buffer yang sama dengan kontrol atas endianness:

import 'dart:typed_data';

// Buat ByteData
final data = ByteData(16); // 16 byte buffer

// Tulis berbagai tipe
data.setUint8(0, 0xFF);                        // 1 byte di offset 0
data.setUint16(1, 0xABCD, Endian.big);         // 2 byte di offset 1
data.setInt32(3, -1234567, Endian.little);     // 4 byte di offset 3
data.setFloat64(7, 3.14159, Endian.little);    // 8 byte di offset 7

// Baca kembali
print(data.getUint8(0));                       // 255
print(data.getUint16(1, Endian.big));          // 43981 (0xABCD)
print(data.getInt32(3, Endian.little));        // -1234567
print(data.getFloat64(7, Endian.little));      // 3.14159...

// Semua getter/setter yang tersedia:
// getUint8, setUint8
// getUint16, setUint16
// getUint32, setUint32
// getUint64, setUint64
// getInt8, setInt8
// getInt16, setInt16
// getInt32, setInt32
// getInt64, setInt64
// getFloat32, setFloat32
// getFloat64, setFloat64

// ByteData dari Uint8List
final uint8 = Uint8List.fromList([0x01, 0x02, 0x03, 0x04]);
final bd = ByteData.sublistView(uint8);
print(bd.getUint32(0, Endian.big));    // 0x01020304 = 16909060
print(bd.getUint32(0, Endian.little)); // 0x04030201 = 67305985

Endianness — Big Endian vs Little Endian #

Endianness adalah urutan byte dalam representasi multi-byte integer:

flowchart LR
    N["Angka: 0x12345678\n(305419896 desimal)"]
    N --> BE["Big Endian\n(Network byte order)\n[0x12, 0x34, 0x56, 0x78]\nByte paling signifikan dulu"]
    N --> LE["Little Endian\n(x86, ARM default)\n[0x78, 0x56, 0x34, 0x12]\nByte paling tidak signifikan dulu"]
import 'dart:typed_data';

final nilai = 0x12345678; // 305419896

// Big Endian — byte paling signifikan di awal
final be = ByteData(4);
be.setUint32(0, nilai, Endian.big);
print(be.buffer.asUint8List().toList());
// [0x12, 0x34, 0x56, 0x78] = [18, 52, 86, 120]

// Little Endian — byte paling tidak signifikan di awal
final le = ByteData(4);
le.setUint32(0, nilai, Endian.little);
print(le.buffer.asUint8List().toList());
// [0x78, 0x56, 0x34, 0x12] = [120, 86, 52, 18]

// Endian.host — endianness platform saat ini
final host = ByteData(4);
host.setUint32(0, nilai, Endian.host);

// Tips:
// Protokol jaringan (TCP/IP, HTTP) → Big Endian (Network byte order)
// File format kebanyakan → Little Endian
// x86/ARM → Little Endian natively

ByteBuffer — Buffer yang Mendasari #

Semua typed list berbagi satu ByteBuffer yang sama — ini memungkinkan melihat data yang sama dari perspektif tipe berbeda:

import 'dart:typed_data';

// Buat buffer 16 byte
final buffer = Uint8List(16).buffer;

// Lihat buffer yang sama sebagai tipe berbeda
final asUint8 = buffer.asUint8List();    // 16 elemen (1 byte each)
final asUint16 = buffer.asUint16List();  // 8 elemen (2 byte each)
final asUint32 = buffer.asUint32List();  // 4 elemen (4 byte each)
final asFloat64 = buffer.asFloat64List(); // 2 elemen (8 byte each)

// Tulis data sebagai uint32
final uint32 = buffer.asUint32List();
uint32[0] = 0xDEADBEEF;

// Baca kembali sebagai byte individual
print(asUint8[0].toRadixString(16)); // byte pertama dari 0xDEADBEEF

// sublistView — view ke subset buffer
final sub = Uint8List.sublistView(asUint8, 4, 8); // byte 4-7

Penggunaan Nyata #

Membangun Paket Protokol Jaringan #

import 'dart:typed_data';
import 'dart:convert';
import 'dart:io';

// Format paket kustom:
// [0-1]: magic bytes (0xDA 0x7A)
// [2]:   versi (1 byte)
// [3]:   tipe pesan (1 byte)
// [4-7]: panjang payload (4 byte, big endian)
// [8+]:  payload (UTF-8 bytes)

Uint8List buatPaket({
  required int tipePesan,
  required String payload,
}) {
  final payloadBytes = utf8.encode(payload);
  final totalPanjang = 8 + payloadBytes.length;

  final paket = ByteData(totalPanjang);

  // Header
  paket.setUint8(0, 0xDA);                              // magic byte 1
  paket.setUint8(1, 0x7A);                              // magic byte 2
  paket.setUint8(2, 1);                                  // versi
  paket.setUint8(3, tipePesan);                          // tipe
  paket.setUint32(4, payloadBytes.length, Endian.big);  // panjang payload

  // Payload
  final result = paket.buffer.asUint8List();
  result.setRange(8, 8 + payloadBytes.length, payloadBytes);

  return result;
}

Map<String, dynamic> parsePaket(Uint8List data) {
  if (data.length < 8) throw FormatException('Paket terlalu pendek');

  final bd = ByteData.sublistView(data);

  final magic1 = bd.getUint8(0);
  final magic2 = bd.getUint8(1);
  if (magic1 != 0xDA || magic2 != 0x7A) {
    throw FormatException('Magic bytes tidak valid');
  }

  final versi = bd.getUint8(2);
  final tipe = bd.getUint8(3);
  final panjangPayload = bd.getUint32(4, Endian.big);

  if (data.length < 8 + panjangPayload) {
    throw FormatException('Payload tidak lengkap');
  }

  final payload = utf8.decode(data.sublist(8, 8 + panjangPayload));

  return {
    'versi': versi,
    'tipe': tipe,
    'payload': payload,
  };
}

// Penggunaan
void main() {
  final paket = buatPaket(tipePesan: 1, payload: 'Halo, Server!');
  print('Paket: ${paket.length} bytes');
  print('Hex: ${paket.map((b) => b.toRadixString(16).padLeft(2, '0')).join(' ')}');

  final parsed = parsePaket(paket);
  print('Tipe: ${parsed['tipe']}, Payload: ${parsed['payload']}');
}

Image Processing — Manipulasi Pixel #

import 'dart:typed_data';

// Gambar RGBA: setiap pixel = 4 byte (R, G, B, A)
class SimpleImage {
  final int lebar;
  final int tinggi;
  final Uint8List pixels;

  SimpleImage(this.lebar, this.tinggi)
      : pixels = Uint8List(lebar * tinggi * 4);

  // Offset pixel di buffer (baris-kolom ke indeks linear)
  int _offset(int x, int y) => (y * lebar + x) * 4;

  // Set pixel RGBA
  void setPixel(int x, int y, int r, int g, int b, int a) {
    final offset = _offset(x, y);
    pixels[offset]     = r;
    pixels[offset + 1] = g;
    pixels[offset + 2] = b;
    pixels[offset + 3] = a;
  }

  // Get pixel
  ({int r, int g, int b, int a}) getPixel(int x, int y) {
    final offset = _offset(x, y);
    return (
      r: pixels[offset],
      g: pixels[offset + 1],
      b: pixels[offset + 2],
      a: pixels[offset + 3],
    );
  }

  // Grayscale filter — rata-rata RGB
  SimpleImage toGrayscale() {
    final hasil = SimpleImage(lebar, tinggi);
    for (int i = 0; i < pixels.length; i += 4) {
      final gray = ((pixels[i] + pixels[i + 1] + pixels[i + 2]) ~/ 3);
      hasil.pixels[i]     = gray;
      hasil.pixels[i + 1] = gray;
      hasil.pixels[i + 2] = gray;
      hasil.pixels[i + 3] = pixels[i + 3]; // pertahankan alpha
    }
    return hasil;
  }

  // Invert warna
  SimpleImage invert() {
    final hasil = SimpleImage(lebar, tinggi);
    for (int i = 0; i < pixels.length; i += 4) {
      hasil.pixels[i]     = 255 - pixels[i];     // R
      hasil.pixels[i + 1] = 255 - pixels[i + 1]; // G
      hasil.pixels[i + 2] = 255 - pixels[i + 2]; // B
      hasil.pixels[i + 3] = pixels[i + 3];         // A (tidak diinvert)
    }
    return hasil;
  }
}

Parsing Format Biner (BMP, binary file) #

import 'dart:typed_data';
import 'dart:io';

// Parse header BMP file
Future<Map<String, dynamic>> parseBmpHeader(String path) async {
  final bytes = await File(path).readAsBytes();
  final bd = ByteData.sublistView(bytes);

  // BMP header format (little endian)
  final signature = String.fromCharCodes(bytes.sublist(0, 2)); // 'BM'
  final fileSize = bd.getUint32(2, Endian.little);
  final dataOffset = bd.getUint32(10, Endian.little);
  final headerSize = bd.getUint32(14, Endian.little);
  final width = bd.getInt32(18, Endian.little);
  final height = bd.getInt32(22, Endian.little);
  final bitsPerPixel = bd.getUint16(28, Endian.little);

  return {
    'signature': signature,
    'fileSize': fileSize,
    'dataOffset': dataOffset,
    'width': width,
    'height': height.abs(),   // negatif = top-down
    'bitsPerPixel': bitsPerPixel,
    'topDown': height < 0,
  };
}

Transfer Antar Isolate #

Uint8List bisa di-transfer antar Isolate secara zero-copy menggunakan TransferableTypedData:

import 'dart:isolate';
import 'dart:typed_data';

// Zero-copy transfer — ownership dipindahkan, bukan di-copy
Future<Uint8List> prosesGambarDiIsolate(Uint8List pixelData) async {
  // Bungkus sebagai TransferableTypedData
  final transferable = TransferableTypedData.fromList([pixelData]);

  final terima = ReceivePort();
  await Isolate.spawn(
    _prosesEntryPoint,
    [terima.sendPort, transferable],
  );

  // Terima hasil dari isolate
  final hasilTransferable = await terima.first as TransferableTypedData;
  return hasilTransferable.materialize().asUint8List();
}

void _prosesEntryPoint(List<dynamic> args) {
  final SendPort kirimKe = args[0];
  final TransferableTypedData data = args[1];

  final pixels = data.materialize().asUint8List();
  // Proses gambar (grayscale, blur, dll.)
  final hasil = _grayscale(pixels);

  // Kirim balik sebagai TransferableTypedData (zero-copy)
  kirimKe.send(TransferableTypedData.fromList([hasil]));
}

Anti-Pattern dart:typed_data #

Overflow Diam-diam #

import 'dart:typed_data';

// ANTI-PATTERN: asumsi tidak ada overflow
Uint8List bytes = Uint8List(1);
bytes[0] = 300; // ✗ tidak error, tapi nilai jadi 44 (300 & 0xFF = 44)!
print(bytes[0]); // 44 — bukan 300!

// BENAR: validasi sebelum assign
void setByteAman(Uint8List buf, int indeks, int nilai) {
  if (nilai < 0 || nilai > 255) {
    throw RangeError.range(nilai, 0, 255, 'nilai');
  }
  buf[indeks] = nilai;
}

Membuat Salinan saat View Sudah Cukup #

import 'dart:typed_data';

final besar = Uint8List(1000000); // 1MB

// ANTI-PATTERN: membuat salinan hanya untuk baca subset
Uint8List subset = Uint8List.fromList(besar.sublist(100, 200)); // ✗ copy!

// BENAR: gunakan view (tanpa alokasi memori baru)
Uint8List subsetView = Uint8List.sublistView(besar, 100, 200); // ✓ view
// atau: besar.buffer.asUint8List(100, 100)

Ringkasan #

  • Uint8List adalah tipe yang paling sering digunakan — merepresentasikan raw bytes dan jauh lebih efisien dari List<int> untuk data biner karena tidak ada boxing overhead.
  • Overflow diam-diam adalah gotcha terbesar — Uint8List[i] = 300 tidak error, tapi nilai menjadi 44 (300 % 256). Selalu validasi nilai sebelum assign jika datang dari input eksternal.
  • ByteData untuk akses multi-type ke buffer yang sama — baca int32 dari byte offset tertentu, tulis float64, dll. Sangat berguna untuk parsing format biner.
  • Endianness wajib diperhatikan saat bekerja dengan protokol jaringan (big endian) dan file format (biasanya little endian). Selalu eksplisit: Endian.big atau Endian.little.
  • Shared ByteBuffer — semua view ke buffer yang sama memungkinkan zero-overhead konversi tipe: buffer.asUint8List(), buffer.asUint32List(), buffer.asFloat64List() semuanya melihat data yang sama.
  • Uint8List.sublistView bukan sublist untuk membaca subset tanpa alokasi memori baru — sublist membuat salinan, sublistView membuat view.
  • TransferableTypedData untuk transfer antar Isolate tanpa copy — krusial untuk image processing dan data besar yang diproses di background.
  • Image processing dengan Uint8List sangat efisien — akses pixel sebagai pixels[y * width * 4 + x * 4] untuk RGBA, dan iterasi 4 byte sekaligus.
  • Float32List dan Float64List untuk komputasi numerik dan machine learning — data point, koordinat, dan fitur vektor tersimpan efisien tanpa boxing overhead.
  • Gunakan dart:typed_data kapanpun bekerja dengan: protokol jaringan biner, format file biner, kriptografi, image/audio processing, atau transfer data ke/dari native code via FFI.

← Sebelumnya: dart:collection   Berikutnya: Isolate →

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