Skip to content

[pigeon] add flutter api protocol #5181

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
Merged
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
4 changes: 4 additions & 0 deletions packages/pigeon/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
## 12.0.1

* [swift] Adds protocol for Flutter APIs.

## 12.0.0

* Adds error handling on Flutter API methods.
Expand Down
9 changes: 6 additions & 3 deletions packages/pigeon/example/app/ios/Runner/Messages.g.swift
Original file line number Diff line number Diff line change
Expand Up @@ -173,13 +173,16 @@ class ExampleHostApiSetup {
}
}
}
/// Generated class from Pigeon that represents Flutter messages that can be called from Swift.
class MessageFlutterApi {
/// 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, FlutterError>) -> Void)
}
class MessageFlutterApi: MessageFlutterApiProtocol {
private let binaryMessenger: FlutterBinaryMessenger
init(binaryMessenger: FlutterBinaryMessenger){
self.binaryMessenger = binaryMessenger
}
func flutterMethod(aString aStringArg: String?, completion: @escaping (Result<String, FlutterError>) -> Void) {
func flutterMethod(aString aStringArg: String?, completion: @escaping (Result<String, FlutterError>) -> Void) {
let channel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.pigeon_example_package.MessageFlutterApi.flutterMethod", binaryMessenger: binaryMessenger)
channel.sendMessage([aStringArg] as [Any?]) { response in
guard let listResponse = response as? [Any?] else {
Expand Down
2 changes: 1 addition & 1 deletion packages/pigeon/lib/generator_tools.dart
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import 'ast.dart';
/// The current version of pigeon.
///
/// This must match the version in pubspec.yaml.
const String pigeonVersion = '12.0.0';
const String pigeonVersion = '12.0.1';

/// Read all the content from [stdin] to a String.
String readStdin() {
Expand Down
76 changes: 41 additions & 35 deletions packages/pigeon/lib/swift_generator.dart
Original file line number Diff line number Diff line change
Expand Up @@ -291,13 +291,22 @@ import FlutterMacOS
if (isCustomCodec) {
_writeCodec(indent, api, root);
}

const List<String> generatedComments = <String>[
' Generated class from Pigeon that represents Flutter messages that can be called from Swift.'
' Generated protocol from Pigeon that represents Flutter messages that can be called from Swift.'
];
addDocumentationComments(indent, api.documentationComments, _docCommentSpec,
generatorComments: generatedComments);

indent.write('class ${api.name} ');
indent.addScoped('protocol ${api.name}Protocol {', '}', () {
for (final Method func in api.methods) {
addDocumentationComments(
indent, func.documentationComments, _docCommentSpec);
indent.writeln(_getMethodSignature(func));
}
});

indent.write('class ${api.name}: ${api.name}Protocol ');
indent.addScoped('{', '}', () {
indent.writeln('private let binaryMessenger: FlutterBinaryMessenger');
indent.write('init(binaryMessenger: FlutterBinaryMessenger)');
Expand All @@ -314,47 +323,19 @@ import FlutterMacOS
});
}
for (final Method func in api.methods) {
final _SwiftFunctionComponents components =
_SwiftFunctionComponents.fromMethod(func);

final String channelName = makeChannelName(api, func, dartPackageName);
final String returnType = func.returnType.isVoid
? 'Void'
: _nullsafeSwiftTypeForDartType(func.returnType);
String sendArgument;

addDocumentationComments(
indent, func.documentationComments, _docCommentSpec);

if (func.arguments.isEmpty) {
indent.write(
'func ${func.name}(completion: @escaping (Result<$returnType, FlutterError>) -> Void) ');
sendArgument = 'nil';
} else {
final Iterable<String> argTypes = func.arguments
.map((NamedType e) => _nullsafeSwiftTypeForDartType(e.type));
final Iterable<String> argLabels = indexMap(components.arguments,
(int index, _SwiftFunctionArgument argument) {
return argument.label ??
_getArgumentName(index, argument.namedType);
});
final Iterable<String> argNames =
indexMap(func.arguments, _getSafeArgumentName);
indent.writeScoped('${_getMethodSignature(func)} {', '}', () {
final Iterable<String> enumSafeArgNames = func.arguments
.asMap()
.entries
.map((MapEntry<int, NamedType> e) =>
getEnumSafeArgumentExpression(root, e.key, e.value));
sendArgument = '[${enumSafeArgNames.join(', ')}] as [Any?]';
final String argsSignature = map3(
argTypes,
argLabels,
argNames,
(String type, String label, String name) =>
'$label $name: $type').join(', ');
indent.write(
'func ${components.name}($argsSignature, completion: @escaping (Result<$returnType, FlutterError>) -> Void) ');
}
indent.addScoped('{', '}', () {
final String sendArgument = func.arguments.isEmpty
? 'nil'
: '[${enumSafeArgNames.join(', ')}] as [Any?]';
const String channel = 'channel';
indent.writeln(
'let $channel = FlutterBasicMessageChannel(name: "$channelName", binaryMessenger: binaryMessenger$codecArgumentString)');
Expand Down Expand Up @@ -893,6 +874,31 @@ String _nullsafeSwiftTypeForDartType(TypeDeclaration type) {
return '${_swiftTypeForDartType(type)}$nullSafe';
}

String _getMethodSignature(Method func) {
final _SwiftFunctionComponents components =
_SwiftFunctionComponents.fromMethod(func);
final String returnType = func.returnType.isVoid
? 'Void'
: _nullsafeSwiftTypeForDartType(func.returnType);

if (func.arguments.isEmpty) {
return 'func ${func.name}(completion: @escaping (Result<$returnType, FlutterError>) -> Void) ';
} else {
final Iterable<String> argTypes = func.arguments
.map((NamedType e) => _nullsafeSwiftTypeForDartType(e.type));
final Iterable<String> argLabels = indexMap(components.arguments,
(int index, _SwiftFunctionArgument argument) {
return argument.label ?? _getArgumentName(index, argument.namedType);
});
final Iterable<String> argNames =
indexMap(func.arguments, _getSafeArgumentName);
final String argsSignature = map3(argTypes, argLabels, argNames,
(String type, String label, String name) => '$label $name: $type')
.join(', ');
return 'func ${components.name}($argsSignature, completion: @escaping (Result<$returnType, FlutterError>) -> Void) ';
}
}

/// A class that represents a Swift function argument.
///
/// The [name] is the name of the argument.
Expand Down
4 changes: 4 additions & 0 deletions packages/pigeon/pigeons/core_tests.dart
Original file line number Diff line number Diff line change
Expand Up @@ -661,6 +661,10 @@ abstract class FlutterSmallApi {
@ObjCSelector('echoWrappedList:')
@SwiftFunction('echo(_:)')
TestMessage echoWrappedList(TestMessage msg);

@ObjCSelector('echoString:')
@SwiftFunction('echo(_:)')
String echoString(String aString);
}

/// A data class containing a List, used in unit tests.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4390,5 +4390,41 @@ public void echoWrappedList(@NonNull TestMessage msgArg, @NonNull Result<TestMes
}
});
}

public void echoString(@NonNull String aStringArg, @NonNull Result<String> result) {
BasicMessageChannel<Object> channel =
new BasicMessageChannel<>(
binaryMessenger,
"dev.flutter.pigeon.pigeon_integration_tests.FlutterSmallApi.echoString",
getCodec());
channel.send(
new ArrayList<Object>(Collections.singletonList(aStringArg)),
channelReply -> {
if (channelReply instanceof List) {
List<Object> listReply = (List<Object>) channelReply;
if (listReply.size() > 1) {
result.error(
new FlutterError(
(String) listReply.get(0),
(String) listReply.get(1),
(String) listReply.get(2)));
} else if (listReply.get(0) == null) {
result.error(
new FlutterError(
"null-error",
"Flutter api returned null value for non-null return value.",
""));
} else {
@SuppressWarnings("ConstantConditions")
String output = (String) listReply.get(0);
result.success(output);
}
} else {
result.error(
new FlutterError(
"channel-error", "Unable to establish connection on channel.", ""));
}
});
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -494,6 +494,8 @@ NSObject<FlutterMessageCodec> *FlutterSmallApiGetCodec(void);
- (instancetype)initWithBinaryMessenger:(id<FlutterBinaryMessenger>)binaryMessenger;
- (void)echoWrappedList:(TestMessage *)msg
completion:(void (^)(TestMessage *_Nullable, FlutterError *_Nullable))completion;
- (void)echoString:(NSString *)aString
completion:(void (^)(NSString *_Nullable, FlutterError *_Nullable))completion;
@end

NS_ASSUME_NONNULL_END
Original file line number Diff line number Diff line change
Expand Up @@ -2971,4 +2971,30 @@ - (void)echoWrappedList:(TestMessage *)arg_msg
}
}];
}
- (void)echoString:(NSString *)arg_aString
completion:(void (^)(NSString *_Nullable, FlutterError *_Nullable))completion {
FlutterBasicMessageChannel *channel = [FlutterBasicMessageChannel
messageChannelWithName:
@"dev.flutter.pigeon.pigeon_integration_tests.FlutterSmallApi.echoString"
binaryMessenger:self.binaryMessenger
codec:FlutterSmallApiGetCodec()];
[channel sendMessage:@[ arg_aString ?: [NSNull null] ]
reply:^(NSArray<id> *reply) {
if (reply != nil) {
if (reply.count > 1) {
completion(nil, [FlutterError errorWithCode:reply[0]
message:reply[1]
details:reply[2]]);
} else {
NSString *output = reply[0] == [NSNull null] ? nil : reply[0];
completion(output, nil);
}
} else {
completion(nil, [FlutterError
errorWithCode:@"channel-error"
message:@"Unable to establish connection on channel."
details:@""]);
}
}];
}
@end
Original file line number Diff line number Diff line change
Expand Up @@ -494,6 +494,8 @@ NSObject<FlutterMessageCodec> *FlutterSmallApiGetCodec(void);
- (instancetype)initWithBinaryMessenger:(id<FlutterBinaryMessenger>)binaryMessenger;
- (void)echoWrappedList:(TestMessage *)msg
completion:(void (^)(TestMessage *_Nullable, FlutterError *_Nullable))completion;
- (void)echoString:(NSString *)aString
completion:(void (^)(NSString *_Nullable, FlutterError *_Nullable))completion;
@end

NS_ASSUME_NONNULL_END
Original file line number Diff line number Diff line change
Expand Up @@ -2971,4 +2971,30 @@ - (void)echoWrappedList:(TestMessage *)arg_msg
}
}];
}
- (void)echoString:(NSString *)arg_aString
completion:(void (^)(NSString *_Nullable, FlutterError *_Nullable))completion {
FlutterBasicMessageChannel *channel = [FlutterBasicMessageChannel
messageChannelWithName:
@"dev.flutter.pigeon.pigeon_integration_tests.FlutterSmallApi.echoString"
binaryMessenger:self.binaryMessenger
codec:FlutterSmallApiGetCodec()];
[channel sendMessage:@[ arg_aString ?: [NSNull null] ]
reply:^(NSArray<id> *reply) {
if (reply != nil) {
if (reply.count > 1) {
completion(nil, [FlutterError errorWithCode:reply[0]
message:reply[1]
details:reply[2]]);
} else {
NSString *output = reply[0] == [NSNull null] ? nil : reply[0];
completion(output, nil);
}
} else {
completion(nil, [FlutterError
errorWithCode:@"channel-error"
message:@"Unable to establish connection on channel."
details:@""]);
}
}];
}
@end
Original file line number Diff line number Diff line change
Expand Up @@ -3120,6 +3120,8 @@ abstract class FlutterSmallApi {

TestMessage echoWrappedList(TestMessage msg);

String echoString(String aString);

static void setup(FlutterSmallApi? api, {BinaryMessenger? binaryMessenger}) {
{
final BasicMessageChannel<Object?> channel = BasicMessageChannel<Object?>(
Expand Down Expand Up @@ -3148,5 +3150,32 @@ abstract class FlutterSmallApi {
});
}
}
{
final BasicMessageChannel<Object?> channel = BasicMessageChannel<Object?>(
'dev.flutter.pigeon.pigeon_integration_tests.FlutterSmallApi.echoString',
codec,
binaryMessenger: binaryMessenger);
if (api == null) {
channel.setMessageHandler(null);
} else {
channel.setMessageHandler((Object? message) async {
assert(message != null,
'Argument for dev.flutter.pigeon.pigeon_integration_tests.FlutterSmallApi.echoString was null.');
final List<Object?> args = (message as List<Object?>?)!;
final String? arg_aString = (args[0] as String?);
assert(arg_aString != null,
'Argument for dev.flutter.pigeon.pigeon_integration_tests.FlutterSmallApi.echoString was null, expected non-null String.');
try {
final String output = api.echoString(arg_aString!);
return wrapResponse(result: output);
} on PlatformException catch (e) {
return wrapResponse(error: e);
} catch (e) {
return wrapResponse(
error: PlatformException(code: 'error', message: e.toString()));
}
});
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2479,4 +2479,21 @@ class FlutterSmallApi(private val binaryMessenger: BinaryMessenger) {
}
}
}
fun echoString(aStringArg: String, callback: (Result<String>) -> Unit) {
val channel = BasicMessageChannel<Any?>(binaryMessenger, "dev.flutter.pigeon.pigeon_integration_tests.FlutterSmallApi.echoString", 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.", "")));
} else {
val output = it[0] as String
callback(Result.success(output));
}
} else {
callback(Result.failure(FlutterError("channel-error", "Unable to establish connection on channel.", "")));
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,14 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

import Flutter
import Foundation
import XCTest

@testable import test_plugin

class RunnerTests: XCTestCase {

func testToListAndBack() throws {
let reply = MessageSearchReply(result: "foobar")
let dict = reply.toList()
Expand All @@ -27,4 +30,32 @@ class RunnerTests: XCTestCase {
let copy = MessageSearchReply.fromList(dict)
XCTAssertEqual(reply.error, copy?.error)
}

/// This validates that pigeon clients can easily write tests that mock out Flutter API
/// calls using a pigeon-generated protocol.
func testEchoStringFromProtocol() throws {
let api: FlutterApiFromProtocol = FlutterApiFromProtocol()
let aString = "aString"
api.echo(aString) { response in
switch response {
case .success(let res):
XCTAssertEqual(aString, res)
case .failure(let error):
XCTFail(error.code)
}
}
}
}

class FlutterApiFromProtocol: FlutterSmallApiProtocol {
func echo(_ aStringArg: String, completion: @escaping (Result<String, FlutterError>) -> Void) {
completion(.success(aStringArg))
}

func echo(
_ msgArg: test_plugin.TestMessage,
completion: @escaping (Result<test_plugin.TestMessage, FlutterError>) -> Void
) {
completion(.success(msgArg))
}
}
Loading