対象者
- DartやFlutterでのアプリ開発において、バイナリデータの操作や変換に関心がある開発者
- ファイル操作、ネットワーク通信、データのエンコードやデコードなど、低レベルのデータ処理について学びたいと考えているプログラマー
- パフォーマンスを意識した効率的なデータ処理方法を探求しているエンジニア
はじめに
プログラミングの世界では、データは生命線です。特に、DartやFlutterを使ったアプリ開発では、バイナリデータの扱い方がアプリのパフォーマンスと直結します。画像を扱う、ファイルを操作する、サーバーとデータをやり取りする…これら全てのタスクに共通して必要なのが、バイナリデータの操作です。しかし、多くの開発者が直面するのは、Uint8List
、List<int>
、ByteData
などのクラス間でのデータ変換の複雑さです。
本記事では、これらのクラス間での変換方法を明快に解説し、あなたのDart/Flutter開発を一歩前進させるための知識を提供します。
Dartのバイナリ関連のクラス
DartやFlutterで扱われるバイトデータを操作するための主要なクラスには、Uint8List
、List<int>
、ByteData
などがあります。これらは、バイナリデータやファイルシステムの操作、ネットワーク通信などで頻繁に使用されます。それぞれのクラスの特徴を以下に説明します。
Uint8List
Uint8List
は、固定長の8ビット符号なし整数のリストです。これはTypedData
ライブラリの一部で、バイナリデータを効率的に扱うために設計されています。- メモリ上で連続した領域を占めるため、大量のデータを扱う際にパフォーマンスの面で有利です。
- バイトデータを直接操作する場合や、ファイル、ネットワーク通信からの生データの読み書きに適しています。
List<int>
List<int>
は、整数のリストで、Dartの基本的なリスト構造です。各要素はint
型で、これをバイトデータとして使用することができますが、各要素がバイトサイズ(0-255)に収まることを自分で保証する必要があります。List<int>
は汎用的で、Uint8List
よりも柔軟性がありますが、バイトデータを扱う際にはUint8List
の方が効率的です。- バイトデータ以外の目的で整数のリストを扱う場合にも使用されます。
ByteData
ByteData
は、バイトバッファ上の固定サイズのビューを提供します。これを使用すると、バイトデータ内の任意の位置から異なるデータ型(例えば、8ビット整数、32ビット浮動小数点数など)を読み書きできます。- エンディアン(バイトオーダー)を指定してデータを読み書きする機能を提供します。これは、異なるプラットフォーム間でデータを交換する際に重要になります。
- バイナリデータを構造化して扱う場合や、特定のフォーマットのデータを解析、生成する場合に便利です。
ByteBuffer
ByteBuffer
は、バイトデータの生のバッファを表します。Uint8List
やByteData
は、内部的にはByteBuffer
を使用しています。- 直接
ByteBuffer
を使用することは少なく、通常はUint8List
やByteData
を介してアクセスします。 - バッファ全体を一括で操作する必要がある場合に使用されます。
これらのクラスは、Dartのdart:typed_data
ライブラリによって提供されており、バイナリデータの効率的な操作を可能にします。用途に応じて適切なクラスを選択することが重要です。例えば、生のバイトデータを扱う場合はUint8List
、複雑なバイナリ構造を解析する場合はByteData
が適しています。
Base64
Base64は、バイナリデータをテキスト形式で表現するためのエンコーディング方式です。データを64種類の印字可能なASCII文字に変換することで、バイナリデータをテキストベースのメディアで安全に転送・保存することが可能になります。Dartでは、dart:convert
ライブラリを通じてBase64のエンコードとデコード機能を提供しています。これにより、画像、ファイル、あるいは任意のバイナリデータを文字列として扱うことができるようになります。
バイトデータ間の変換
概要
Uint8List を中心にして、データ変換をすると良い気します。
- Uint8List ↔ List<int>
- Uint8List ↔ ByteData
- ByteData↔ByteBuffer
準備
import 'dart:typed_data';
Uint8List と List の変換
test('Uint8List と List<int> の変換', () {
Uint8List uint8list1 = Uint8List.fromList([0, 1, 2, 3]);
List<int> listInt = uint8list1;
Uint8List uint8list2 = Uint8List.fromList(listInt);
expect(uint8list1, uint8list2);
});
Uint8List と ByteData の変換
test('Uint8List と ByteData の変換', () {
Uint8List uint8list1 = Uint8List.fromList([0, 1, 2, 3]);
ByteData byteData = ByteData.view(uint8list1.buffer);
Uint8List uint8list2 = byteData.buffer.asUint8List();
expect(uint8list1, uint8list2);
});
Uint8List と ByteBuffer の変換
test('Uint8List と ByteBuffer の変換', () {
Uint8List uint8list1 = Uint8List.fromList([0, 1, 2, 3]);
ByteBuffer buffer = uint8list1.buffer;
Uint8List uint8list2 = buffer.asUint8List();
expect(uint8list1, uint8list2);
});
Uint8List、List、および String の相互変換
test('Uint8List、List<int>、および String の相互変換', () {
String string1 = "Hello, World!";
Uint8List uint8list1 = Uint8List.fromList(utf8.encode(string1));
List<int> listInt = Uint8List.fromList(utf8.encode(string1));
String string2 = utf8.decode(uint8list1);
expect(string1, string2);
String string3 = utf8.decode(listInt);
expect(string1, string3);
});
ByteData と String の変換
test('ByteData と String の変換', () {
ByteData byteData1 = ByteData(5);
byteData1.setUint8(0, 72); // H
byteData1.setUint8(1, 101); // e
byteData1.setUint8(2, 108); // l
byteData1.setUint8(3, 108); // l
byteData1.setUint8(4, 111); // o
Uint8List uint8list1 = byteData1.buffer.asUint8List();
String string1 = utf8.decode(uint8list1);
expect(string1, 'Hello');
ByteData byteData2 = ByteData.sublistView(Uint8List.fromList(utf8.encode(string1)));
expect(byteData1.lengthInBytes, byteData2.lengthInBytes);
for (int i = 0; i < byteData1.lengthInBytes; i++) {
expect(byteData1.getUint8(i), byteData2.getUint8(i));
}
});
File と Uint8List の変換
test('File と Uint8List の変換', () async {
final file = File('temp.txt');
await file.writeAsString('Hello, World!');
Uint8List uint8list1 = await file.readAsBytes();
await file.writeAsBytes(uint8list1);
Uint8List uint8list2 = await file.readAsBytes();
expect(utf8.decode(uint8list2), 'Hello, World!');
String string = await file.readAsString();
expect(string, 'Hello, World!');
await file.delete();
});
String と Base64 の変換
test('String と Base64 の変換', () {
String originalString = 'Hello, World!';
String encoded = base64Encode(utf8.encode(originalString));
String decoded = utf8.decode(base64Decode(encoded));
expect(decoded, originalString);
});
Uint8List と Stream<int>, Stream<List<int>> の変換
test('Uint8List と Stream<int>, Stream<List<int>> の変換', () {
Uint8List uint8list = Uint8List.fromList([0, 1, 2, 3]);
Stream<int> streamInt = Stream.fromIterable(uint8list);
expect(
streamInt,
emitsInOrder([0, 1, 2, 3, emitsDone]),
);
Stream<List<int>> streamListInt = Stream.fromIterable([uint8list]);
expect(
streamListInt,
emitsInOrder([
[0, 1, 2, 3],
emitsDone
]),
);
});
Q&A
Q1: Uint8List の挙動がなんかおかしい
A1: 「import ‘dart:nativewrappers/_internal/vm/lib/typed_data_patch.dart’;」でインポートしてないか確認してください。そうなら「import ‘dart:typed_data’;」に修正することで、正しい挙動になります。この問題は、間違ったライブラリをインポートすることで発生するため、正しいパッケージを使用することで解決できます。
まとめ
DartでバイナリやString,Fileの変換処理を迷うときがあるので、一挙にまとめてみました。参考になれば幸いです!