Typed Data #

dart:typed_data provides high-performance typed arrays for numeric and binary data — unlike regular List<int> which stores boxed objects, typed lists store values directly in contiguous memory, like arrays in C. This makes them far more efficient for image processing, network protocols, cryptography, audio processing, and all cases involving large amounts of binary data.

Why Typed Data? #

// Regular List<int> — every element is a Dart object (boxed)
// Overhead: pointer (8 bytes) + object header + the integer value
List<int> listBiasa = [1, 2, 3, 4, 5];

// Uint8List — a byte array directly in memory, no boxing
// Efficient: every element is just 1 byte, arranged contiguously
Uint8List typedList = Uint8List.fromList([1, 2, 3, 4, 5]);

// Size difference for 1 million integers:
// List<int>: ~8MB (pointers) + per-element overhead
// Int32List: 4MB (4 bytes per element, flat)
// Uint8List: 1MB (1 byte per element, flat)

Typed List Types #

ClassElement TypeSize/ElementValue Range
Uint8ListUnsigned integer1 byte0 – 255
Int8ListSigned integer1 byte-128 – 127
Uint16ListUnsigned integer2 bytes0 – 65,535
Int16ListSigned integer2 bytes-32,768 – 32,767
Uint32ListUnsigned integer4 bytes0 – 4,294,967,295
Int32ListSigned integer4 bytes-2,147,483,648 – 2,147,483,647
Uint64ListUnsigned integer8 bytes0 – 2⁶⁴-1
Int64ListSigned integer8 bytes-2⁶³ – 2⁶³-1
Float32ListFloating point4 bytesIEEE 754 single
Float64ListFloating point8 bytesIEEE 754 double

Uint8List — the Most Commonly Used #

Uint8List is the most frequently used type — it represents raw bytes:

import 'dart:typed_data';

// Creating a 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);     // a view into an existing ByteBuffer

// Access and modification — like List<int>
final bytes = Uint8List(5);
bytes[0] = 255;   // ✓ 0-255
bytes[1] = 128;
bytes[2] = 0;
// bytes[0] = 256; // ✗ the value is clamped: becomes 0 (silent overflow!)
// bytes[0] = -1;  // ✗ becomes 255 (wrapping)

// Properties
print(bytes.length);          // 5
print(bytes.lengthInBytes);   // 5 (same for Uint8List)
print(bytes.elementSizeInBytes); // 1 byte per element
print(bytes.buffer);          // the underlying ByteBuffer

// Slice (a view, not a copy)
final slice = bytes.sublist(1, 3); // elements at indices 1 and 2

String ↔ Bytes Conversion #

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

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

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

// As a regular List<int>
List<int> asList = bytesUtf8.toList();

// To a hex string (useful for 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'

// From a 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 — Multi-Type Access to a Buffer #

ByteData lets you read and write various numeric types to the same buffer with control over endianness:

import 'dart:typed_data';

// Create a ByteData
final data = ByteData(16); // a 16-byte buffer

// Write various types
data.setUint8(0, 0xFF);                        // 1 byte at offset 0
data.setUint16(1, 0xABCD, Endian.big);         // 2 bytes at offset 1
data.setInt32(3, -1234567, Endian.little);     // 4 bytes at offset 3
data.setFloat64(7, 3.14159, Endian.little);    // 8 bytes at offset 7

// Read them back
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...

// All available getters/setters:
// getUint8, setUint8
// getUint16, setUint16
// getUint32, setUint32
// getUint64, setUint64
// getInt8, setInt8
// getInt16, setInt16
// getInt32, setInt32
// getInt64, setInt64
// getFloat32, setFloat32
// getFloat64, setFloat64

// ByteData from a 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 is the byte order in multi-byte integer representations:

flowchart LR
    N["Number: 0x12345678\n(305419896 decimal)"]
    N --> BE["Big Endian\n(Network byte order)\n[0x12, 0x34, 0x56, 0x78]\nMost significant byte first"]
    N --> LE["Little Endian\n(x86, ARM default)\n[0x78, 0x56, 0x34, 0x12]\nLeast significant byte first"]
import 'dart:typed_data';

final nilai = 0x12345678; // 305419896

// Big Endian — most significant byte first
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 — least significant byte first
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 — the current platform's endianness
final host = ByteData(4);
host.setUint32(0, nilai, Endian.host);

// Tips:
// Network protocols (TCP/IP, HTTP) → Big Endian (Network byte order)
// Most file formats → Little Endian
// x86/ARM → Little Endian natively

ByteBuffer — the Underlying Buffer #

All typed lists share one ByteBuffer — this lets you view the same data from different type perspectives:

import 'dart:typed_data';

// Create a 16-byte buffer
final buffer = Uint8List(16).buffer;

