In Pigeon for Flutter, how do @HostApi and @FlutterApi differ, and what do @async and @asyncCallback change in the generated Kotlin and Swift signatures?
answer
- who implements, who calls
- HostApi methods sync by default on host
- @async: suspend fun, async throws
- @asyncCallback: completion Result
- pigeon 28.0.0 flipped the default
basics
~20 s@HostApi methods are implemented on the host and called from Dart; @FlutterApi methods are implemented in Dart and called from the host. Since pigeon 28, @async gives Kotlin suspend functions and Swift async methods, while @asyncCallback keeps completion-callback signatures.
solid answer
~50 sThe annotation says **who implements** the API. `@HostApi()` generates a Dart class you call and a Kotlin interface or Swift protocol you implement; `@FlutterApi()` is the reverse: an abstract Dart class you implement and register with `setUp`, and a host class you call. From Dart every host call returns a `Future`, but a plain `@HostApi` method is implemented **synchronously** on the host (`fun add(a: Long, b: Long): Long`, `func add(...) throws -> Int64`), which guarantees exactly one reply. When the host work is asynchronous, mark the method `@async`: since pigeon 28.0.0 Kotlin gets `suspend fun` (launched on `Dispatchers.Main`) and Swift gets `async throws`. `@asyncCallback` instead generates a completion parameter (`callback: (Result<T>) -> Unit` in Kotlin). Host calls into `@FlutterApi` are `suspend`/`async` by default. Errors are thrown as `FlutterError` (Kotlin) or `PigeonError` (Swift) and arrive in Dart as `PlatformException`.
code
dart · 17 linesimport 'package:pigeon/pigeon.dart';
@HostApi()
abstract class ScannerHostApi {
bool isSupported();
@async
ScanResult? scanOnce(ScanRequest request);
@asyncCallback
void warmUpCamera();
}
@FlutterApi()
abstract class ScannerFlutterApi {
void onTorchChanged(bool isOn);
}go deeper
Recall which side implements @HostApi and which implements @FlutterApi, and that Dart always sees Futures.
Explain why unannotated host methods are synchronous, what @async and @asyncCallback generate, and how thrown errors become PlatformException.
Plan a Pigeon upgrade across the 28.0.0 signature change and choose between suspend and callback styles for a callback-based vendor SDK.
Decide the concurrency style a team standardises on for host bridges, balancing coroutine or async adoption against existing callback-heavy SDKs.
## Direction: who implements what Pigeon's two main annotations describe the **direction** of a call: | Annotation | Implemented by | Called by | Dart output | Host output | |---|---|---|---|---| | `@HostApi()` | Kotlin / Swift / C++ | Dart | concrete class with `Future` methods | interface or protocol + `setUp` | | `@FlutterApi()` | Dart | host | abstract class + static `setUp` | concrete class that sends calls | | `@EventChannelApi()` | host (stream handler) | Dart listens | top-level function returning `Stream` | stream handler base class + `register` | For a barcode bridge, `scanOnce` belongs on a `@HostApi` (Dart asks the SDK to scan), while a torch-state callback that the SDK raises belongs on a `@FlutterApi` (the host tells Dart). ## Synchronous host methods are the default All channel traffic is asynchronous from Flutter's point of view, so the Dart side always returns a `Future`. On the **host**, however, a plain `@HostApi` method is generated with a synchronous signature: ```kotlin interface ScannerHostApi { fun isSupported(): Boolean suspend fun scanOnce(request: ScanRequest): ScanResult? } ``` The README gives the reason: synchronous host methods make it simpler to **reply exactly once**. The generated handler calls your method, wraps the return value, and replies; a thrown exception becomes an error reply. There is no way to forget the reply. ## @async and @asyncCallback When the host work is asynchronous (a camera session, a network call), annotate the method: - **`@async`**: generates modern concurrency signatures, `suspend fun` in Kotlin and `async throws` in Swift. In the generated Kotlin handler the call is launched in a `CoroutineScope(Dispatchers.Main)`, and the result or exception is turned into the reply. - **`@asyncCallback`**: generates completion-callback signatures, a `callback: (Result<T>) -> Unit` in Kotlin and a completion closure in Swift. You must call it exactly once. Version detail that trips people up: **pigeon 28.0.0** made `suspend` / `async` the generated style for `@async` host methods and, by default, for `@FlutterApi` methods; code written against earlier versions used callbacks and needs `@asyncCallback` to keep them. Only the Kotlin and Swift generators distinguish the two annotations; Java, Objective-C, C++ and GObject generate callback-style methods for both. ## Error handling across the boundary 1. **Kotlin**: throw the generated `FlutterError(code, message, details)`; the handler catches it and sends an error reply. Any other `Throwable` is also caught and reported with the exception's class name as the code. 2. **Swift**: throw the generated `PigeonError(code:message:details:)`, because Flutter's `FlutterError` does not conform to `Swift.Error`. 3. **`@asyncCallback` methods** report failures through the callback (`Result.failure(...)` in Kotlin) instead of throwing. 4. In Dart all of these surface as **`PlatformException`** with the code, message and details you set. In the other direction, a `@FlutterApi` call from Kotlin that finds no Dart handler fails with a `FlutterError` whose code is `channel-error`. ## Choosing - Pure lookups (`isSupported`) stay synchronous. - Anything that waits on the SDK uses `@async` on a coroutine- or async-aware codebase, or `@asyncCallback` where the SDK exposes callbacks and wrapping them is awkward.
- Why does Pigeon generate synchronous host signatures by default when the Dart call returns a Future anyway?Because the generated handler can then reply for you: it calls your method, wraps the return value or the thrown error, and sends exactly one reply. The Dart `Future` models the message round trip, not the host method's shape. Use `@async` only when the host work really completes later.
- How should a Swift implementation report a scan failure to Dart?Throw the generated `PigeonError(code: "NO_CAMERA", message: ..., details: ...)` from a throwing or `async throws` method. Pigeon catches it and sends an error reply, which Dart receives as a `PlatformException` with that code. Swift uses `PigeonError` because Flutter's `FlutterError` does not conform to `Swift.Error`.
saying these in an interview costs you the question
- Says @FlutterApi methods are implemented on the host
- Believes @async makes the Dart call synchronous
- Throws FlutterError from Swift code instead of PigeonError
- Assumes callback-style Kotlin signatures are still the @async default
- Thinks unannotated host methods can reply later from another callback