Question #156EasyFlutter Basics

What is flutter pigeon?

#flutter#native

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

yaml
dev_dependencies:
  pigeon: ^16.0.0

Basic Example

1. Define API (pigeon/messages.dart)

dart
import '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

bash
flutter pub run pigeon --input pigeon/messages.dart

Generated files:

  • text
    lib/pigeon_generated.dart
    (Dart)
  • text
    android/.../Messages.kt
    (Kotlin)
  • text
    ios/.../Messages.swift
    (Swift)

3. Implement Native Side

Android (Messages.kt):

kotlin
import 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):

swift
import 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

dart
import '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

dart
class 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)

dart
import '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)

kotlin
class 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

dart
final 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

FeatureManual Platform ChannelsPigeon
Type safety❌ No (dynamic maps)✅ Yes (generated code)
Autocomplete❌ No✅ Yes
Compile-time checks❌ No✅ Yes
Boilerplate⚠️ High✅ Low (generated)
SerializationManual (JSON)✅ Automatic
Error handlingManual✅ Built-in

Error Handling

dart
try {
  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:

kotlin
override 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

FeatureBenefit
Type safetyCompile-time error detection
Code generationLess boilerplate
AutocompleteBetter DX (developer experience)
BidirectionalDart ↔ Native communication
Multiple platformsAndroid, iOS support

Pigeon workflow:

text
1. 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: