Dart と Flutter の連携 — 共有ロジックとプラットフォーム
Dart は Flutter の心臓部である — 1 つの言語で UI、ロジック、ネイティブ通信を駆動する。
1. 学べること
- Flutter アーキテクチャにおける Dart 層の位置づけ
- 共有ビジネスロジックパッケージ(純粋な Dart パッケージ)の設計
- MethodChannel:Dart ↔ Native 通信
- FFI:Dart から C ライブラリを呼び出す
- Bob のシナリオ:CLI と Flutter ダッシュボードの両方にサービスを提供する DataPipeline コアロジック
2. 開発者のリアルな物語
(1) 課題:CLI と Flutter アプリがそれぞれ別々のビジネスロジックを実装
Bob の DataPipeline には 2 つのフロントエンドがある:CLI ツール(開発者向け)と Flutter ダッシュボード(SaaS 顧客がレポートを閲覧するため)。2 つのシステムがそれぞれのデータ処理ロジックを実装したため、税計算バグの修正に 2 か所での変更が必要だった。あるとき 1 か所の変更が漏れて、CLI レポートは正しいがダッシュボードが間違っている状態になり、顧客の苦情につながった。
(2) 解決策:共有ロジックパッケージ
コアビジネスロジックを純粋な Dart パッケージに抽出し、CLI と Flutter の両方がそれに依存する。バグ修正は 1 か所だけ変更すればよくなる。
graph TD A[DataPipeline Core<br/>純粋な Dart] --> B[CLI アプリ] A --> C[Flutter ダッシュボード] C --> D[MethodChannel] D --> E[iOS ネイティブ] D --> F[Android ネイティブ] C --> G[FFI] G --> H[C ライブラリ]
> 出力: ローカルの DartPad または `dart run` で実行してください。この Dart コースの全例は Dart 3.x / Flutter 3.x ベースです。SDK バージョンにより結果が多少異なる場合があります。
(3) 効果
- コアロジックは 1 か所で保守され、CLI と Flutter が自動的に同期
- 純粋な Dart パッケージはすべてのプラットフォーム(VM/Web/Native)で実行可能
- MethodChannel により Flutter はネイティブ機能(センサー、プッシュ通知など)にアクセス可能
3. Flutter アーキテクチャにおける Dart の役割
(1) アーキテクチャ層
| 層 | 技術 | 責任 |
|---|---|---|
| UI 層 | Flutter Widget | インターフェースレンダリングとインタラクション |
| ロジック層 | Dart(純粋) | ビジネスロジック、データ処理 |
| プラットフォーム層 | ネイティブ(Kotlin/Swift) | プラットフォーム固有機能 |
| 通信層 | MethodChannel / FFI | Dart ↔ ネイティブブリッジ |
▶ サンプル:Flutter プロジェクトにおける Dart ロジック
// このコードは CLI と Flutter 環境の両方で動作する
// lib/data_pipeline_core.dart
class OrderAnalyzer {
final double taxRate;
OrderAnalyzer({this.taxRate = 0.08});
double calculateTax(double amount) => amount * taxRate;
double calculateTotal(double amount) => amount * (1 + taxRate);
Map<String, double> groupByCategory(List<Order> orders) {
final result = <String, double>{};
for (final order in orders) {
result.update(order.category, (v) => v + order.amount, ifAbsent: () => order.amount);
}
return result;
}
}
class Order {
final String id;
final double amount;
final String category;
Order({required this.id, required this.amount, required this.category});
}
> 出力: ローカルの DartPad または `dart run` で実行してください。この Dart コースの全例は Dart 3.x / Flutter 3.x ベースです。SDK バージョンにより結果が多少異なる場合があります。
4. 共有ビジネスロジックパッケージ設計
(1) 純粋な Dart パッケージ構造
▶ サンプル:共有パッケージ構造
packages/
data_pipeline_core/
lib/
src/
models/
order.dart
product.dart
customer.dart
services/
analyzer.dart
aggregator.dart
transformer.dart
utils/
formatters.dart
validators.dart
data_pipeline_core.dart # バレルエクスポート
test/
analyzer_test.dart
aggregator_test.dart
pubspec.yaml
▶ サンプル:バレルエクスポート
// lib/data_pipeline_core.dart
// バレルファイル - すべての公開 API をエクスポート
// モデル
export 'src/models/order.dart';
export 'src/models/product.dart';
export 'src/models/customer.dart';
// サービス
export 'src/services/analyzer.dart';
export 'src/services/aggregator.dart';
export 'src/services/transformer.dart';
// ユーティリティ
export 'src/utils/formatters.dart';
export 'src/utils/validators.dart';
▶ サンプル:純粋な Dart パッケージの pubspec.yaml
name: data_pipeline_core
description: DataPipeline のコアビジネスロジック - 純粋な Dart、Flutter 依存なし
version: 1.0.0
environment:
sdk: ^3.0.0
dependencies:
json_annotation: ^4.8.0
dev_dependencies:
test: ^1.24.0
json_serializable: ^6.7.0
build_runner: ^2.4.0
▶ サンプル:2 つのコンシューマー
# CLI アプリ pubspec.yaml
dependencies:
data_pipeline_core:
path: ../packages/data_pipeline_core
args: ^2.4.2
# Flutter アプリ pubspec.yaml
dependencies:
data_pipeline_core:
path: ../packages/data_pipeline_core
flutter:
sdk: flutter
| 原則 | 説明 |
|---|---|
| Flutter 依存なし | pubspec.yaml は flutter に依存しない |
| dart:io の直接使用なし | ファイル/ネットワーク操作用の抽象インターフェースを使用 |
| UI コードなし | 純粋なデータ処理ロジック |
| 完全なテスト | Flutter ランタイムなしでテスト可能 |
5. MethodChannel
(1) Dart ↔ Native 通信
▶ サンプル:ネイティブ呼び出し MethodChannel
import 'package:flutter/services.dart';
class NativeService {
static const _channel = MethodChannel('com.datapipeline/native');
// ネイティブメソッドを呼び出す
Future<String> getDeviceId() async {
try {
final deviceId = await _channel.invokeMethod<String>('getDeviceId');
return deviceId ?? 'unknown';
} on PlatformException catch (e) {
print('Failed to get device ID: ${e.message}');
return 'error';
}
}
// 引数付きで呼び出す
Future<bool> saveToFile(String path, String content) async {
try {
final result = await _channel.invokeMethod<bool>('saveToFile', {
'path': path,
'content': content,
});
return result ?? false;
} on PlatformException catch (e) {
print('Failed to save: ${e.message}');
return false;
}
}
}
(2) ネイティブ側実装
▶ サンプル:Android(Kotlin)側
// Android 実装
class MainActivity : FlutterActivity() {
override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
super.configureFlutterEngine(flutterEngine)
MethodChannel(flutterEngine.dartExecutor.binaryMessenger,
"com.datapipeline/native").setMethodCallHandler { call, result ->
when (call.method) {
"getDeviceId" -> {
val deviceId = Settings.Secure.getString(
contentResolver, Settings.Secure.ANDROID_ID)
result.success(deviceId)
}
"saveToFile" -> {
val path = call.argument<String>("path")
val content = call.argument<String>("content")
// ファイル保存ロジック
result.success(true)
}
else -> result.notImplemented()
}
}
}
}
| 通信方法 | 方向 | データ型 | ユースケース |
|---|---|---|---|
| MethodChannel | Dart → Native | 標準型 | ネイティブメソッドの呼び出し |
| EventChannel | Native → Dart | Stream | ネイティブイベントストリーム |
| BasicMessageChannel | 双方向 | String/Bytes | 双方向メッセージ |
6. FFI — C ライブラリの呼び出し
▶ サンプル:dart:ffi による C 関数の呼び出し
import 'dart:ffi';
import 'package:ffi/ffi.dart';
// C 関数シグネチャ: double process_data(double* data, int length)
typedef ProcessDataNative = Double Function(Pointer<Double>, Int32);
typedef ProcessDataDart = double Function(Pointer<Double>, int);
void main() {
// 動的ライブラリをロード
final dylib = DynamicLibrary.open('libdatapipeline.so');
// 関数を検索
final processData = dylib.lookupFunction<ProcessDataNative, ProcessDataDart>('process_data');
// データを準備
final dataPtr = calloc<Double>(5);
for (var i = 0; i < 5; i++) {
dataPtr[i] = (i + 1) * 100.0;
}
// C 関数を呼び出す
final result = processData(dataPtr, 5);
print('Result from C: $result');
// メモリを解放
calloc.free(dataPtr);
}
| FFI コンセプト | 説明 |
|---|---|
DynamicLibrary |
動的リンクライブラリをロード |
lookupFunction |
C 関数を検索 |
Pointer<T> |
ネイティブメモリを指す |
calloc / free |
ネイティブメモリの割り当て/解放 |
NativeType |
C 型マッピング |
7. Bob のシナリオ:DataPipeline デュアルフロントエンドアーキテクチャ
▶ サンプル:共有コア + デュアルフロントエンド
// packages/data_pipeline_core/lib/src/services/analyzer.dart
class OrderAnalyzer {
final double taxRate;
OrderAnalyzer({this.taxRate = 0.08});
AnalysisResult analyze(List<Order> orders) {
final completed = orders.where((o) => o.status == 'completed').toList();
final revenue = completed.fold<double>(0, (s, o) => s + o.amount);
final tax = revenue * taxRate;
final byCategory = <String, double>{};
for (final o in completed) {
byCategory.update(o.category, (v) => v + o.amount, ifAbsent: () => o.amount);
}
return AnalysisResult(
totalOrders: orders.length,
completedOrders: completed.length,
revenue: revenue,
tax: tax,
revenueByCategory: byCategory,
);
}
}
class AnalysisResult {
final int totalOrders;
final int completedOrders;
final double revenue;
final double tax;
final Map<String, double> revenueByCategory;
AnalysisResult({
required this.totalOrders,
required this.completedOrders,
required this.revenue,
required this.tax,
required this.revenueByCategory,
});
double get totalWithTax => revenue + tax;
double get averageOrderValue => completedOrders > 0 ? revenue / completedOrders : 0;
}
class Order {
final String id;
final double amount;
final String status;
final String category;
Order({required this.id, required this.amount, required this.status, required this.category});
}
// CLI コンシューマー
void main() {
final analyzer = OrderAnalyzer(taxRate: 0.08);
final orders = [
Order(id: 'ORD-001', amount: 1500.0, status: 'completed', category: 'Electronics'),
Order(id: 'ORD-002', amount: 890.0, status: 'completed', category: 'Clothing'),
];
final result = analyzer.analyze(orders);
print('Revenue: \$${result.revenue.toStringAsFixed(2)} USD');
print('Tax: \$${result.tax.toStringAsFixed(2)} USD');
}
// Flutter コンシューマーは同じ OrderAnalyzer を使うが、
// print() の代わりに Widget で結果をレンダリングする
8. 完全なサンプル:DataPipeline 共有アーキテクチャ
// ============================================
// DataPipeline 共有アーキテクチャ
// 純粋な Dart コア + CLI コンシューマー + Flutter コンシューマー
// ============================================
// ---- コアライブラリ(純粋な Dart) ----
// lib/src/models/order.dart
class Order {
final String id;
final double amount;
final String status;
final String category;
final String region;
const Order({
required this.id,
required this.amount,
required this.status,
required this.category,
this.region = 'US',
});
double get tax => amount * _taxRate(region);
double get total => amount + tax;
String get formatAmount => '\$${amount.toStringAsFixed(2)} USD';
static double _taxRate(String region) => switch (region) {
'US' => 0.08,
'EU' => 0.20,
'UK' => 0.15,
_ => 0.10,
};
}
// lib/src/services/analyzer.dart
class OrderAnalyzer {
const OrderAnalyzer();
AnalysisResult analyze(List<Order> orders) {
final completed = orders.where((o) => o.status == 'completed').toList();
final revenue = completed.fold<double>(0, (s, o) => s + o.amount);
final tax = completed.fold<double>(0, (s, o) => s + o.tax);
final total = revenue + tax;
final byCategory = <String, CategoryResult>{};
for (final o in completed) {
byCategory.update(
o.category,
(v) => v.addOrder(o),
ifAbsent: () => CategoryResult.fromOrder(o),
);
}
return AnalysisResult(
totalOrders: orders.length,
completedOrders: completed.length,
revenue: revenue,
tax: tax,
total: total,
byCategory: byCategory,
);
}
}
class CategoryResult {
final int count;
final double revenue;
const CategoryResult({required this.count, required this.revenue});
factory CategoryResult.fromOrder(Order order) =>
CategoryResult(count: 1, revenue: order.amount);
CategoryResult addOrder(Order order) =>
CategoryResult(count: count + 1, revenue: revenue + order.amount);
double get average => count > 0 ? revenue / count : 0;
}
class AnalysisResult {
final int totalOrders;
final int completedOrders;
final double revenue;
final double tax;
final double total;
final Map<String, CategoryResult> byCategory;
const AnalysisResult({
required this.totalOrders,
required this.completedOrders,
required this.revenue,
required this.tax,
required this.total,
required this.byCategory,
});
double get averageOrderValue => completedOrders > 0 ? revenue / completedOrders : 0;
}
// ---- CLI コンシューマー ----
void main() {
final analyzer = OrderAnalyzer();
final orders = [
Order(id: 'ORD-001', amount: 1500.0, status: 'completed', category: 'Electronics'),
Order(id: 'ORD-002', amount: 890.0, status: 'completed', category: 'Clothing'),
Order(id: 'ORD-003', amount: 3200.0, status: 'pending', category: 'Electronics'),
Order(id: 'ORD-004', amount: 2100.0, status: 'completed', category: 'Electronics', region: 'EU'),
];
final result = analyzer.analyze(orders);
print('=== DataPipeline Analytics ===');
print('Orders: ${result.completedOrders}/${result.totalOrders}');
print('Revenue: \$${result.revenue.toStringAsFixed(2)} USD');
print('Tax: \$${result.tax.toStringAsFixed(2)} USD');
print('Total: \$${result.total.toStringAsFixed(2)} USD');
print('Average: \$${result.averageOrderValue.toStringAsFixed(2)} USD');
print('\nBy Category:');
final sorted = result.byCategory.entries.toList()
..sort((a, b) => b.value.revenue.compareTo(a.value.revenue));
for (final entry in sorted) {
final cat = entry.value;
print(' ${entry.key}: ${cat.count} orders, '
'\$${cat.revenue.toStringAsFixed(2)} USD (avg: \$${cat.average.toStringAsFixed(2)})');
}
print('\n--- Platform Compatibility ---');
print('Core logic: Pure Dart (CLI + Flutter + Web)');
print('CLI: dart:io + args package');
print('Flutter: Widgets + MethodChannel + FFI');
}
> 出力: ローカルの DartPad または `dart run` で実行してください。この Dart コースの全例は Dart 3.x / Flutter 3.x ベースです。SDK バージョンにより結果が多少異なる場合があります。
出力:
=== DataPipeline Analytics ===
Orders: 3/4
Revenue: $4490.00 USD
Tax: $798.00 USD
Total: $5288.00 USD
Average: $1496.67 USD
By Category:
Electronics: 2 orders, $3600.00 USD (avg: $1800.00)
Clothing: 1 orders, $890.00 USD (avg: $890.00)
--- Platform Compatibility ---
Core logic: Pure Dart (CLI + Flutter + Web)
CLI: dart:io + args package
Flutter: Widgets + MethodChannel + FFI
❓ よくある質問
Q: 純粋な Dart パッケージは Flutter Web で動作しますか? A: はい、dart:io を使用しない限り動作します。Web プラットフォームは dart:io をサポートしません。直接のファイル/ネットワーク操作を置き換えるには、抽象インターフェース(http パッケージの Client など)を使用してください。
Q: MethodChannel 経由のデータ転送にサイズ制限はありますか? A: 厳密な制限はありませんが、大きなオブジェクトにはシリアライズ/デシリアライズが必要で、パフォーマンスが低下します。大きなデータには FFI や一時ファイルの使用を検討してください。
Q: FFI は iOS をサポートしますか? A: はい。iOS では .dylib または .フレームワーク をロードします。Flutter 3.0+ の FFI はすべての主要プラットフォームをサポートします。
Q: MethodChannel と FFI はいつ使い分けるべきですか? A: MethodChannel はプラットフォーム API の呼び出し(センサー、プッシュ通知、ファイル保存)に適し、FFI は C ライブラリの呼び出し(画像処理、暗号化、データベースエンジン)に適しています。
Q: 共有パッケージのテストには Flutter 環境が必要ですか? A: いいえ。純粋な Dart パッケージは
dart testだけでテストでき、Flutter に依存しません。これは共有パッケージを抽出する大きな利点です。
Q: 共有パッケージが誤って Flutter 依存を導入していないことを保証するには? A: pubspec.yaml で flutter に依存せず、flutter パッケージをインポートしないでください。CI では
flutter testではなくdart testでテストを実行して検証してください。
Q: DataPipeline コアパッケージはモノレポと別リポジトリのどちらを使うべきですか? A: モノレポを推奨します(Melos または path 依存関係を使用)。コアパッケージとコンシューマー間の同期が容易になります。別リポジトリはオープンソース配布に適しています。
📖 まとめ
- Dart は Flutter のコア言語であり、UI とロジック層の両方を駆動する
- 共有ロジックパッケージ(純粋な Dart)により CLI と Flutter が同じビジネスコードを再利用できる
- MethodChannel はプラットフォーム固有機能のために Dart ↔ Native 通信を実装する
- FFI は C ライブラリを直接呼び出し、高性能計算と既存 C コードの統合に適する
- アーキテクチャの原則:純粋な Dart のコアロジック、プラットフォームごとにカスタマイズされた UI 層
📝 練習問題
- 基礎(難易度 ⭐):純粋な Dart パッケージ(
dart create -t package-simple)を作成し、formatUSD関数を含めてください。CLI と Flutter プロジェクトの両方から参照して呼び出します。 - 中級(難易度 ⭐⭐):DataPipeline コアパッケージの構造を設計してください:3 モデルクラス + 2 サービスクラス + 1 バレルエクスポート。pubspec.yaml に Flutter 依存がないことを保証し、
dart testで実行可能なテストを書いてください。 - 挑戦(難易度 ⭐⭐⭐):抽象
DataStorageインターフェース(純粋な Dart)を実装し、dart:io(CLI)と SharedPreferences(Flutter)を使ってそれぞれ実装してください。コアパッケージはインターフェースのみに依存し、具体的な実装はコンシューマーが注入します。