0Pricing
Flutter Mobile Development · 강의

Pigeon을 활용한 타입 안전 플랫폼 채널

Pigeon 도구를 사용해 강력한 타입이 지정된 호스트 및 Flutter 메시징 인터페이스를 생성합니다.

Pigeon을 활용한 타입 안전 플랫폼 채널은(는) CoddyKit의 무료 Flutter Mobile Development 강의입니다. 이것은 4개 중 2번째 강의입니다. 아래에서 전체 강의를 무료로 읽을 수 있으며, 내장 코드 에디터와 24/7 AI 튜터와 함께 브라우저에서 직접 실습할 수 있습니다. 이 강의는 Flutter Mobile Development 학습 경로의 일부이며, 진행 상황이 웹과 CoddyKit 앱에 동기화됩니다. Flutter Mobile Development 강의에는 총 4개의 강의가 포함되어 있습니다.

이 강의의 일부는 아직 번역되지 않았으며 영어로 표시됩니다.

What is Pigeon and Why Use It?

Platform channels in Flutter traditionally use string-based method names and untyped dynamic maps. A typo in a method name or a mismatched argument type only fails at runtime — often on a device you don't own.

Pigeon is a code-generation tool from the Flutter team that solves this problem. You define a Dart API file describing the messages and host APIs, and Pigeon generates:

  • Type-safe Dart classes and abstract channel stubs
  • Matching native code for Android (Kotlin/Java) and iOS (Swift/ObjC)

The result is a compile-time-checked contract between Flutter and the host platform — no raw strings, no untyped maps.

Adding Pigeon to Your Project

Pigeon is a dev-only dependency. Add it to pubspec.yaml under dev_dependencies:

You also need the Flutter standard method codec, which Pigeon uses internally — it is already part of the Flutter SDK, so no extra package is required.

After adding the dependency, run flutter pub get. Pigeon is invoked via dart run pigeon (not as a build_runner builder), which gives you full control over when code is regenerated.

# pubspec.yaml (relevant excerpt)
# dev_dependencies:
#   pigeon: ^22.0.0

// Run generation (in terminal, not Dart code):
// dart run pigeon --input pigeons/messages.dart

Defining a Pigeon API File

A Pigeon API file is plain Dart, decorated with special annotations. Place it in a pigeons/ folder at the project root — it is never compiled into your app; it is only read by the generator.

Key annotations:

  • @ConfigurePigeon — declares output paths for each platform
  • @HostApi() — Flutter calls native (platform implements)
  • @FlutterApi() — Native calls Flutter (Flutter implements)
  • @EventChannelApi() — Streaming events from native to Flutter

All data classes are plain Dart classes with typed fields — no dynamic anywhere.

// pigeons/messages.dart
import 'package:pigeon/pigeon.dart';

@ConfigurePigeon(PigeonOptions(
  dartOut: 'lib/src/messages.g.dart',
  dartOptions: DartOptions(),
  kotlinOut:
    'android/app/src/main/kotlin/com/example/app/Messages.g.kt',
  kotlinOptions: KotlinOptions(),
  swiftOut: 'ios/Runner/Messages.g.swift',
  swiftOptions: SwiftOptions(),
))

// Data class shared between Flutter and native
class BatteryInfo {
  BatteryInfo({required this.level, required this.isCharging});
  final int level;
  final bool isCharging;
}

// Flutter calls native to read battery
@HostApi()
abstract class BatteryHostApi {
  BatteryInfo getBatteryInfo();
  @async
  BatteryInfo getBatteryInfoAsync();
}

// Native calls Flutter to report a low-battery event
@FlutterApi()
abstract class BatteryFlutterApi {
  void onLowBattery(BatteryInfo info);
}

Running the Generator

Once the API file is ready, generate platform code with a single command:

dart run pigeon --input pigeons/messages.dart

Pigeon reads the @ConfigurePigeon options and writes three files:

  • lib/src/messages.g.dart — Dart channel wrapper + data classes
  • android/.../Messages.g.kt — Kotlin interface + registration helpers
  • ios/Runner/Messages.g.swift — Swift protocol + setup call

These .g.dart / .g.kt / .g.swift files are committed to source control (unlike build_runner outputs that are sometimes gitignored) because they are stable, reviewable platform code.

Re-run the command whenever you change the API file. CI should fail if the generated files are out of sync with the source.

Implementing the Host API on Android (Kotlin)

After generation, Pigeon gives you a Kotlin interface that mirrors your @HostApi definition. Your MainActivity (or a plugin class) implements it and registers it on the binary messenger.

Key points:

  • The generated interface is named exactly as your Dart abstract class
  • Registration call is BatteryHostApi.setUp(binding.binaryMessenger, impl)
  • Async methods receive a Result<T> callback instead of returning directly
// android/app/src/main/kotlin/com/example/app/MainActivity.kt
import io.flutter.embedding.android.FlutterActivity
import io.flutter.embedding.engine.FlutterEngine

class MainActivity : FlutterActivity() {

  override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
    super.configureFlutterEngine(flutterEngine)

    BatteryHostApi.setUp(
      flutterEngine.dartExecutor.binaryMessenger,
      BatteryHostApiImpl()
    )
  }
}

class BatteryHostApiImpl : BatteryHostApi {
  override fun getBatteryInfo(): BatteryInfo {
    // Real impl would query BatteryManager
    return BatteryInfo(level = 87L, isCharging = true)
  }

  override fun getBatteryInfoAsync(result: Result<BatteryInfo>) {
    // Offload to coroutine in production
    result.success(BatteryInfo(level = 87L, isCharging = true))
  }
}

Implementing the Host API on iOS (Swift)

On iOS, Pigeon generates a Swift protocol. Your AppDelegate (or a Flutter plugin) conforms to it and registers via the generated setup function.

Notice how the generated Swift API matches the Kotlin API structurally — Pigeon enforces this symmetry so both platforms honour the same contract.

// ios/Runner/AppDelegate.swift
import Flutter
import UIKit

@main
@objc class AppDelegate: FlutterAppDelegate {

  override func application(
    _ application: UIApplication,
    didFinishLaunchingWithOptions options: [UIApplication.LaunchOptionsKey: Any]?
  ) -> Bool {
    let controller = window?.rootViewController as! FlutterViewController
    let messenger = controller.binaryMessenger

    BatteryHostApiSetup.setUp(
      binaryMessenger: messenger,
      api: BatteryHostApiImpl()
    )

    return super.application(application,
      didFinishLaunchingWithOptions: options)
  }
}

class BatteryHostApiImpl: BatteryHostApi {
  func getBatteryInfo() throws -> BatteryInfo {
    // UIDevice.current.batteryLevel in production
    return BatteryInfo(level: 87, isCharging: true)
  }

  func getBatteryInfoAsync(
    completion: @escaping (Result<BatteryInfo, Error>) -> Void
  ) {
    completion(.success(BatteryInfo(level: 87, isCharging: true)))
  }
}

Calling the Host API from Dart

On the Flutter side, the generated code gives you a concrete class — not an abstract one. You simply instantiate it and call methods as regular async Dart:

  • No channel name strings to type
  • No invokeMethod calls
  • No manual argument packing/unpacking
  • Full type safety — the return type is BatteryInfo, not dynamic

Errors thrown by the native side are surfaced as PlatformException — catch them normally.

// lib/battery_page.dart
import 'package:flutter/material.dart';
import 'src/messages.g.dart'; // generated

class BatteryPage extends StatefulWidget {
  const BatteryPage({super.key});
  @override
  State<BatteryPage> createState() => _BatteryPageState();
}

class _BatteryPageState extends State<BatteryPage> {
  final _api = BatteryHostApi(); // generated class
  BatteryInfo? _info;

  Future<void> _fetch() async {
    try {
      // Typed return — no casts needed
      final info = await _api.getBatteryInfoAsync();
      setState(() => _info = info);
    } on PlatformException catch (e) {
      debugPrint('Native error: ${e.message}');
    }
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Battery')),
      body: Column(
        children: [
          Text('Level: ${_info?.level ?? '--'}%'),
          Text('Charging: ${_info?.isCharging ?? '--'}'),
          ElevatedButton(
            onPressed: _fetch,
            child: const Text('Refresh'),
          ),
        ],
      ),
    );
  }
}

