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 #
| Class | Element Type | Size/Element | Value Range |
|---|---|---|---|
Uint8List | Unsigned integer | 1 byte | 0 – 255 |
Int8List | Signed integer | 1 byte | -128 – 127 |
Uint16List | Unsigned integer | 2 bytes | 0 – 65,535 |
Int16List | Signed integer | 2 bytes | -32,768 – 32,767 |
Uint32List | Unsigned integer | 4 bytes | 0 – 4,294,967,295 |
Int32List | Signed integer | 4 bytes | -2,147,483,648 – 2,147,483,647 |
Uint64List | Unsigned integer | 8 bytes | 0 – 2⁶⁴-1 |
Int64List | Signed integer | 8 bytes | -2⁶³ – 2⁶³-1 |
Float32List | Floating point | 4 bytes | IEEE 754 single |
Float64List | Floating point | 8 bytes | IEEE 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 #
Uint8Listis the most commonly used type — it represents raw bytes and is far more efficient thanList<int>for binary data because there’s no boxing overhead.- Silent overflow is the biggest gotcha —
Uint8List[i] = 300doesn’t error, but the value becomes 44 (300 % 256). Always validate values before assigning when they come from external input.ByteDatafor 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.bigorEndian.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, notsublist, to read subsets without new memory allocation —sublistmakes a copy,sublistViewmakes a view.TransferableTypedDatafor 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_datawhenever working with: binary network protocols, binary file formats, cryptography, image/audio processing, or transferring data to/from native code via FFI.