// View the same buffer as different types
final asUint8 = buffer.asUint8List();    // 16 elements (1 byte each)
final asUint16 = buffer.asUint16List();  // 8 elements (2 bytes each)
final asUint32 = buffer.asUint32List();  // 4 elements (4 bytes each)
final asFloat64 = buffer.asFloat64List(); // 2 elements (8 bytes each)

// Write data as uint32
final uint32 = buffer.asUint32List();
uint32[0] = 0xDEADBEEF;

// Read it back as individual bytes
print(asUint8[0].toRadixString(16)); // the first byte of 0xDEADBEEF

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

Real-World Uses #

Building Network Protocol Packets #

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

// Custom packet format:
// [0-1]: magic bytes (0xDA 0x7A)
// [2]:   version (1 byte)
// [3]:   message type (1 byte)
// [4-7]: payload length (4 bytes, 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);                                  // version
  paket.setUint8(3, tipePesan);                          // type
  paket.setUint32(4, payloadBytes.length, Endian.big);  // payload length

  // 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('Packet too short');

  final bd = ByteData.sublistView(data);

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

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

  if (data.length < 8 + panjangPayload) {
    throw FormatException('Incomplete payload');
  }

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

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

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

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

Image Processing — Pixel Manipulation #

import 'dart:typed_data';

// An RGBA image: every pixel = 4 bytes (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);

  // The pixel offset in the buffer (row-column to a linear index)
  int _offset(int x, int y) => (y * lebar + x) * 4;

  // Set an RGBA pixel
  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 a 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 — average the 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]; // keep the alpha
    }
    return hasil;
  }

  // Invert colors
  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 (not inverted)
    }
    return hasil;
  }
}

Parsing Binary Formats (BMP, binary files) #

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

// Parse a BMP file header
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(),   // negative = top-down
    'bitsPerPixel': bitsPerPixel,
    'topDown': height < 0,
  };
}

Transferring Between Isolates #

Uint8List can be transferred between Isolates zero-copy using TransferableTypedData:

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

// Zero-copy transfer — ownership is moved, not copied
Future<Uint8List> prosesGambarDiIsolate(Uint8List pixelData) async {
  // Wrap as TransferableTypedData
  final transferable = TransferableTypedData.fromList([pixelData]);

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

  // Receive the result from the 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();
  // Process the image (grayscale, blur, etc.)
  final hasil = _grayscale(pixels);

  // Send back as TransferableTypedData (zero-copy)
  kirimKe.send(TransferableTypedData.fromList([hasil]));
}

dart:typed_data Anti-Patterns #

Silent Overflow #

import 'dart:typed_data';

// ANTI-PATTERN: assuming there's no overflow
Uint8List bytes = Uint8List(1);
bytes[0] = 300; // ✗ no error, but the value becomes 44 (300 & 0xFF = 44)!
print(bytes[0]); // 44 — not 300!

// CORRECT: validate before assigning
void setByteAman(Uint8List buf, int indeks, int nilai) {
  if (nilai < 0 || nilai > 255) {
    throw RangeError.range(nilai, 0, 255, 'nilai');
  }
  buf[indeks] = nilai;
}

Making Copies When a View Is Enough #

import 'dart:typed_data';

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

// ANTI-PATTERN: making a copy just to read a subset
Uint8List subset = Uint8List.fromList(besar.sublist(100, 200)); // ✗ copy!

// CORRECT: use a view (no new memory allocation)
Uint8List subsetView = Uint8List.sublistView(besar, 100, 200); // ✓ view
// or: besar.buffer.asUint8List(100, 100)

Summary #

  • Uint8List is the most commonly used type — it represents raw bytes and is far more efficient than List<int> for binary data because there’s no boxing overhead.
  • Silent overflow is the biggest gotchaUint8List[i] = 300 doesn’t error, but the value becomes 44 (300 % 256). Always validate values before assigning when they come from external input.
  • ByteData for multi-type access to the same buffer — read an int32 at a byte offset, write a float64, etc. Very useful for parsing binary formats.
  • Endianness must be considered when working with network protocols (big endian) and file formats (usually little endian). Always be explicit: Endian.big or Endian.little.
  • Shared ByteBuffer — all views into the same buffer enable zero-overhead type conversion: buffer.asUint8List(), buffer.asUint32List(), buffer.asFloat64List() all see the same data.
  • Uint8List.sublistView, not sublist, to read subsets without new memory allocation — sublist makes a copy, sublistView makes a view.
  • TransferableTypedData for copy-free transfers between Isolates — crucial for image processing and large data processed in the background.
  • Image processing with Uint8List is very efficient — access pixels as pixels[y * width * 4 + x * 4] for RGBA, and iterate 4 bytes at a time.
  • Float32List and Float64List for numeric computation and machine learning — data points, coordinates, and vector features stored efficiently without boxing overhead.
  • Use dart:typed_data whenever working with: binary network protocols, binary file formats, cryptography, image/audio processing, or transferring data to/from native code via FFI.

← Previous: Collection   Next: Isolate →

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