Implementing FlutterApi — Native Calls Flutter

@FlutterApi reverses the direction: native code calls into Flutter. Pigeon generates a concrete class on the native side that you instantiate and call, while Flutter provides an implementation of the generated abstract interface.

A common use case is push-notification delivery: the native SDK receives the notification and calls Flutter to update the UI without Flutter polling.

On the Dart side, register the Flutter implementation using the generated setUp method:

// lib/src/battery_flutter_api_impl.dart
import 'messages.g.dart';

class BatteryFlutterApiImpl extends BatteryFlutterApi {
  // Called by native when battery drops below threshold
  @override
  void onLowBattery(BatteryInfo info) {
    debugPrint(
      'LOW BATTERY: ${info.level}% '
      '(charging: ${info.isCharging})',
    );
    // Update state, show snackbar, etc.
  }
}

// Register once — e.g. in main() or an initState
void registerFlutterApis() {
  BatteryFlutterApi.setUp(BatteryFlutterApiImpl());
}

Nullable Fields and Enum Support

Pigeon data classes support nullable fields and Dart enums natively. Enums are serialised as integers over the wire — Pigeon generates the mapping code on all sides so you never deal with raw integers in your business logic.

  • Nullable fields become platform-native optionals (Swift Optional, Kotlin ?)
  • Enums become sealed Kotlin enum classes and Swift enum types
  • Lists and Maps of typed values are supported (List<String>, Map<String, int>)
// pigeons/messages.dart  (additions)

enum ChargingStatus { unknown, charging, discharging, full }

class DetailedBatteryInfo {
  DetailedBatteryInfo({
    required this.level,
    required this.status,
    this.temperature,  // nullable — not available on all devices
    this.voltages,
  });

  final int level;
  final ChargingStatus status;
  final double? temperature;
  final List<double>? voltages;
}

@HostApi()
abstract class DetailedBatteryHostApi {
  @async
  DetailedBatteryInfo getDetailedInfo();
}

Error Handling with PigeonError

Pigeon propagates native errors to Dart as structured exceptions. On the native side you throw a FlutterError (iOS) or use result.error() (Android async). On the Dart side this surfaces as a PlatformException with typed fields.

For richer error contracts, Pigeon also supports a dedicated error class pattern: define a class annotated with nothing special — just return it as a Result error. This lets you send structured error payloads (code + details) instead of bare strings.

// pigeons/messages.dart
class BatteryError {
  BatteryError({required this.code, this.details});
  final String code;   // e.g. 'PERMISSION_DENIED'
  final String? details;
}

// Dart call-site
Future<void> safeFetch() async {
  try {
    final info = await BatteryHostApi().getBatteryInfoAsync();
    print('Level: ${info.level}');
  } on PlatformException catch (e) {
    // e.code  = native error code string
    // e.message = human-readable message
    // e.details = arbitrary details object
    if (e.code == 'PERMISSION_DENIED') {
      print('Need battery permission');
    } else {
      rethrow;
    }
  }
}

Streaming with EventChannelApi

For continuous streams (sensor data, connectivity changes, etc.) Pigeon 14+ introduced @EventChannelApi(). It generates a typed Stream<T> on the Dart side backed by a real EventChannel — no manual codec work needed.

  • Define the data type as a normal Pigeon data class
  • Native implements StreamHandler (Kotlin) / FlutterStreamHandler (Swift)
  • Dart consumes a plain Stream — works with StreamBuilder, listen, async for
// pigeons/messages.dart
@EventChannelApi()
abstract class BatteryEventApi {
  // The return type defines what flows down the stream
  BatteryInfo onBatteryChanged();
}

// lib/battery_stream_page.dart
import 'src/messages.g.dart';

class BatteryStreamPage extends StatelessWidget {
  const BatteryStreamPage({super.key});

