Dart: Dart and Flutter Collaboration
Last updated: 2026-08-26
Dart is the heart of Flutter — a single language that drives UI, logic, and native communication.
1. What You Will Learn
- Dart layer's positioning in the Flutter architecture
- Designing a shared business logic package (pure Dart package)
- MethodChannel: Dart ↔ Native communication
- FFI: Dart calling C libraries
- Bob scenario: DataPipeline core logic serving both CLI and Flutter dashboard
2. A Developer's Real Story
(1) Pain Point: CLI and Flutter App Each Implementing Separate Business Logic
Bob's DataPipeline has two front-ends: a CLI tool (for developers) and a Flutter dashboard (for SaaS customers to view reports). The two systems implemented their own data processing logic, leading to: fixing a tax calculation bug required changes in two places; missing one change once caused the CLI report to be correct but the dashboard to be wrong, resulting in customer complaints.
(2) Solution: Shared Logic Package
Extract the core business logic into a pure Dart package, which both the CLI and Flutter depend on. Fixing a bug only requires a change in one place.
graph TD A[DataPipeline Core<br/>pure Dart] --> B[CLI App] A --> C[Flutter Dashboard] C --> D[MethodChannel] D --> E[iOS Native] D --> F[Android Native] C --> G[FFI] G --> H[C Library]
> **Output:** Run locally in DartPad or with `dart run`. All examples in the Dart course are based on Dart 3.x / Flutter 3.x; results may vary slightly due to SDK versions.
(3) Benefits
- Core logic is maintained in a single place, with CLI and Flutter automatically synchronized
- The pure Dart package can run on all platforms (VM/Web/Native)
- MethodChannel allows Flutter to access native capabilities (sensors, push notifications, etc.)
3. Dart's Role in the Flutter Architecture
(1) Architecture Layers
| Layer | Technology | Responsibility |
|---|---|---|
| UI Layer | Flutter Widget | Interface rendering and interaction |
| Logic Layer | Dart (pure) | Business logic, data processing |
| Platform Layer | Native (Kotlin/Swift) | Platform-specific functions |
| Communication Layer | MethodChannel / FFI | Dart ↔ Native bridge |
▶ Example
> **Output:** Run locally in DartPad or with `dart run`. All examples in the Dart course are based on Dart 3.x / Flutter 3.x; results may vary slightly due to SDK versions.
: Dart logic in a Flutter project
// This code runs in BOTH CLI and Flutter environments
// 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});
}
> **Output:** Run locally in DartPad or with `dart run`. All examples in the Dart course are based on Dart 3.x / Flutter 3.x; results may vary slightly due to SDK versions.
4. Shared Business Logic Package Design
(1) Pure Dart Package Structure
▶ Example
> **Output:** Run locally in DartPad or with `dart run`. All examples in the Dart course are based on Dart 3.x / Flutter 3.x; results may vary slightly due to SDK versions.
: Shared package structure
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 # Barrel export
test/
analyzer_test.dart
aggregator_test.dart
pubspec.yaml
```text
```text
> **Output:** Run locally in DartPad or with `dart run`. All examples in the Dart course are based on Dart 3.x / Flutter 3.x; results may vary slightly due to SDK versions.
▶ Example
> **Output:** Run locally in DartPad or with `dart run`. All examples in the Dart course are based on Dart 3.x / Flutter 3.x; results may vary slightly due to SDK versions.
: Barrel export
// lib/data_pipeline_core.dart
// Barrel file - exports all public APIs
// Models
export 'src/models/order.dart';
export 'src/models/product.dart';
export 'src/models/customer.dart';
// Services
export 'src/services/analyzer.dart';
export 'src/services/aggregator.dart';
export 'src/services/transformer.dart';
// Utils
export 'src/utils/formatters.dart';
export 'src/utils/validators.dart';
> **Output:** Run locally in DartPad or with `dart run`. All examples in the Dart course are based on Dart 3.x / Flutter 3.x; results may vary slightly due to SDK versions.
▶ Example
> **Output:** Run locally in DartPad or with `dart run`. All examples in the Dart course are based on Dart 3.x / Flutter 3.x; results may vary slightly due to SDK versions.
: pubspec.yaml for pure Dart package
name: data_pipeline_core
description: Core business logic for DataPipeline - pure Dart, no Flutter dependency
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
▶ Example
> **Output:** Run locally in DartPad or with `dart run`. All examples in the Dart course are based on Dart 3.x / Flutter 3.x; results may vary slightly due to SDK versions.
: Two consumers
# CLI app pubspec.yaml
dependencies:
data_pipeline_core:
path: ../packages/data_pipeline_core
args: ^2.4.2
# Flutter app pubspec.yaml
dependencies:
data_pipeline_core:
path: ../packages/data_pipeline_core
flutter:
sdk: flutter
| Principle | Description |
|---|---|
| No Flutter dependency | pubspec.yaml does not depend on flutter |
| No direct use of dart:io | Use abstract interfaces for file/network operations |
| No UI code | Pure data processing logic |
| Complete testing | Testable without the Flutter runtime |
5. MethodChannel
(1) Dart ↔ Native Communication
▶ Example
> **Output:** Run locally in DartPad or with `dart run`. All examples in the Dart course are based on Dart 3.x / Flutter 3.x; results may vary slightly due to SDK versions.
: MethodChannel calling native
import 'package:flutter/services.dart';
class NativeService {
static const _channel = MethodChannel('com.datapipeline/native');
// Call native method
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';
}
}
// Call with arguments
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;
}
}
}
> **Output:** Run locally in DartPad or with `dart run`. All examples in the Dart course are based on Dart 3.x / Flutter 3.x; results may vary slightly due to SDK versions.
(2) Native-side Implementation
▶ Example
> **Output:** Run locally in DartPad or with `dart run`. All examples in the Dart course are based on Dart 3.x / Flutter 3.x; results may vary slightly due to SDK versions.
: Android (Kotlin) side
// Android implementation
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")
// Save file logic
result.success(true)
}
else -> result.notImplemented()
}
}
}
}
> **Output:** Run locally in DartPad or with `dart run`. All examples in the Dart course are based on Dart 3.x / Flutter 3.x; results may vary slightly due to SDK versions.
| Communication Method | Direction | Data Type | Use Case |
|---|---|---|---|
| MethodChannel | Dart → Native | Standard types | Calling native methods |
| EventChannel | Native → Dart | Stream | Native event streams |
| BasicMessageChannel | Bidirectional | String/Bytes | Bidirectional messages |
6. FFI — Calling C Libraries
▶ Example
> **Output:** Run locally in DartPad or with `dart run`. All examples in the Dart course are based on Dart 3.x / Flutter 3.x; results may vary slightly due to SDK versions.
: dart:ffi calling a C function
import 'dart:ffi';
import 'package:ffi/ffi.dart';
// C function signature: double process_data(double* data, int length)
typedef ProcessDataNative = Double Function(Pointer``<Double>``, Int32);
typedef ProcessDataDart = double Function(Pointer``<Double>``, int);
void main() {
// Load dynamic library
final dylib = DynamicLibrary.open('libdatapipeline.so');
// Look up function
final processData = dylib.lookupFunction<ProcessDataNative, ProcessDataDart>('process_data');
// Prepare data
final dataPtr = calloc``<Double>``(5);
for (var i = 0; i < 5; i++) {
dataPtr[i] = (i + 1) * 100.0;
}
// Call C function
final result = processData(dataPtr, 5);
print('Result from C: $result');
// Free memory
calloc.free(dataPtr);
}
> **Output:** Run locally in DartPad or with `dart run`. All examples in the Dart course are based on Dart 3.x / Flutter 3.x; results may vary slightly due to SDK versions.
| FFI Concept | Description |
|---|---|
DynamicLibrary |
Load dynamic link libraries |
lookupFunction |
Look up a C function |
| `Pointer`` |
Points to native memory |
calloc / free |
Allocate/free native memory |
NativeType |
C type mapping |
7. Bob's Scenario: DataPipeline Dual Front-end Architecture
▶ Example
> **Output:** Run locally in DartPad or with `dart run`. All examples in the Dart course are based on Dart 3.x / Flutter 3.x; results may vary slightly due to SDK versions.
: Shared core + dual front-ends
// 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 consumer
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 consumer would use the same OrderAnalyzer
// but render results in a Widget instead of print()
> **Output:** Run locally in DartPad or with `dart run`. All examples in the Dart course are based on Dart 3.x / Flutter 3.x; results may vary slightly due to SDK versions.
8. Complete Example: DataPipeline Shared Architecture
// ============================================
// DataPipeline Shared Architecture
// Pure Dart core + CLI consumer + Flutter consumer
// ============================================
// ---- Core Library (pure 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 Consumer ----
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');
}
> **Output:** Run locally in DartPad or with `dart run`. All examples in the Dart course are based on Dart 3.x / Flutter 3.x; results may vary slightly due to SDK versions.
Output:
=== 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
❓ FAQ
Q: Can a pure Dart package run on Flutter Web? A: Yes, as long as dart:io is not used. The web platform does not support dart:io; use abstract interfaces (like the http package's Client) to replace direct file/network operations.
Q: Is there a size limit for data transfer via MethodChannel? A: There is no hard limit, but large objects require serialization/deserialization, which has poor performance. For large data, consider using FFI or temporary files.
Q: Does FFI support iOS? A: Yes. On iOS, load .dylib or .framework. Flutter 3.0+ FFI supports all major platforms.
Q: When should I use MethodChannel vs. FFI? A: MethodChannel is suitable for calling platform APIs (sensors, push notifications, file saving); FFI is suitable for calling C libraries (image processing, encryption, database engines).
Q: Does testing a shared package require a Flutter environment? A: No. A pure Dart package can be tested with
dart testalone and does not depend on Flutter. This is a major benefit of extracting a shared package.
Q: How can I ensure a shared package doesn't accidentally introduce Flutter dependencies? A: Don't depend on flutter in pubspec.yaml, and don't import flutter packages. In CI, run tests with
dart test(notflutter test) to verify.
Q: Should the DataPipeline core package use a monorepo or a separate repository? A: A monorepo is recommended (using Melos or path dependencies) for easier synchronization between core package and consumers. A separate repository is suitable for open-source distribution.
📖 Summary
- Dart is the core language of Flutter, driving both the UI and logic layers
- A shared logic package (pure Dart) allows CLI and Flutter to reuse the same business code
- MethodChannel implements Dart ↔ Native communication for platform-specific functions
- FFI directly calls C libraries, suitable for high-performance computation and integrating existing C code
- Architecture principle: Core logic in pure Dart, UI layer customized per platform
📝 Exercises
- Basic (Difficulty ⭐): Create a pure Dart package (
dart create -t package-simple) containing aformatUSDfunction. Reference and call it from both a CLI and a Flutter project. - Intermediate (Difficulty ⭐⭐): Design the DataPipeline core package structure: 3 model classes + 2 service classes + 1 barrel export. Ensure pubspec.yaml has no Flutter dependency, and write tests runnable with
dart test. - Challenge (Difficulty ⭐⭐⭐): Implement an abstract
DataStorageinterface (pure Dart), then implement it using dart:io (CLI) and SharedPreferences (Flutter) separately. The core package should depend only on the interface, with concrete implementations injected by consumers.