Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions packages/pigeon/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
## 28.0.0

* **Breaking Change** Updates Kotlin and Swift generators to generate `suspend` functions and `async throws` signatures for `@FlutterApi` methods by default.
* Use `@asyncCallback` if callback-style signatures are required.

## 27.3.1

* Updates `analyzer` dependency to support versions 13 and 14.
Expand Down
42 changes: 29 additions & 13 deletions packages/pigeon/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,26 +38,42 @@ as a Swift class instead.

### Synchronous and Asynchronous methods

While all calls across platform channel APIs (such as pigeon methods) are asynchronous,
pigeon methods can be written on the native side as synchronous methods,
to make it simpler to always reply exactly once.
While all calls across platform channel APIs (such as pigeon methods) are asynchronous
from Flutter's perspective, pigeon methods can be written on the native side as synchronous
methods to make it simpler to always reply exactly once.

If asynchronous methods are needed, the `@async` annotation can be used. This will require
results or errors to be returned via a provided callback. [Example](./example/README.md#HostApi_Example).
If asynchronous methods are needed, two annotations are available:
* `@async`: Generates modern concurrency signatures (`suspend` functions in Kotlin and
`async throws` methods in Swift). This is the default style for asynchronous methods.
* `@asyncCallback`: Generates completion callback-based asynchronous methods (e.g.
accepting a `(Result<T>) -> Unit` or completion closure parameter).

> [!NOTE]
> Currently, only the Kotlin and Swift generators distinguish between `@async` and
> `@asyncCallback`. In other generators (Java, Objective-C, C++, and GObject), both annotations
> generate identical callback-based asynchronous method signatures.

[Example](./example/README.md#HostApi_Example).

### Error Handling

#### Kotlin, Java and Swift
#### Kotlin and Swift

All Host API exceptions are translated into Flutter `PlatformException`.
* For synchronous methods, thrown exceptions will be caught and translated.
* For asynchronous methods, there is no default exception handling; errors
should be returned via the provided callback.
* For synchronous methods and modern `@async` methods, thrown exceptions (`FlutterError` in
Kotlin, `PigeonError` in Swift) will be caught and translated automatically.
* For callback-style `@asyncCallback` methods, errors should be returned via the provided
result callback (e.g., `Result.failure(...)`).

To pass custom details into `PlatformException` for error handling,
use `FlutterError` in your Host API. [Example](./example/README.md#HostApi_Example).
To pass custom details into `PlatformException` for error handling, use `FlutterError` in
Kotlin and `PigeonError` in Swift. [Example](./example/README.md#HostApi_Example).

For swift, use `PigeonError` instead of `FlutterError` when throwing an error. See [Example#Swift](./example/README.md#Swift) for more details.
#### Java

All Host API exceptions are translated into Flutter `PlatformException`.
* For synchronous methods, thrown exceptions (`FlutterError`) will be caught and translated.
* For asynchronous methods (`@async` and `@asyncCallback`), errors should be returned via
the provided callback.

#### Objective-C and C++

Expand All @@ -67,7 +83,7 @@ For synchronous methods:
* Objective-C - Set the `error` argument to a `FlutterError` reference.
* C++ - Return a `FlutterError`.

For async methods:
For async methods (`@async` and `@asyncCallback`):
* Return a `FlutterError` through the provided callback.


Expand Down
14 changes: 6 additions & 8 deletions packages/pigeon/example/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -136,12 +136,11 @@ private class PigeonApiImplementation: ExampleHostApi {
return a + b
}

func sendMessage(message: MessageData, completion: @escaping (Result<Bool, Error>) -> Void) {
func sendMessage(message: MessageData) async throws -> Bool {
if message.code == Code.one {
completion(.failure(PigeonError(code: "code", message: "message", details: "details")))
return
throw PigeonError(code: "code", message: "message", details: "details")
}
completion(.success(true))
return true
}
}
```
Expand All @@ -161,12 +160,11 @@ private class PigeonApiImplementation : ExampleHostApi {
return a + b
}

override fun sendMessage(message: MessageData, callback: (Result<Boolean>) -> Unit) {
override suspend fun sendMessage(message: MessageData): Boolean {
if (message.code == Code.ONE) {
callback(Result.failure(FlutterError("code", "message", "details")))
return
throw FlutterError("code", "message", "details")
}
callback(Result.success(true))
return true
}
}
```
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,12 +32,11 @@ private class PigeonApiImplementation : ExampleHostApi {
return a + b
}

override fun sendMessage(message: MessageData, callback: (Result<Boolean>) -> Unit) {
override suspend fun sendMessage(message: MessageData): Boolean {
if (message.code == Code.ONE) {
callback(Result.failure(FlutterError("code", "message", "details")))
return
throw FlutterError("code", "message", "details")
}
callback(Result.success(true))
return true
}
}
// #enddocregion kotlin-class
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,12 @@ import io.flutter.plugin.common.MessageCodec
import io.flutter.plugin.common.StandardMessageCodec
import java.io.ByteArrayOutputStream
import java.nio.ByteBuffer
import kotlin.coroutines.resume
import kotlin.coroutines.resumeWithException
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.suspendCancellableCoroutine

const val aStringConstant: String = "stringConstantValue"
const val anIntConstant: Long = 42L
Expand Down Expand Up @@ -297,7 +303,7 @@ interface ExampleHostApi {

fun add(a: Long, b: Long): Long

fun sendMessage(message: MessageData, callback: (Result<Boolean>) -> Unit)
suspend fun sendMessage(message: MessageData): Boolean

companion object {
/** The codec used by ExampleHostApi. */
Expand Down Expand Up @@ -364,14 +370,14 @@ interface ExampleHostApi {
channel.setMessageHandler { message, reply ->
val args = message as List<Any?>
val messageArg = args[0] as MessageData
api.sendMessage(messageArg) { result: Result<Boolean> ->
val error = result.exceptionOrNull()
if (error != null) {
reply.reply(MessagesPigeonUtils.wrapError(error))
} else {
val data = result.getOrNull()
reply.reply(MessagesPigeonUtils.wrapResult(data))
}
CoroutineScope(Dispatchers.Main).launch {
val wrapped: List<Any?> =
try {
listOf(api.sendMessage(messageArg))
} catch (exception: Throwable) {
MessagesPigeonUtils.wrapError(exception)
}
reply.reply(wrapped)
}
}
} else {
Expand All @@ -391,29 +397,29 @@ class MessageFlutterApi(
val codec: MessageCodec<Any?> by lazy { MessagesPigeonCodec() }
}

fun flutterMethod(aStringArg: String?, callback: (Result<String>) -> Unit) {
suspend fun flutterMethod(aStringArg: String?): String {
val separatedMessageChannelSuffix =
if (messageChannelSuffix.isNotEmpty()) ".$messageChannelSuffix" else ""
val channelName =
"dev.flutter.pigeon.pigeon_example_package.MessageFlutterApi.flutterMethod$separatedMessageChannelSuffix"
val channel = BasicMessageChannel<Any?>(binaryMessenger, channelName, codec)
channel.send(listOf(aStringArg)) {
if (it is List<*>) {
if (it.size > 1) {
callback(Result.failure(FlutterError(it[0] as String, it[1] as String, it[2] as String?)))
} else if (it[0] == null) {
callback(
Result.failure(
FlutterError(
"null-error",
"Flutter api returned null value for non-null return value.",
"")))
return suspendCancellableCoroutine { continuation ->
val channelName =
"dev.flutter.pigeon.pigeon_example_package.MessageFlutterApi.flutterMethod$separatedMessageChannelSuffix"
val channel = BasicMessageChannel<Any?>(binaryMessenger, channelName, codec)
channel.send(listOf(aStringArg)) {
if (it is List<*>) {
if (it.size > 1) {
continuation.resumeWithException(
FlutterError(it[0] as String, it[1] as String, it[2] as String?))
} else if (it[0] == null) {
continuation.resumeWithException(
FlutterError(
"null-error", "Flutter api returned null value for non-null return value.", ""))
} else {
val output = it[0] as String
continuation.resume(output)
}
} else {
val output = it[0] as String
callback(Result.success(output))
continuation.resumeWithException(MessagesPigeonUtils.createConnectionError(channelName))
}
} else {
callback(Result.failure(MessagesPigeonUtils.createConnectionError(channelName)))
}
}
}
Expand Down
7 changes: 3 additions & 4 deletions packages/pigeon/example/app/ios/Runner/AppDelegate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,11 @@ private class PigeonApiImplementation: ExampleHostApi {
return a + b
}

func sendMessage(message: MessageData, completion: @escaping (Result<Bool, Error>) -> Void) {
func sendMessage(message: MessageData) async throws -> Bool {
if message.code == Code.one {
completion(.failure(PigeonError(code: "code", message: "message", details: "details")))
return
throw PigeonError(code: "code", message: "message", details: "details")
}
completion(.success(true))
return true
}
}
// #enddocregion swift-class
Expand Down
64 changes: 31 additions & 33 deletions packages/pigeon/example/app/ios/Runner/Messages.g.swift
Original file line number Diff line number Diff line change
Expand Up @@ -304,7 +304,7 @@ class MessagesPigeonCodec: FlutterStandardMessageCodec, @unchecked Sendable {
protocol ExampleHostApi {
func getHostLanguage() throws -> String
func add(_ a: Int64, to b: Int64) throws -> Int64
func sendMessage(message: MessageData, completion: @escaping (Result<Bool, Error>) -> Void)
func sendMessage(message: MessageData) async throws -> Bool
}

/// Generated setup class from Pigeon to handle messages through the `binaryMessenger`.
Expand Down Expand Up @@ -356,11 +356,11 @@ class ExampleHostApiSetup {
sendMessageChannel.setMessageHandler { message, reply in
let args = message as! [Any?]
let messageArg = args[0] as! MessageData
api.sendMessage(message: messageArg) { result in
switch result {
case .success(let res):
reply(wrapResult(res))
case .failure(let error):
Task { @MainActor in
do {
let result = try await api.sendMessage(message: messageArg)
reply(wrapResult(result))
} catch {
reply(wrapError(error))
}
}
Expand All @@ -373,8 +373,7 @@ class ExampleHostApiSetup {

/// Generated protocol from Pigeon that represents Flutter messages that can be called from Swift.
protocol MessageFlutterApiProtocol {
func flutterMethod(
aString aStringArg: String?, completion: @escaping (Result<String, PigeonError>) -> Void)
func flutterMethod(aString aStringArg: String?) async throws -> String
}
class MessageFlutterApi: MessageFlutterApiProtocol {
private let binaryMessenger: FlutterBinaryMessenger
Expand All @@ -386,32 +385,31 @@ class MessageFlutterApi: MessageFlutterApiProtocol {
var codec: MessagesPigeonCodec {
return MessagesPigeonCodec.shared
}
func flutterMethod(
aString aStringArg: String?, completion: @escaping (Result<String, PigeonError>) -> Void
) {
let channelName: String =
"dev.flutter.pigeon.pigeon_example_package.MessageFlutterApi.flutterMethod\(messageChannelSuffix)"
let channel = FlutterBasicMessageChannel(
name: channelName, binaryMessenger: binaryMessenger, codec: codec)
channel.sendMessage([aStringArg] as [Any?]) { response in
guard let listResponse = response as? [Any?] else {
completion(.failure(createConnectionError(withChannelName: channelName)))
return
}
if listResponse.count > 1 {
let code: String = listResponse[0] as! String
let message: String? = nilOrValue(listResponse[1])
let details: String? = nilOrValue(listResponse[2])
completion(.failure(PigeonError(code: code, message: message, details: details)))
} else if listResponse[0] == nil {
completion(
.failure(
PigeonError(
func flutterMethod(aString aStringArg: String?) async throws -> String {
return try await withCheckedThrowingContinuation { continuation in
let channelName: String =
"dev.flutter.pigeon.pigeon_example_package.MessageFlutterApi.flutterMethod\(messageChannelSuffix)"
let channel = FlutterBasicMessageChannel(
name: channelName, binaryMessenger: binaryMessenger, codec: codec)
channel.sendMessage([aStringArg] as [Any?]) { response in
guard let listResponse = response as? [Any?] else {
continuation.resume(throwing: createConnectionError(withChannelName: channelName))
return
}
if listResponse.count > 1 {
let code: String = listResponse[0] as! String
let message: String? = nilOrValue(listResponse[1])
let details: String? = nilOrValue(listResponse[2])
continuation.resume(throwing: PigeonError(code: code, message: message, details: details))
} else if listResponse[0] == nil {
continuation.resume(
throwing: PigeonError(
code: "null-error",
message: "Flutter api returned null value for non-null return value.", details: "")))
} else {
let result = listResponse[0] as! String
completion(.success(result))
message: "Flutter api returned null value for non-null return value.", details: ""))
} else {
let result = listResponse[0] as! String
continuation.resume(returning: result)
}
}
}
}
Expand Down
4 changes: 4 additions & 0 deletions packages/pigeon/lib/src/ast.dart
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ class Method extends Node {
required this.location,
this.isRequired = true,
this.isAsynchronous = false,
this.isAsynchronousCallback = false,
this.isStatic = false,
this.offset,
this.objcSelector = '',
Expand All @@ -56,6 +57,9 @@ class Method extends Node {
/// Whether the receiver of this method is expected to return synchronously or not.
bool isAsynchronous;

/// Whether this asynchronous method uses callback-based completions instead of native async/await or suspend functions.
bool isAsynchronousCallback;

/// The offset in the source file where the field appears.
int? offset;

Expand Down
2 changes: 1 addition & 1 deletion packages/pigeon/lib/src/generator_tools.dart
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import 'generator.dart';
/// The current version of pigeon.
///
/// This must match the version in pubspec.yaml.
const String pigeonVersion = '27.3.1';
const String pigeonVersion = '28.0.0';

/// Default plugin package name.
const String defaultPluginPackageName = 'dev.flutter.pigeon';
Expand Down
Loading
Loading