  @override
  Widget build(BuildContext context) {
    // BatteryEventApi().onBatteryChangedStream() returns Stream<BatteryInfo>
    return StreamBuilder<BatteryInfo>(
      stream: BatteryEventApi().onBatteryChangedStream(),
      builder: (context, snapshot) {
        if (!snapshot.hasData) return const CircularProgressIndicator();
        final info = snapshot.data!;
        return Text('${info.level}% – charging: ${info.isCharging}');
      },
    );
  }
}

Knowledge Check: Pigeon API Direction

Which Pigeon annotation should you use when you want native platform code to call into Flutter (for example, to push a real-time notification payload into a Dart callback)?

Recap: Type-Safe Platform Channels with Pigeon

In this lesson you learned how Pigeon eliminates the fragility of hand-written platform channels:

  • API file — a plain Dart file in pigeons/ annotated with @HostApi, @FlutterApi, or @EventChannelApi that acts as the single source of truth
  • Code generation — dart run pigeon --input pigeons/messages.dart produces type-safe Dart, Kotlin, and Swift in one command
  • @HostApi — Flutter calls native; native implements the interface and registers it on the binary messenger
  • @FlutterApi — Native calls Flutter; Flutter implements the abstract interface via setUp()
  • @EventChannelApi — typed Stream<T> from native to Flutter for continuous data
  • Rich types — nullable fields, enums, lists, and maps are all supported with platform-native mappings generated automatically
  • Error handling — native errors surface as PlatformException with structured code and details fields

Pigeon should be your default choice for any new platform channel — the compile-time safety it provides pays for itself the first time it catches a mismatch before you ship.

자주 묻는 질문

“Pigeon을 활용한 타입 안전 플랫폼 채널” 강의는 무료인가요?

네 — “Pigeon을 활용한 타입 안전 플랫폼 채널” 전체 내용을 이 웹사이트에서 무료로 읽을 수 있습니다. 인터랙티브하게 실습하려면(내장 코드 에디터와 24/7 AI 튜터), CoddyKit PRO로 업그레이드하면 Flutter Mobile Development 강의 전체를 잠금 해제할 수 있습니다. Flutter Mobile Development 강의에는 총 4개의 강의가 포함되어 있습니다.

“Pigeon을 활용한 타입 안전 플랫폼 채널”에서 뭘 배우나요?

Pigeon 도구를 사용해 강력한 타입이 지정된 호스트 및 Flutter 메시징 인터페이스를 생성합니다. 브라우저에서 직접 실행하는 실습 코드로 Flutter Mobile Development을(를) 배우며, 24/7 AI 튜터가 강의를 진행하면서 질문에 답변해줍니다.

Flutter Mobile Development을(를) 시작하는 데 경험이 필요한가요?

사전 경험은 필요하지 않습니다. CoddyKit의 Flutter Mobile Development은(는) 초급자부터 고급 학습자까지를 위해 구성되어 있으므로, 여기서 시작하거나 처음부터 시작할 수 있으며 자신의 속도대로 진행할 수 있습니다. 이것은 4개 중 2번째 강의입니다.

“Pigeon을 활용한 타입 안전 플랫폼 채널” 강의는 얼마나 걸리나요?

대부분의 CoddyKit 강의는 약 5~10분이 소요됩니다. 각 강의는 간결하고 인터랙티브하여 꾸준한 진행이 가능하며, 웹과 앱에서 중단한 부분부터 바로 시작할 수 있습니다.

이 Flutter Mobile Development 강의에서 코드를 작성하고 실행할 수 있나요?

네. 모든 Flutter Mobile Development 강의에는 내장 코드 에디터가 포함되어 있으므로, 브라우저에서 바로 실제 코드를 작성하고 실행한 후 즉시 AI 피드백을 받을 수 있습니다 — 로컬 설정이 필요 없습니다.

이 강의의 모든 강의

  1. dart:ffi로 C 라이브러리 호출하기
  2. Pigeon을 활용한 타입 안전 플랫폼 채널
  3. iOS 및 Android용 사용자 지정 플랫폼 플러그인 작성
  4. 백그라운드 아이솔레이트 및 네이티브 메모리 관리
← Flutter Mobile Development(으)로 돌아가기