Dart と Flutter の連携 — 共有ロジックとプラットフォーム

Dart は Flutter の心臓部である — 1 つの言語で UI、ロジック、ネイティブ通信を駆動する。

1. 学べること


2. 開発者のリアルな物語

(1) 課題:CLI と Flutter アプリがそれぞれ別々のビジネスロジックを実装

Bob の DataPipeline には 2 つのフロントエンドがある:CLI ツール(開発者向け)と Flutter ダッシュボード(SaaS 顧客がレポートを閲覧するため)。2 つのシステムがそれぞれのデータ処理ロジックを実装したため、税計算バグの修正に 2 か所での変更が必要だった。あるとき 1 か所の変更が漏れて、CLI レポートは正しいがダッシュボードが間違っている状態になり、顧客の苦情につながった。

(2) 解決策:共有ロジックパッケージ

コアビジネスロジックを純粋な Dart パッケージに抽出し、CLI と Flutter の両方がそれに依存する。バグ修正は 1 か所だけ変更すればよくなる。

100%
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 ライブラリ]
TEXT
> 出力: ローカルの DartPad または `dart run` で実行してください。この Dart コースの全例は Dart 3.x / Flutter 3.x ベースです。SDK バージョンにより結果が多少異なる場合があります。

(3) 効果


3. Flutter アーキテクチャにおける Dart の役割

(1) アーキテクチャ層

技術 責任
UI 層 Flutter Widget インターフェースレンダリングとインタラクション
ロジック層 Dart(純粋) ビジネスロジック、データ処理
プラットフォーム層 ネイティブ(Kotlin/Swift) プラットフォーム固有機能
通信層 MethodChannel / FFI Dart ↔ ネイティブブリッジ

▶ サンプル:Flutter プロジェクトにおける Dart ロジック

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});
}
TEXT
> 出力: ローカルの DartPad または `dart run` で実行してください。この Dart コースの全例は Dart 3.x / Flutter 3.x ベースです。SDK バージョンにより結果が多少異なる場合があります。

4. 共有ビジネスロジックパッケージ設計

(1) 純粋な Dart パッケージ構造

▶ サンプル:共有パッケージ構造

TEXT
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

▶ サンプル:バレルエクスポート

DART
// 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

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 つのコンシューマー

YAML
# 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

DART
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)側

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 関数の呼び出し

DART
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 デュアルフロントエンドアーキテクチャ

▶ サンプル:共有コア + デュアルフロントエンド

DART
// 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 共有アーキテクチャ

DART
// ============================================
// 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');
}
TEXT
> 出力: ローカルの DartPad または `dart run` で実行してください。この Dart コースの全例は Dart 3.x / Flutter 3.x ベースです。SDK バージョンにより結果が多少異なる場合があります。

出力:

TEXT
=== 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 依存関係を使用)。コアパッケージとコンシューマー間の同期が容易になります。別リポジトリはオープンソース配布に適しています。


📖 まとめ


📝 練習問題

  1. 基礎(難易度 ⭐):純粋な Dart パッケージ(dart create -t package-simple)を作成し、formatUSD 関数を含めてください。CLI と Flutter プロジェクトの両方から参照して呼び出します。
  2. 中級(難易度 ⭐⭐):DataPipeline コアパッケージの構造を設計してください:3 モデルクラス + 2 サービスクラス + 1 バレルエクスポート。pubspec.yaml に Flutter 依存がないことを保証し、dart test で実行可能なテストを書いてください。
  3. 挑戦(難易度 ⭐⭐⭐):抽象 DataStorage インターフェース(純粋な Dart)を実装し、dart:io(CLI)と SharedPreferences(Flutter)を使ってそれぞれ実装してください。コアパッケージはインターフェースのみに依存し、具体的な実装はコンシューマーが注入します。

← 前のレッスン | 次のレッスン →

Web-Tutorial.com

Web-Tutorial 技術チーム

複数の開発者によって共同維持されているプログラミングチュートリアルプラットフォーム。各チュートリアルは専門分野の開発者が執筆・レビューしています。正確で信頼性の高いコンテンツを目指しています — 問題を見つけた場合はお知らせください。

100%