Answer
Overview
Flutter Pigeon is a code generation tool that creates type-safe platform channels for communication between Dart (Flutter) and native code (Kotlin/Java for Android, Swift/Objective-C for iOS).
Problem Without Pigeon
Standard platform channels use dynamic maps — no type safety.
dart// ❌ Not type-safe (error-prone) const platform = MethodChannel('com.example/battery'); try { final int result = await platform.invokeMethod('getBatteryLevel'); print('Battery level: $result'); } catch (e) { print('Error: $e'); }
Issues:
- No compile-time type checking
- Easy to make typos in method names
- Requires manual JSON serialization
- No autocomplete support
Solution: Flutter Pigeon
Pigeon generates type-safe APIs from a shared interface definition.
Installation
yamldev_dependencies: pigeon: ^16.0.0
Basic Example
1. Define API (pigeon/messages.dart)
dartimport 'package:pigeon/pigeon.dart'; (PigeonOptions( dartOut: 'lib/pigeon_generated.dart', kotlinOut: 'android/app/src/main/kotlin/Messages.kt', swiftOut: 'ios/Runner/Messages.swift', )) class BatteryInfo { final int level; BatteryInfo(this.level); } () abstract class BatteryApi { BatteryInfo getBatteryLevel(); }
2. Generate Code
bashflutter pub run pigeon --input pigeon/messages.dart
Generated files:
- (Dart)text
lib/pigeon_generated.dart - (Kotlin)text
android/.../Messages.kt - (Swift)text
ios/.../Messages.swift
3. Implement Native Side
Android (Messages.kt):
kotlinimport android.content.Context import android.content.Intent import android.content.IntentFilter import android.os.BatteryManager class BatteryApiImpl(private val context: Context) : BatteryApi { override fun getBatteryLevel(result: Result<BatteryInfo>) { val batteryIntent = context.registerReceiver( null, IntentFilter(Intent.ACTION_BATTERY_CHANGED) ) val level = batteryIntent?.getIntExtra(BatteryManager.EXTRA_LEVEL, -1) ?: -1 if (level != -1) { result.success(BatteryInfo(level.toLong())) } else { result.error(FlutterError("UNAVAILABLE", "Battery level not available", null)) } } } // Register in MainActivity class MainActivity: FlutterActivity() { override fun configureFlutterEngine(flutterEngine: FlutterEngine) { super.configureFlutterEngine(flutterEngine) BatteryApi.setUp(flutterEngine.dartExecutor.binaryMessenger, BatteryApiImpl(this)) } }
iOS (AppDelegate.swift):
swiftimport UIKit import Flutter class BatteryApiImpl: BatteryApi { func getBatteryLevel(completion: @escaping (Result<BatteryInfo, Error>) -> Void) { let device = UIDevice.current device.isBatteryMonitoringEnabled = true if device.batteryState == .unknown { completion(.failure(PigeonError(code: "UNAVAILABLE", message: "Battery info unavailable", details: nil))) } else { let batteryLevel = Int(device.batteryLevel * 100) completion(.success(BatteryInfo(level: Int64(batteryLevel)))) } } } @UIApplicationMain @objc class AppDelegate: FlutterAppDelegate { override func application( _ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? ) -> Bool { let controller = window?.rootViewController as! FlutterViewController BatteryApiSetup.setUp(binaryMessenger: controller.binaryMessenger, api: BatteryApiImpl()) return super.application(application, didFinishLaunchingWithOptions: launchOptions) } }
4. Use in Flutter
dartimport 'pigeon_generated.dart'; class BatteryScreen extends StatefulWidget { _BatteryScreenState createState() => _BatteryScreenState(); } class _BatteryScreenState extends State<BatteryScreen> { final BatteryApi _batteryApi = BatteryApi(); int? _batteryLevel; Future<void> _getBatteryLevel() async { try { final info = await _batteryApi.getBatteryLevel(); setState(() { _batteryLevel = info.level; }); } catch (e) { print('Error: $e'); } } Widget build(BuildContext context) { return Scaffold( appBar: AppBar(title: Text('Battery Info')), body: Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Text( _batteryLevel != null ? 'Battery Level: $_batteryLevel%' : 'Unknown', style: TextStyle(fontSize: 24), ), ElevatedButton( onPressed: _getBatteryLevel, child: Text('Get Battery Level'), ), ], ), ), ); } }
Features
1. Type-Safe APIs
dart// ✅ Type-safe (compile-time checks) final info = await batteryApi.getBatteryLevel(); print(info.level); // ✅ Autocomplete works // ❌ Old way (not type-safe) final result = await platform.invokeMethod('getBatteryLevel'); print(result['level']); // ❌ No autocomplete
2. Complex Data Types
dartclass User { final String id; final String name; final int age; User({required this.id, required this.name, required this.age}); } () abstract class UserApi { User getUser(String id); List<User> getAllUsers(); void saveUser(User user); }
3. Async Methods
dart() abstract class DataApi { String fetchData(); // Returns Future<String> }
4. Flutter-to-Native (HostApi) and Native-to-Flutter (FlutterApi)
HostApi: Dart calls native
dart() abstract class NativeApi { String getNativeValue(); }
FlutterApi: Native calls Dart
dart() abstract class DartApi { void onNativeEvent(String data); } // Implement in Dart class DartApiImpl extends DartApi { void onNativeEvent(String data) { print('Received from native: $data'); } }
Advanced Example: Todo API
Definition (pigeon/todo_api.dart)
dartimport 'package:pigeon/pigeon.dart'; (PigeonOptions( dartOut: 'lib/todo_pigeon.dart', kotlinOut: 'android/app/src/main/kotlin/TodoMessages.kt', swiftOut: 'ios/Runner/TodoMessages.swift', )) class Todo { final String id; final String title; final bool completed; Todo({required this.id, required this.title, required this.completed}); } () abstract class TodoApi { List<Todo> getTodos(); void addTodo(Todo todo); void deleteTodo(String id); }
Implementation (Kotlin)
kotlinclass TodoApiImpl : TodoApi { private val todos = mutableListOf<Todo>() override fun getTodos(result: Result<List<Todo>>) { result.success(todos) } override fun addTodo(todo: Todo, result: Result<Void>) { todos.add(todo) result.success(null) } override fun deleteTodo(id: String, result: Result<Void>) { todos.removeIf { it.id == id } result.success(null) } }
Usage in Flutter
dartfinal todoApi = TodoApi(); // Get todos final todos = await todoApi.getTodos(); print('Total todos: ${todos.length}'); // Add todo await todoApi.addTodo(Todo( id: '1', title: 'Buy groceries', completed: false, )); // Delete todo await todoApi.deleteTodo('1');
Comparison: Manual vs Pigeon
| Feature | Manual Platform Channels | Pigeon |
|---|---|---|
| Type safety | ❌ No (dynamic maps) | ✅ Yes (generated code) |
| Autocomplete | ❌ No | ✅ Yes |
| Compile-time checks | ❌ No | ✅ Yes |
| Boilerplate | ⚠️ High | ✅ Low (generated) |
| Serialization | Manual (JSON) | ✅ Automatic |
| Error handling | Manual | ✅ Built-in |
Error Handling
darttry { final data = await api.fetchData(); print(data); } on PlatformException catch (e) { if (e.code == 'NETWORK_ERROR') { print('Network error: ${e.message}'); } else { print('Unknown error: $e'); } }
Native side:
kotlinoverride fun fetchData(result: Result<String>) { try { val data = performNetworkRequest() result.success(data) } catch (e: IOException) { result.error("NETWORK_ERROR", "Failed to fetch data", e.toString()) } }
Best Practices
dart// ✅ Use clear naming for APIs () abstract class PaymentApi { ... } // ✅ Use async for slow operations Future<String> fetchData(); // ✅ Use meaningful error codes result.error("PERMISSION_DENIED", "Camera permission denied", null) // ❌ Don't use Pigeon for simple one-off calls // Use standard platform channels for single-use cases
When to Use Pigeon
✅ Large apps with many native integrations ✅ Complex data types (classes, lists, maps) ✅ Multiple native APIs (camera, location, sensors) ✅ Team collaboration (type-safe contracts) ❌ Simple one-off calls (use standard channels) ❌ Prototyping (Pigeon adds build step)
Summary
| Feature | Benefit |
|---|---|
| Type safety | Compile-time error detection |
| Code generation | Less boilerplate |
| Autocomplete | Better DX (developer experience) |
| Bidirectional | Dart ↔ Native communication |
| Multiple platforms | Android, iOS support |
Pigeon workflow:
text1. Define API (pigeon/messages.dart) 2. Generate code (flutter pub run pigeon) 3. Implement native side (Kotlin/Swift) 4. Use in Flutter (type-safe calls)
Learn more: