Skip to content

Commit 24654d3

Browse files
authored
[pigeon] add flutter api protocol (#5181)
Protocols to make testing with mocks easier. Fixes flutter/flutter#136811
1 parent 09c6b11 commit 24654d3

File tree

18 files changed

+482
-99
lines changed

18 files changed

+482
-99
lines changed

packages/pigeon/CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
## 12.0.1
2+
3+
* [swift] Adds protocol for Flutter APIs.
4+
15
## 12.0.0
26

37
* Adds error handling on Flutter API methods.

packages/pigeon/example/app/ios/Runner/Messages.g.swift

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -173,13 +173,16 @@ class ExampleHostApiSetup {
173173
}
174174
}
175175
}
176-
/// Generated class from Pigeon that represents Flutter messages that can be called from Swift.
177-
class MessageFlutterApi {
176+
/// Generated protocol from Pigeon that represents Flutter messages that can be called from Swift.
177+
protocol MessageFlutterApiProtocol {
178+
func flutterMethod(aString aStringArg: String?, completion: @escaping (Result<String, FlutterError>) -> Void)
179+
}
180+
class MessageFlutterApi: MessageFlutterApiProtocol {
178181
private let binaryMessenger: FlutterBinaryMessenger
179182
init(binaryMessenger: FlutterBinaryMessenger){
180183
self.binaryMessenger = binaryMessenger
181184
}
182-
func flutterMethod(aString aStringArg: String?, completion: @escaping (Result<String, FlutterError>) -> Void) {
185+
func flutterMethod(aString aStringArg: String?, completion: @escaping (Result<String, FlutterError>) -> Void) {
183186
let channel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.pigeon_example_package.MessageFlutterApi.flutterMethod", binaryMessenger: binaryMessenger)
184187
channel.sendMessage([aStringArg] as [Any?]) { response in
185188
guard let listResponse = response as? [Any?] else {

packages/pigeon/lib/generator_tools.dart

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ import 'ast.dart';
1313
/// The current version of pigeon.
1414
///
1515
/// This must match the version in pubspec.yaml.
16-
const String pigeonVersion = '12.0.0';
16+
const String pigeonVersion = '12.0.1';
1717

1818
/// Read all the content from [stdin] to a String.
1919
String readStdin() {

packages/pigeon/lib/swift_generator.dart

Lines changed: 41 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -291,13 +291,22 @@ import FlutterMacOS
291291
if (isCustomCodec) {
292292
_writeCodec(indent, api, root);
293293
}
294+
294295
const List<String> generatedComments = <String>[
295-
' Generated class from Pigeon that represents Flutter messages that can be called from Swift.'
296+
' Generated protocol from Pigeon that represents Flutter messages that can be called from Swift.'
296297
];
297298
addDocumentationComments(indent, api.documentationComments, _docCommentSpec,
298299
generatorComments: generatedComments);
299300

300-
indent.write('class ${api.name} ');
301+
indent.addScoped('protocol ${api.name}Protocol {', '}', () {
302+
for (final Method func in api.methods) {
303+
addDocumentationComments(
304+
indent, func.documentationComments, _docCommentSpec);
305+
indent.writeln(_getMethodSignature(func));
306+
}
307+
});
308+
309+
indent.write('class ${api.name}: ${api.name}Protocol ');
301310
indent.addScoped('{', '}', () {
302311
indent.writeln('private let binaryMessenger: FlutterBinaryMessenger');
303312
indent.write('init(binaryMessenger: FlutterBinaryMessenger)');
@@ -314,47 +323,19 @@ import FlutterMacOS
314323
});
315324
}
316325
for (final Method func in api.methods) {
317-
final _SwiftFunctionComponents components =
318-
_SwiftFunctionComponents.fromMethod(func);
319-
320326
final String channelName = makeChannelName(api, func, dartPackageName);
321-
final String returnType = func.returnType.isVoid
322-
? 'Void'
323-
: _nullsafeSwiftTypeForDartType(func.returnType);
324-
String sendArgument;
327+
325328
addDocumentationComments(
326329
indent, func.documentationComments, _docCommentSpec);
327-
328-
if (func.arguments.isEmpty) {
329-
indent.write(
330-
'func ${func.name}(completion: @escaping (Result<$returnType, FlutterError>) -> Void) ');
331-
sendArgument = 'nil';
332-
} else {
333-
final Iterable<String> argTypes = func.arguments
334-
.map((NamedType e) => _nullsafeSwiftTypeForDartType(e.type));
335-
final Iterable<String> argLabels = indexMap(components.arguments,
336-
(int index, _SwiftFunctionArgument argument) {
337-
return argument.label ??
338-
_getArgumentName(index, argument.namedType);
339-
});
340-
final Iterable<String> argNames =
341-
indexMap(func.arguments, _getSafeArgumentName);
330+
indent.writeScoped('${_getMethodSignature(func)} {', '}', () {
342331
final Iterable<String> enumSafeArgNames = func.arguments
343332
.asMap()
344333
.entries
345334
.map((MapEntry<int, NamedType> e) =>
346335
getEnumSafeArgumentExpression(root, e.key, e.value));
347-
sendArgument = '[${enumSafeArgNames.join(', ')}] as [Any?]';
348-
final String argsSignature = map3(
349-
argTypes,
350-
argLabels,
351-
argNames,
352-
(String type, String label, String name) =>
353-
'$label $name: $type').join(', ');
354-
indent.write(
355-
'func ${components.name}($argsSignature, completion: @escaping (Result<$returnType, FlutterError>) -> Void) ');
356-
}
357-
indent.addScoped('{', '}', () {
336+
final String sendArgument = func.arguments.isEmpty
337+
? 'nil'
338+
: '[${enumSafeArgNames.join(', ')}] as [Any?]';
358339
const String channel = 'channel';
359340
indent.writeln(
360341
'let $channel = FlutterBasicMessageChannel(name: "$channelName", binaryMessenger: binaryMessenger$codecArgumentString)');
@@ -893,6 +874,31 @@ String _nullsafeSwiftTypeForDartType(TypeDeclaration type) {
893874
return '${_swiftTypeForDartType(type)}$nullSafe';
894875
}
895876

877+
String _getMethodSignature(Method func) {
878+
final _SwiftFunctionComponents components =
879+
_SwiftFunctionComponents.fromMethod(func);
880+
final String returnType = func.returnType.isVoid
881+
? 'Void'
882+
: _nullsafeSwiftTypeForDartType(func.returnType);
883+
884+
if (func.arguments.isEmpty) {
885+
return 'func ${func.name}(completion: @escaping (Result<$returnType, FlutterError>) -> Void) ';
886+
} else {
887+
final Iterable<String> argTypes = func.arguments
888+
.map((NamedType e) => _nullsafeSwiftTypeForDartType(e.type));
889+
final Iterable<String> argLabels = indexMap(components.arguments,
890+
(int index, _SwiftFunctionArgument argument) {
891+
return argument.label ?? _getArgumentName(index, argument.namedType);
892+
});
893+
final Iterable<String> argNames =
894+
indexMap(func.arguments, _getSafeArgumentName);
895+
final String argsSignature = map3(argTypes, argLabels, argNames,
896+
(String type, String label, String name) => '$label $name: $type')
897+
.join(', ');
898+
return 'func ${components.name}($argsSignature, completion: @escaping (Result<$returnType, FlutterError>) -> Void) ';
899+
}
900+
}
901+
896902
/// A class that represents a Swift function argument.
897903
///
898904
/// The [name] is the name of the argument.

packages/pigeon/pigeons/core_tests.dart

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -661,6 +661,10 @@ abstract class FlutterSmallApi {
661661
@ObjCSelector('echoWrappedList:')
662662
@SwiftFunction('echo(_:)')
663663
TestMessage echoWrappedList(TestMessage msg);
664+
665+
@ObjCSelector('echoString:')
666+
@SwiftFunction('echo(_:)')
667+
String echoString(String aString);
664668
}
665669

666670
/// A data class containing a List, used in unit tests.

packages/pigeon/platform_tests/alternate_language_test_plugin/android/src/main/java/com/example/alternate_language_test_plugin/CoreTests.java

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4390,5 +4390,41 @@ public void echoWrappedList(@NonNull TestMessage msgArg, @NonNull Result<TestMes
43904390
}
43914391
});
43924392
}
4393+
4394+
public void echoString(@NonNull String aStringArg, @NonNull Result<String> result) {
4395+
BasicMessageChannel<Object> channel =
4396+
new BasicMessageChannel<>(
4397+
binaryMessenger,
4398+
"dev.flutter.pigeon.pigeon_integration_tests.FlutterSmallApi.echoString",
4399+
getCodec());
4400+
channel.send(
4401+
new ArrayList<Object>(Collections.singletonList(aStringArg)),
4402+
channelReply -> {
4403+
if (channelReply instanceof List) {
4404+
List<Object> listReply = (List<Object>) channelReply;
4405+
if (listReply.size() > 1) {
4406+
result.error(
4407+
new FlutterError(
4408+
(String) listReply.get(0),
4409+
(String) listReply.get(1),
4410+
(String) listReply.get(2)));
4411+
} else if (listReply.get(0) == null) {
4412+
result.error(
4413+
new FlutterError(
4414+
"null-error",
4415+
"Flutter api returned null value for non-null return value.",
4416+
""));
4417+
} else {
4418+
@SuppressWarnings("ConstantConditions")
4419+
String output = (String) listReply.get(0);
4420+
result.success(output);
4421+
}
4422+
} else {
4423+
result.error(
4424+
new FlutterError(
4425+
"channel-error", "Unable to establish connection on channel.", ""));
4426+
}
4427+
});
4428+
}
43934429
}
43944430
}

packages/pigeon/platform_tests/alternate_language_test_plugin/ios/Classes/CoreTests.gen.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -494,6 +494,8 @@ NSObject<FlutterMessageCodec> *FlutterSmallApiGetCodec(void);
494494
- (instancetype)initWithBinaryMessenger:(id<FlutterBinaryMessenger>)binaryMessenger;
495495
- (void)echoWrappedList:(TestMessage *)msg
496496
completion:(void (^)(TestMessage *_Nullable, FlutterError *_Nullable))completion;
497+
- (void)echoString:(NSString *)aString
498+
completion:(void (^)(NSString *_Nullable, FlutterError *_Nullable))completion;
497499
@end
498500

499501
NS_ASSUME_NONNULL_END

packages/pigeon/platform_tests/alternate_language_test_plugin/ios/Classes/CoreTests.gen.m

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2971,4 +2971,30 @@ - (void)echoWrappedList:(TestMessage *)arg_msg
29712971
}
29722972
}];
29732973
}
2974+
- (void)echoString:(NSString *)arg_aString
2975+
completion:(void (^)(NSString *_Nullable, FlutterError *_Nullable))completion {
2976+
FlutterBasicMessageChannel *channel = [FlutterBasicMessageChannel
2977+
messageChannelWithName:
2978+
@"dev.flutter.pigeon.pigeon_integration_tests.FlutterSmallApi.echoString"
2979+
binaryMessenger:self.binaryMessenger
2980+
codec:FlutterSmallApiGetCodec()];
2981+
[channel sendMessage:@[ arg_aString ?: [NSNull null] ]
2982+
reply:^(NSArray<id> *reply) {
2983+
if (reply != nil) {
2984+
if (reply.count > 1) {
2985+
completion(nil, [FlutterError errorWithCode:reply[0]
2986+
message:reply[1]
2987+
details:reply[2]]);
2988+
} else {
2989+
NSString *output = reply[0] == [NSNull null] ? nil : reply[0];
2990+
completion(output, nil);
2991+
}
2992+
} else {
2993+
completion(nil, [FlutterError
2994+
errorWithCode:@"channel-error"
2995+
message:@"Unable to establish connection on channel."
2996+
details:@""]);
2997+
}
2998+
}];
2999+
}
29743000
@end

packages/pigeon/platform_tests/alternate_language_test_plugin/macos/Classes/CoreTests.gen.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -494,6 +494,8 @@ NSObject<FlutterMessageCodec> *FlutterSmallApiGetCodec(void);
494494
- (instancetype)initWithBinaryMessenger:(id<FlutterBinaryMessenger>)binaryMessenger;
495495
- (void)echoWrappedList:(TestMessage *)msg
496496
completion:(void (^)(TestMessage *_Nullable, FlutterError *_Nullable))completion;
497+
- (void)echoString:(NSString *)aString
498+
completion:(void (^)(NSString *_Nullable, FlutterError *_Nullable))completion;
497499
@end
498500

499501
NS_ASSUME_NONNULL_END

packages/pigeon/platform_tests/alternate_language_test_plugin/macos/Classes/CoreTests.gen.m

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2971,4 +2971,30 @@ - (void)echoWrappedList:(TestMessage *)arg_msg
29712971
}
29722972
}];
29732973
}
2974+
- (void)echoString:(NSString *)arg_aString
2975+
completion:(void (^)(NSString *_Nullable, FlutterError *_Nullable))completion {
2976+
FlutterBasicMessageChannel *channel = [FlutterBasicMessageChannel
2977+
messageChannelWithName:
2978+
@"dev.flutter.pigeon.pigeon_integration_tests.FlutterSmallApi.echoString"
2979+
binaryMessenger:self.binaryMessenger
2980+
codec:FlutterSmallApiGetCodec()];
2981+
[channel sendMessage:@[ arg_aString ?: [NSNull null] ]
2982+
reply:^(NSArray<id> *reply) {
2983+
if (reply != nil) {
2984+
if (reply.count > 1) {
2985+
completion(nil, [FlutterError errorWithCode:reply[0]
2986+
message:reply[1]
2987+
details:reply[2]]);
2988+
} else {
2989+
NSString *output = reply[0] == [NSNull null] ? nil : reply[0];
2990+
completion(output, nil);
2991+
}
2992+
} else {
2993+
completion(nil, [FlutterError
2994+
errorWithCode:@"channel-error"
2995+
message:@"Unable to establish connection on channel."
2996+
details:@""]);
2997+
}
2998+
}];
2999+
}
29743000
@end

0 commit comments

Comments
 (0)