[pigeon] Adds Kotlin support for Pigeon (#999)
diff --git a/packages/pigeon/CHANGELOG.md b/packages/pigeon/CHANGELOG.md
index 583a625..8d6aa74 100644
--- a/packages/pigeon/CHANGELOG.md
+++ b/packages/pigeon/CHANGELOG.md
@@ -1,3 +1,7 @@
+## 4.2.0
+
+* Adds experimental support for Kotlin generation.
+
## 4.1.1
* [java] Adds missing `@NonNull` annotations to some methods.
diff --git a/packages/pigeon/README.md b/packages/pigeon/README.md
index 86607f7..f277ff0 100644
--- a/packages/pigeon/README.md
+++ b/packages/pigeon/README.md
@@ -5,9 +5,10 @@
## Supported Platforms
-Currently Pigeon supports generating Objective-C code for usage on iOS, Java
-code for Android, and has experimental support for C++ for Windows. The
-Objective-C code is
+Currently Pigeon supports generating Objective-C and experimental Swift code
+for usage on iOS, Java and experimental Kotlin code for Android,
+and has experimental support for C++ for Windows.
+The Objective-C code is
[accessible to Swift](https://developer.apple.com/documentation/swift/imported_c_and_objective-c_apis/importing_objective-c_into_swift)
and the Java code is accessible to Kotlin.
@@ -31,18 +32,24 @@
### Flutter calling into iOS steps
-1) Add the generated Objective-C code to your Xcode project for compilation
+1) Add the generated Objective-C or Swift code to your Xcode project for compilation
(e.g. `ios/Runner.xcworkspace` or `.podspec`).
1) Implement the generated iOS protocol for handling the calls on iOS, set it up
as the handler for the messages.
+**Note:** Swift code generation for iOS is experimental while we get more usage and add more
+testing. Not all features may be supported.
+
### Flutter calling into Android Steps
-1) Add the generated Java code to your `./android/app/src/main/java` directory
+1) Add the generated Java or Kotlin code to your `./android/app/src/main/java` directory
for compilation.
-1) Implement the generated Java interface for handling the calls on Android, set
+1) Implement the generated Java or Kotlin interface for handling the calls on Android, set
it up as the handler for the messages.
+**Note:** Kotlin code generation for Android is experimental while we get more usage and add more
+testing and works just with Flutter 3.3.0 or later. Not all features may be supported.
+
### Flutter calling into Windows Steps
1) Add the generated C++ code to your `./windows` directory for compilation, and
@@ -112,6 +119,15 @@
@end
```
+```swift
+// Swift
+
+/** Generated interface from Pigeon that represents a handler of messages from Flutter.*/
+protocol Api2Host {
+ func calculate(value: Value, completion: @escaping (Value) -> Void)
+}
+```
+
```java
// Java
public interface Result<T> {
@@ -124,6 +140,15 @@
}
```
+```kotlin
+// Kotlin
+
+/** Generated interface from Pigeon that represents a handler of messages from Flutter.*/
+interface Api2Host {
+ fun calculate(value: Value, callback: (Value) -> Unit)
+}
+```
+
```c++
// C++
diff --git a/packages/pigeon/lib/generator_tools.dart b/packages/pigeon/lib/generator_tools.dart
index 53a0aac..fc32d23 100644
--- a/packages/pigeon/lib/generator_tools.dart
+++ b/packages/pigeon/lib/generator_tools.dart
@@ -9,7 +9,7 @@
import 'ast.dart';
/// The current version of pigeon. This must match the version in pubspec.yaml.
-const String pigeonVersion = '4.1.1';
+const String pigeonVersion = '4.2.0';
/// Read all the content from [stdin] to a String.
String readStdin() {
diff --git a/packages/pigeon/lib/kotlin_generator.dart b/packages/pigeon/lib/kotlin_generator.dart
new file mode 100644
index 0000000..dc70534
--- /dev/null
+++ b/packages/pigeon/lib/kotlin_generator.dart
@@ -0,0 +1,655 @@
+// Copyright 2013 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'ast.dart';
+import 'functional.dart';
+import 'generator_tools.dart';
+import 'pigeon_lib.dart' show TaskQueueType;
+
+/// Options that control how Kotlin code will be generated.
+class KotlinOptions {
+ /// Creates a [KotlinOptions] object
+ const KotlinOptions({
+ this.package,
+ this.copyrightHeader,
+ });
+
+ /// The package where the generated class will live.
+ final String? package;
+
+ /// A copyright header that will get prepended to generated code.
+ final Iterable<String>? copyrightHeader;
+
+ /// Creates a [KotlinOptions] from a Map representation where:
+ /// `x = KotlinOptions.fromMap(x.toMap())`.
+ static KotlinOptions fromMap(Map<String, Object> map) {
+ return KotlinOptions(
+ package: map['package'] as String?,
+ copyrightHeader: map['copyrightHeader'] as Iterable<String>?,
+ );
+ }
+
+ /// Converts a [KotlinOptions] to a Map representation where:
+ /// `x = KotlinOptions.fromMap(x.toMap())`.
+ Map<String, Object> toMap() {
+ final Map<String, Object> result = <String, Object>{
+ if (package != null) 'package': package!,
+ if (copyrightHeader != null) 'copyrightHeader': copyrightHeader!,
+ };
+ return result;
+ }
+
+ /// Overrides any non-null parameters from [options] into this to make a new
+ /// [KotlinOptions].
+ KotlinOptions merge(KotlinOptions options) {
+ return KotlinOptions.fromMap(mergeMaps(toMap(), options.toMap()));
+ }
+}
+
+/// Calculates the name of the codec that will be generated for [api].
+String _getCodecName(Api api) => '${api.name}Codec';
+
+/// Writes the codec class that will be used by [api].
+/// Example:
+/// private static class FooCodec extends StandardMessageCodec {...}
+void _writeCodec(Indent indent, Api api, Root root) {
+ final String codecName = _getCodecName(api);
+ indent.writeln('@Suppress("UNCHECKED_CAST")');
+ indent.write('private object $codecName : StandardMessageCodec() ');
+ indent.scoped('{', '}', () {
+ if (getCodecClasses(api, root).isNotEmpty) {
+ indent.write(
+ 'override fun readValueOfType(type: Byte, buffer: ByteBuffer): Any? ');
+ indent.scoped('{', '}', () {
+ indent.write('return when (type) ');
+ indent.scoped('{', '}', () {
+ for (final EnumeratedClass customClass
+ in getCodecClasses(api, root)) {
+ indent.write('${customClass.enumeration}.toByte() -> ');
+ indent.scoped('{', '}', () {
+ indent.write(
+ 'return (readValue(buffer) as? Map<String, Any?>)?.let ');
+ indent.scoped('{', '}', () {
+ indent.writeln('${customClass.name}.fromMap(it)');
+ });
+ });
+ }
+ indent.writeln('else -> super.readValueOfType(type, buffer)');
+ });
+ });
+
+ indent.write(
+ 'override fun writeValue(stream: ByteArrayOutputStream, value: Any?) ');
+ indent.writeScoped('{', '}', () {
+ indent.write('when (value) ');
+ indent.scoped('{', '}', () {
+ for (final EnumeratedClass customClass
+ in getCodecClasses(api, root)) {
+ indent.write('is ${customClass.name} -> ');
+ indent.scoped('{', '}', () {
+ indent.writeln('stream.write(${customClass.enumeration})');
+ indent.writeln('writeValue(stream, value.toMap())');
+ });
+ }
+ indent.writeln('else -> super.writeValue(stream, value)');
+ });
+ });
+ }
+ });
+}
+
+/// Write the kotlin code that represents a host [Api], [api].
+/// Example:
+/// interface Foo {
+/// Int add(x: Int, y: Int);
+/// companion object {
+/// fun setUp(binaryMessenger: BinaryMessenger, api: Api) {...}
+/// }
+/// }
+void _writeHostApi(Indent indent, Api api, Root root) {
+ assert(api.location == ApiLocation.host);
+
+ final String apiName = api.name;
+
+ indent.writeln(
+ '/** Generated interface from Pigeon that represents a handler of messages from Flutter. */');
+ indent.write('interface $apiName ');
+ indent.scoped('{', '}', () {
+ for (final Method method in api.methods) {
+ final List<String> argSignature = <String>[];
+ if (method.arguments.isNotEmpty) {
+ final Iterable<String> argTypes = method.arguments
+ .map((NamedType e) => _nullsafeKotlinTypeForDartType(e.type));
+ final Iterable<String> argNames =
+ method.arguments.map((NamedType e) => e.name);
+ argSignature
+ .addAll(map2(argTypes, argNames, (String argType, String argName) {
+ return '$argName: $argType';
+ }));
+ }
+
+ final String returnType = method.returnType.isVoid
+ ? ''
+ : _nullsafeKotlinTypeForDartType(method.returnType);
+ if (method.isAsynchronous) {
+ argSignature.add('callback: ($returnType) -> Unit');
+ indent.writeln('fun ${method.name}(${argSignature.join(', ')})');
+ } else if (method.returnType.isVoid) {
+ indent.writeln('fun ${method.name}(${argSignature.join(', ')})');
+ } else {
+ indent.writeln(
+ 'fun ${method.name}(${argSignature.join(', ')}): $returnType');
+ }
+ }
+
+ indent.addln('');
+ indent.write('companion object ');
+ indent.scoped('{', '}', () {
+ indent.writeln('/** The codec used by $apiName. */');
+ indent.scoped('val codec: MessageCodec<Any?> by lazy {', '}', () {
+ indent.writeln(_getCodecName(api));
+ });
+ indent.writeln(
+ '/** Sets up an instance of `$apiName` to handle messages through the `binaryMessenger`. */');
+ indent.writeln('@Suppress("UNCHECKED_CAST")');
+ indent.write(
+ 'fun setUp(binaryMessenger: BinaryMessenger, api: $apiName?) ');
+ indent.scoped('{', '}', () {
+ for (final Method method in api.methods) {
+ indent.write('');
+ indent.scoped('run {', '}', () {
+ String? taskQueue;
+ if (method.taskQueueType != TaskQueueType.serial) {
+ taskQueue = 'taskQueue';
+ indent.writeln(
+ 'val $taskQueue = binaryMessenger.makeBackgroundTaskQueue()');
+ }
+
+ final String channelName = makeChannelName(api, method);
+
+ indent.write(
+ 'val channel = BasicMessageChannel<Any?>(binaryMessenger, "$channelName", codec');
+
+ if (taskQueue != null) {
+ indent.addln(', $taskQueue)');
+ } else {
+ indent.addln(')');
+ }
+
+ indent.write('if (api != null) ');
+ indent.scoped('{', '}', () {
+ final String messageVarName =
+ method.arguments.isNotEmpty ? 'message' : '_';
+
+ indent.write('channel.setMessageHandler ');
+ indent.scoped('{ $messageVarName, reply ->', '}', () {
+ indent.writeln('val wrapped = hashMapOf<String, Any?>()');
+
+ indent.write('try ');
+ indent.scoped('{', '}', () {
+ final List<String> methodArgument = <String>[];
+ if (method.arguments.isNotEmpty) {
+ indent.writeln('val args = message as List<Any?>');
+ enumerate(method.arguments, (int index, NamedType arg) {
+ final String argName = _getSafeArgumentName(index, arg);
+ final String argIndex = 'args[$index]';
+ indent.writeln(
+ 'val $argName = ${_castForceUnwrap(argIndex, arg.type, root)}');
+ methodArgument.add(argName);
+ });
+ }
+ final String call =
+ 'api.${method.name}(${methodArgument.join(', ')})';
+ if (method.isAsynchronous) {
+ indent.write('$call ');
+ if (method.returnType.isVoid) {
+ indent.scoped('{', '}', () {
+ indent.writeln('reply.reply(null)');
+ });
+ } else {
+ indent.scoped('{', '}', () {
+ indent.writeln('reply.reply(wrapResult(it))');
+ });
+ }
+ } else if (method.returnType.isVoid) {
+ indent.writeln(call);
+ indent.writeln('wrapped["${Keys.result}"] = null');
+ } else {
+ indent.writeln('wrapped["${Keys.result}"] = $call');
+ }
+ }, addTrailingNewline: false);
+ indent.add(' catch (exception: Error) ');
+ indent.scoped('{', '}', () {
+ indent.writeln(
+ 'wrapped["${Keys.error}"] = wrapError(exception)');
+ if (method.isAsynchronous) {
+ indent.writeln('reply.reply(wrapped)');
+ }
+ });
+ if (!method.isAsynchronous) {
+ indent.writeln('reply.reply(wrapped)');
+ }
+ });
+ }, addTrailingNewline: false);
+ indent.scoped(' else {', '}', () {
+ indent.writeln('channel.setMessageHandler(null)');
+ });
+ });
+ }
+ });
+ });
+ });
+}
+
+String _getArgumentName(int count, NamedType argument) =>
+ argument.name.isEmpty ? 'arg$count' : argument.name;
+
+/// Returns an argument name that can be used in a context where it is possible to collide.
+String _getSafeArgumentName(int count, NamedType argument) =>
+ '${_getArgumentName(count, argument)}Arg';
+
+/// Writes the code for a flutter [Api], [api].
+/// Example:
+/// class Foo(private val binaryMessenger: BinaryMessenger) {
+/// fun add(x: Int, y: Int, callback: (Int?) -> Unit) {...}
+/// }
+void _writeFlutterApi(Indent indent, Api api) {
+ assert(api.location == ApiLocation.flutter);
+ indent.writeln(
+ '/** Generated class from Pigeon that represents Flutter messages that can be called from Kotlin. */');
+ final String apiName = api.name;
+ indent.writeln('@Suppress("UNCHECKED_CAST")');
+ indent.write('class $apiName(private val binaryMessenger: BinaryMessenger) ');
+ indent.scoped('{', '}', () {
+ indent.write('companion object ');
+ indent.scoped('{', '}', () {
+ indent.writeln('/** The codec used by $apiName. */');
+ indent.write('val codec: MessageCodec<Any?> by lazy ');
+ indent.scoped('{', '}', () {
+ indent.writeln(_getCodecName(api));
+ });
+ });
+
+ for (final Method func in api.methods) {
+ final String channelName = makeChannelName(api, func);
+ final String returnType = func.returnType.isVoid
+ ? ''
+ : _nullsafeKotlinTypeForDartType(func.returnType);
+ String sendArgument;
+ if (func.arguments.isEmpty) {
+ indent.write('fun ${func.name}(callback: ($returnType) -> Unit) ');
+ sendArgument = 'null';
+ } else {
+ final Iterable<String> argTypes = func.arguments
+ .map((NamedType e) => _nullsafeKotlinTypeForDartType(e.type));
+ final Iterable<String> argNames =
+ indexMap(func.arguments, _getSafeArgumentName);
+ sendArgument = 'listOf(${argNames.join(', ')})';
+ final String argsSignature = map2(argTypes, argNames,
+ (String type, String name) => '$name: $type').join(', ');
+ if (func.returnType.isVoid) {
+ indent
+ .write('fun ${func.name}($argsSignature, callback: () -> Unit) ');
+ } else {
+ indent.write(
+ 'fun ${func.name}($argsSignature, callback: ($returnType) -> Unit) ');
+ }
+ }
+ indent.scoped('{', '}', () {
+ const String channel = 'channel';
+ indent.writeln(
+ 'val $channel = BasicMessageChannel<Any?>(binaryMessenger, "$channelName", codec)');
+ indent.write('$channel.send($sendArgument) ');
+ if (func.returnType.isVoid) {
+ indent.scoped('{', '}', () {
+ indent.writeln('callback()');
+ });
+ } else {
+ final String forceUnwrap = func.returnType.isNullable ? '?' : '';
+ indent.scoped('{', '}', () {
+ indent.writeln('val result = it as$forceUnwrap $returnType');
+ indent.writeln('callback(result)');
+ });
+ }
+ });
+ }
+ });
+}
+
+String _castForceUnwrap(String value, TypeDeclaration type, Root root) {
+ if (isEnum(root, type)) {
+ final String forceUnwrap = type.isNullable ? '' : '!!';
+ final String nullableConditionPrefix =
+ type.isNullable ? '$value == null ? null : ' : '';
+ return '$nullableConditionPrefix${_kotlinTypeForDartType(type)}.ofRaw($value as Int)$forceUnwrap';
+ } else {
+ final String castUnwrap = type.isNullable ? '?' : '';
+
+ // The StandardMessageCodec can give us [Integer, Long] for
+ // a Dart 'int'. To keep things simple we just use 64bit
+ // longs in Pigeon with Kotlin.
+ if (type.baseName == 'int') {
+ return '$value.let { if (it is Int) it.toLong() else it as$castUnwrap Long }';
+ } else {
+ return '$value as$castUnwrap ${_kotlinTypeForDartType(type)}';
+ }
+ }
+}
+
+/// Converts a [List] of [TypeDeclaration]s to a comma separated [String] to be
+/// used in Kotlin code.
+String _flattenTypeArguments(List<TypeDeclaration> args) {
+ return args.map(_kotlinTypeForDartType).join(', ');
+}
+
+String _kotlinTypeForBuiltinGenericDartType(TypeDeclaration type) {
+ if (type.typeArguments.isEmpty) {
+ switch (type.baseName) {
+ case 'List':
+ return 'List<Any?>';
+ case 'Map':
+ return 'Map<Any, Any?>';
+ default:
+ return 'Any';
+ }
+ } else {
+ switch (type.baseName) {
+ case 'List':
+ return 'List<${_nullsafeKotlinTypeForDartType(type.typeArguments.first)}>';
+ case 'Map':
+ return 'Map<${_nullsafeKotlinTypeForDartType(type.typeArguments.first)}, ${_nullsafeKotlinTypeForDartType(type.typeArguments.last)}>';
+ default:
+ return '${type.baseName}<${_flattenTypeArguments(type.typeArguments)}>';
+ }
+ }
+}
+
+String? _kotlinTypeForBuiltinDartType(TypeDeclaration type) {
+ const Map<String, String> kotlinTypeForDartTypeMap = <String, String>{
+ 'void': 'Void',
+ 'bool': 'Boolean',
+ 'String': 'String',
+ 'int': 'Long',
+ 'double': 'Double',
+ 'Uint8List': 'ByteArray',
+ 'Int32List': 'IntArray',
+ 'Int64List': 'LongArray',
+ 'Float32List': 'FloatArray',
+ 'Float64List': 'DoubleArray',
+ 'Object': 'Any',
+ };
+ if (kotlinTypeForDartTypeMap.containsKey(type.baseName)) {
+ return kotlinTypeForDartTypeMap[type.baseName];
+ } else if (type.baseName == 'List' || type.baseName == 'Map') {
+ return _kotlinTypeForBuiltinGenericDartType(type);
+ } else {
+ return null;
+ }
+}
+
+String _kotlinTypeForDartType(TypeDeclaration type) {
+ return _kotlinTypeForBuiltinDartType(type) ?? type.baseName;
+}
+
+String _nullsafeKotlinTypeForDartType(TypeDeclaration type) {
+ final String nullSafe = type.isNullable ? '?' : '';
+ return '${_kotlinTypeForDartType(type)}$nullSafe';
+}
+
+/// Generates the ".kotlin" file for the AST represented by [root] to [sink] with the
+/// provided [options].
+void generateKotlin(KotlinOptions options, Root root, StringSink sink) {
+ final Set<String> rootClassNameSet =
+ root.classes.map((Class x) => x.name).toSet();
+ final Set<String> rootEnumNameSet =
+ root.enums.map((Enum x) => x.name).toSet();
+ final Indent indent = Indent(sink);
+
+ HostDatatype getHostDatatype(NamedType field) {
+ return getFieldHostDatatype(field, root.classes, root.enums,
+ (TypeDeclaration x) => _kotlinTypeForBuiltinDartType(x));
+ }
+
+ void writeHeader() {
+ if (options.copyrightHeader != null) {
+ addLines(indent, options.copyrightHeader!, linePrefix: '// ');
+ }
+ indent.writeln('// $generatedCodeWarning');
+ indent.writeln('// $seeAlsoWarning');
+ }
+
+ void writeImports() {
+ indent.writeln('import android.util.Log');
+ indent.writeln('import io.flutter.plugin.common.BasicMessageChannel');
+ indent.writeln('import io.flutter.plugin.common.BinaryMessenger');
+ indent.writeln('import io.flutter.plugin.common.MessageCodec');
+ indent.writeln('import io.flutter.plugin.common.StandardMessageCodec');
+ indent.writeln('import java.io.ByteArrayOutputStream');
+ indent.writeln('import java.nio.ByteBuffer');
+ }
+
+ void writeEnum(Enum anEnum) {
+ indent.write('enum class ${anEnum.name}(val raw: Int) ');
+ indent.scoped('{', '}', () {
+ // We use explicit indexing here as use of the ordinal() method is
+ // discouraged. The toMap and fromMap API matches class API to allow
+ // the same code to work with enums and classes, but this
+ // can also be done directly in the host and flutter APIs.
+ int index = 0;
+ for (final String member in anEnum.members) {
+ indent.write('${member.toUpperCase()}($index)');
+ if (index != anEnum.members.length - 1) {
+ indent.addln(',');
+ } else {
+ indent.addln(';');
+ }
+ index++;
+ }
+
+ indent.writeln('');
+ indent.write('companion object ');
+ indent.scoped('{', '}', () {
+ indent.write('fun ofRaw(raw: Int): ${anEnum.name}? ');
+ indent.scoped('{', '}', () {
+ indent.writeln('return values().firstOrNull { it.raw == raw }');
+ });
+ });
+ });
+ }
+
+ void writeDataClass(Class klass) {
+ void writeField(NamedType field) {
+ indent.write(
+ 'val ${field.name}: ${_nullsafeKotlinTypeForDartType(field.type)}');
+ final String defaultNil = field.type.isNullable ? ' = null' : '';
+ indent.add(defaultNil);
+ }
+
+ void writeToMap() {
+ indent.write('fun toMap(): Map<String, Any?> ');
+ indent.scoped('{', '}', () {
+ indent.writeln('val map = mutableMapOf<String, Any?>()');
+
+ for (final NamedType field in klass.fields) {
+ final HostDatatype hostDatatype = getHostDatatype(field);
+ String toWriteValue = '';
+ final String fieldName = field.name;
+ final String prefix = field.type.isNullable ? 'it' : fieldName;
+ if (!hostDatatype.isBuiltin &&
+ rootClassNameSet.contains(field.type.baseName)) {
+ toWriteValue = '$prefix.toMap()';
+ } else if (!hostDatatype.isBuiltin &&
+ rootEnumNameSet.contains(field.type.baseName)) {
+ toWriteValue = '$prefix.raw';
+ } else {
+ toWriteValue = prefix;
+ }
+
+ if (field.type.isNullable) {
+ indent.writeln(
+ '$fieldName?.let { map["${field.name}"] = $toWriteValue }');
+ } else {
+ indent.writeln('map["${field.name}"] = $toWriteValue');
+ }
+ }
+
+ indent.writeln('return map');
+ });
+ }
+
+ void writeFromMap() {
+ final String className = klass.name;
+
+ indent.write('companion object ');
+ indent.scoped('{', '}', () {
+ indent.writeln('@Suppress("UNCHECKED_CAST")');
+ indent.write('fun fromMap(map: Map<String, Any?>): $className ');
+
+ indent.scoped('{', '}', () {
+ for (final NamedType field in klass.fields) {
+ final HostDatatype hostDatatype = getHostDatatype(field);
+
+ // The StandardMessageCodec can give us [Integer, Long] for
+ // a Dart 'int'. To keep things simple we just use 64bit
+ // longs in Pigeon with Kotlin.
+ final bool isInt = field.type.baseName == 'int';
+
+ final String mapValue = 'map["${field.name}"]';
+ final String fieldType = _kotlinTypeForDartType(field.type);
+
+ if (field.type.isNullable) {
+ if (!hostDatatype.isBuiltin &&
+ rootClassNameSet.contains(field.type.baseName)) {
+ indent.write('val ${field.name}: $fieldType? = ');
+ indent.add('($mapValue as? Map<String, Any?>)?.let ');
+ indent.scoped('{', '}', () {
+ indent.writeln('$fieldType.fromMap(it)');
+ });
+ } else if (!hostDatatype.isBuiltin &&
+ rootEnumNameSet.contains(field.type.baseName)) {
+ indent.write('val ${field.name}: $fieldType? = ');
+ indent.add('($mapValue as? Int)?.let ');
+ indent.scoped('{', '}', () {
+ indent.writeln('$fieldType.ofRaw(it)');
+ });
+ } else if (isInt) {
+ indent.write('val ${field.name} = $mapValue');
+ indent.addln(
+ '.let { if (it is Int) it.toLong() else it as? Long }');
+ } else {
+ indent.writeln('val ${field.name} = $mapValue as? $fieldType');
+ }
+ } else {
+ if (!hostDatatype.isBuiltin &&
+ rootClassNameSet.contains(field.type.baseName)) {
+ indent.writeln(
+ 'val ${field.name} = $fieldType.fromMap($mapValue as Map<String, Any?>)');
+ } else if (!hostDatatype.isBuiltin &&
+ rootEnumNameSet.contains(field.type.baseName)) {
+ indent.write(
+ 'val ${field.name} = $fieldType.ofRaw($mapValue as Int)!!');
+ } else {
+ indent.writeln('val ${field.name} = $mapValue as $fieldType');
+ }
+ }
+ }
+
+ indent.writeln('');
+ indent.write('return $className(');
+ for (final NamedType field in klass.fields) {
+ final String comma = klass.fields.last == field ? '' : ', ';
+ indent.add('${field.name}$comma');
+ }
+ indent.addln(')');
+ });
+ });
+ }
+
+ indent.writeln(
+ '/** Generated class from Pigeon that represents data sent in messages. */');
+ indent.write('data class ${klass.name}(');
+ indent.scoped('', '', () {
+ for (final NamedType element in klass.fields) {
+ writeField(element);
+ if (klass.fields.last != element) {
+ indent.addln(',');
+ }
+ }
+ });
+
+ indent.scoped(') {', '}', () {
+ writeFromMap();
+ writeToMap();
+ });
+ }
+
+ void writeApi(Api api) {
+ if (api.location == ApiLocation.host) {
+ _writeHostApi(indent, api, root);
+ } else if (api.location == ApiLocation.flutter) {
+ _writeFlutterApi(indent, api);
+ }
+ }
+
+ void writeWrapResult() {
+ indent.write('private fun wrapResult(result: Any?): Map<String, Any?> ');
+ indent.scoped('{', '}', () {
+ indent.writeln('return hashMapOf("result" to result)');
+ });
+ }
+
+ void writeWrapError() {
+ indent.write(
+ 'private fun wrapError(exception: Throwable): Map<String, Any> ');
+ indent.scoped('{', '}', () {
+ indent.write('return ');
+ indent.scoped('hashMapOf<String, Any>(', ')', () {
+ indent.write('"error" to ');
+ indent.scoped('hashMapOf<String, Any>(', ')', () {
+ indent.writeln(
+ '"${Keys.errorCode}" to exception.javaClass.simpleName,');
+ indent.writeln('"${Keys.errorMessage}" to exception.toString(),');
+ indent.writeln(
+ '"${Keys.errorDetails}" to "Cause: " + exception.cause + ", Stacktrace: " + Log.getStackTraceString(exception)');
+ });
+ });
+ });
+ }
+
+ writeHeader();
+ indent.addln('');
+ if (options.package != null) {
+ indent.writeln('package ${options.package}');
+ }
+ indent.addln('');
+ writeImports();
+ indent.addln('');
+ indent.writeln('/** Generated class from Pigeon. */');
+ for (final Enum anEnum in root.enums) {
+ indent.writeln('');
+ writeEnum(anEnum);
+ }
+
+ for (final Class klass in root.classes) {
+ indent.addln('');
+ writeDataClass(klass);
+ }
+
+ if (root.apis.any((Api api) =>
+ api.location == ApiLocation.host &&
+ api.methods.any((Method it) => it.isAsynchronous))) {
+ indent.addln('');
+ }
+
+ for (final Api api in root.apis) {
+ _writeCodec(indent, api, root);
+ indent.addln('');
+ writeApi(api);
+ }
+
+ indent.addln('');
+ writeWrapResult();
+ indent.addln('');
+ writeWrapError();
+}
diff --git a/packages/pigeon/lib/pigeon.dart b/packages/pigeon/lib/pigeon.dart
index d4875c3..7f2fb75 100644
--- a/packages/pigeon/lib/pigeon.dart
+++ b/packages/pigeon/lib/pigeon.dart
@@ -7,6 +7,7 @@
export 'cpp_generator.dart' show CppOptions;
export 'dart_generator.dart' show DartOptions;
export 'java_generator.dart' show JavaOptions;
+export 'kotlin_generator.dart' show KotlinOptions;
export 'objc_generator.dart' show ObjcOptions;
export 'pigeon_lib.dart';
export 'swift_generator.dart' show SwiftOptions;
diff --git a/packages/pigeon/lib/pigeon_lib.dart b/packages/pigeon/lib/pigeon_lib.dart
index 32c6122..2c04ed3 100644
--- a/packages/pigeon/lib/pigeon_lib.dart
+++ b/packages/pigeon/lib/pigeon_lib.dart
@@ -30,6 +30,7 @@
import 'generator_tools.dart';
import 'generator_tools.dart' as generator_tools;
import 'java_generator.dart';
+import 'kotlin_generator.dart';
import 'objc_generator.dart';
import 'swift_generator.dart';
@@ -164,6 +165,8 @@
this.javaOptions,
this.swiftOut,
this.swiftOptions,
+ this.kotlinOut,
+ this.kotlinOptions,
this.cppHeaderOut,
this.cppSourceOut,
this.cppOptions,
@@ -203,6 +206,12 @@
/// Options that control how Swift will be generated.
final SwiftOptions? swiftOptions;
+ /// Path to the kotlin file that will be generated.
+ final String? kotlinOut;
+
+ /// Options that control how Kotlin will be generated.
+ final KotlinOptions? kotlinOptions;
+
/// Path to the ".h" C++ file that will be generated.
final String? cppHeaderOut;
@@ -247,6 +256,11 @@
swiftOptions: map.containsKey('swiftOptions')
? SwiftOptions.fromMap((map['swiftOptions'] as Map<String, Object>?)!)
: null,
+ kotlinOut: map['kotlinOut'] as String?,
+ kotlinOptions: map.containsKey('kotlinOptions')
+ ? KotlinOptions.fromMap(
+ (map['kotlinOptions'] as Map<String, Object>?)!)
+ : null,
cppHeaderOut: map['experimental_cppHeaderOut'] as String?,
cppSourceOut: map['experimental_cppSourceOut'] as String?,
cppOptions: map.containsKey('experimental_cppOptions')
@@ -277,6 +291,8 @@
if (javaOptions != null) 'javaOptions': javaOptions!.toMap(),
if (swiftOut != null) 'swiftOut': swiftOut!,
if (swiftOptions != null) 'swiftOptions': swiftOptions!.toMap(),
+ if (kotlinOut != null) 'kotlinOut': kotlinOut!,
+ if (kotlinOptions != null) 'kotlinOptions': kotlinOptions!.toMap(),
if (cppHeaderOut != null) 'experimental_cppHeaderOut': cppHeaderOut!,
if (cppSourceOut != null) 'experimental_cppSourceOut': cppSourceOut!,
if (cppOptions != null) 'experimental_cppOptions': cppOptions!.toMap(),
@@ -571,6 +587,29 @@
List<Error> validate(PigeonOptions options, Root root) => <Error>[];
}
+/// A [Generator] that generates Kotlin source code.
+class KotlinGenerator implements Generator {
+ /// Constructor for [KotlinGenerator].
+ const KotlinGenerator();
+
+ @override
+ void generate(StringSink sink, PigeonOptions options, Root root) {
+ KotlinOptions kotlinOptions =
+ options.kotlinOptions ?? const KotlinOptions();
+ kotlinOptions = kotlinOptions.merge(KotlinOptions(
+ copyrightHeader: options.copyrightHeader != null
+ ? _lineReader(options.copyrightHeader!)
+ : null));
+ generateKotlin(kotlinOptions, root, sink);
+ }
+
+ @override
+ IOSink? shouldGenerate(PigeonOptions options) => _openSink(options.kotlinOut);
+
+ @override
+ List<Error> validate(PigeonOptions options, Root root) => <Error>[];
+}
+
dart_ast.Annotation? _findMetadata(
dart_ast.NodeList<dart_ast.Annotation> metadata, String query) {
final Iterable<dart_ast.Annotation> annotations = metadata
@@ -1202,6 +1241,11 @@
help: 'Adds the java.annotation.Generated annotation to the output.')
..addOption('experimental_swift_out',
help: 'Path to generated Swift file (.swift).')
+ ..addOption('experimental_kotlin_out',
+ help: 'Path to generated Kotlin file (.kt). (experimental)')
+ ..addOption('experimental_kotlin_package',
+ help:
+ 'The package that generated Kotlin code will be in. (experimental)')
..addOption('experimental_cpp_header_out',
help: 'Path to generated C++ header file (.h). (experimental)')
..addOption('experimental_cpp_source_out',
@@ -1247,6 +1291,10 @@
useGeneratedAnnotation: results['java_use_generated_annotation'],
),
swiftOut: results['experimental_swift_out'],
+ kotlinOut: results['experimental_kotlin_out'],
+ kotlinOptions: KotlinOptions(
+ package: results['experimental_kotlin_package'],
+ ),
cppHeaderOut: results['experimental_cpp_header_out'],
cppSourceOut: results['experimental_cpp_source_out'],
cppOptions: CppOptions(
@@ -1295,6 +1343,7 @@
const DartGenerator(),
const JavaGenerator(),
const SwiftGenerator(),
+ const KotlinGenerator(),
const CppHeaderGenerator(),
const CppSourceGenerator(),
const DartTestGenerator(),
diff --git a/packages/pigeon/pigeons/background_platform_channels.dart b/packages/pigeon/pigeons/background_platform_channels.dart
index c21be6c..643b880 100644
--- a/packages/pigeon/pigeons/background_platform_channels.dart
+++ b/packages/pigeon/pigeons/background_platform_channels.dart
@@ -5,7 +5,7 @@
import 'package:pigeon/pigeon.dart';
@HostApi()
-abstract class Api2Host {
+abstract class BackgroundApi2Host {
@TaskQueue(type: TaskQueueType.serialBackgroundThread)
int add(int x, int y);
}
diff --git a/packages/pigeon/platform_tests/android_kotlin_unit_tests/.gitignore b/packages/pigeon/platform_tests/android_kotlin_unit_tests/.gitignore
new file mode 100644
index 0000000..0fa6b67
--- /dev/null
+++ b/packages/pigeon/platform_tests/android_kotlin_unit_tests/.gitignore
@@ -0,0 +1,46 @@
+# Miscellaneous
+*.class
+*.log
+*.pyc
+*.swp
+.DS_Store
+.atom/
+.buildlog/
+.history
+.svn/
+
+# IntelliJ related
+*.iml
+*.ipr
+*.iws
+.idea/
+
+# The .vscode folder contains launch configuration and tasks you configure in
+# VS Code which you may wish to be included in version control, so this line
+# is commented out by default.
+#.vscode/
+
+# Flutter/Dart/Pub related
+**/doc/api/
+**/ios/Flutter/.last_build_id
+.dart_tool/
+.flutter-plugins
+.flutter-plugins-dependencies
+.packages
+.pub-cache/
+.pub/
+/build/
+
+# Web related
+lib/generated_plugin_registrant.dart
+
+# Symbolication related
+app.*.symbols
+
+# Obfuscation related
+app.*.map.json
+
+# Android Studio will place build artifacts here
+/android/app/debug
+/android/app/profile
+/android/app/release
diff --git a/packages/pigeon/platform_tests/android_kotlin_unit_tests/.metadata b/packages/pigeon/platform_tests/android_kotlin_unit_tests/.metadata
new file mode 100644
index 0000000..62b61c3
--- /dev/null
+++ b/packages/pigeon/platform_tests/android_kotlin_unit_tests/.metadata
@@ -0,0 +1,10 @@
+# This file tracks properties of this Flutter project.
+# Used by Flutter tool to assess capabilities and perform upgrades etc.
+#
+# This file should be version controlled and should not be manually edited.
+
+version:
+ revision: 891511d58f6550ce9e9b03b8d7c6a602caa97488
+ channel: master
+
+project_type: app
diff --git a/packages/pigeon/platform_tests/android_kotlin_unit_tests/README.md b/packages/pigeon/platform_tests/android_kotlin_unit_tests/README.md
new file mode 100644
index 0000000..d2383b7
--- /dev/null
+++ b/packages/pigeon/platform_tests/android_kotlin_unit_tests/README.md
@@ -0,0 +1,3 @@
+# android_kotlin_unit_tests
+
+Unit-tests for Pigeon generated Kotlin code. See [../../tools/run_tests.dart](../../tools/run_tests.dart).
diff --git a/packages/pigeon/platform_tests/android_kotlin_unit_tests/android/.gitignore b/packages/pigeon/platform_tests/android_kotlin_unit_tests/android/.gitignore
new file mode 100644
index 0000000..0a741cb
--- /dev/null
+++ b/packages/pigeon/platform_tests/android_kotlin_unit_tests/android/.gitignore
@@ -0,0 +1,11 @@
+gradle-wrapper.jar
+/.gradle
+/captures/
+/gradlew
+/gradlew.bat
+/local.properties
+GeneratedPluginRegistrant.java
+
+# Remember to never publicly share your keystore.
+# See https://flutter.dev/docs/deployment/android#reference-the-keystore-from-the-app
+key.properties
diff --git a/packages/pigeon/platform_tests/android_kotlin_unit_tests/android/.project b/packages/pigeon/platform_tests/android_kotlin_unit_tests/android/.project
new file mode 100644
index 0000000..fe67538
--- /dev/null
+++ b/packages/pigeon/platform_tests/android_kotlin_unit_tests/android/.project
@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<projectDescription>
+ <name>android_</name>
+ <comment>Project android_ created by Buildship.</comment>
+ <projects>
+ </projects>
+ <buildSpec>
+ <buildCommand>
+ <name>org.eclipse.buildship.core.gradleprojectbuilder</name>
+ <arguments>
+ </arguments>
+ </buildCommand>
+ </buildSpec>
+ <natures>
+ <nature>org.eclipse.buildship.core.gradleprojectnature</nature>
+ </natures>
+ <filteredResources>
+ <filter>
+ <id>1624921942866</id>
+ <name></name>
+ <type>30</type>
+ <matcher>
+ <id>org.eclipse.core.resources.regexFilterMatcher</id>
+ <arguments>node_modules|.git|__CREATED_BY_JAVA_LANGUAGE_SERVER__</arguments>
+ </matcher>
+ </filter>
+ </filteredResources>
+</projectDescription>
diff --git a/packages/pigeon/platform_tests/android_kotlin_unit_tests/android/app/build.gradle b/packages/pigeon/platform_tests/android_kotlin_unit_tests/android/app/build.gradle
new file mode 100644
index 0000000..38eaf17
--- /dev/null
+++ b/packages/pigeon/platform_tests/android_kotlin_unit_tests/android/app/build.gradle
@@ -0,0 +1,70 @@
+def localProperties = new Properties()
+def localPropertiesFile = rootProject.file('local.properties')
+if (localPropertiesFile.exists()) {
+ localPropertiesFile.withReader('UTF-8') { reader ->
+ localProperties.load(reader)
+ }
+}
+
+def flutterRoot = localProperties.getProperty('flutter.sdk')
+if (flutterRoot == null) {
+ throw new GradleException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.")
+}
+
+def flutterVersionCode = localProperties.getProperty('flutter.versionCode')
+if (flutterVersionCode == null) {
+ flutterVersionCode = '1'
+}
+
+def flutterVersionName = localProperties.getProperty('flutter.versionName')
+if (flutterVersionName == null) {
+ flutterVersionName = '1.0'
+}
+
+apply plugin: 'com.android.application'
+apply plugin: 'kotlin-android'
+apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle"
+
+android {
+ compileSdkVersion 31
+
+ sourceSets {
+ main.java.srcDirs += 'src/main/kotlin'
+ }
+
+ defaultConfig {
+ // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
+ applicationId "com.example.android_kotlin_unit_tests"
+ minSdkVersion 16
+ targetSdkVersion 30
+ versionCode flutterVersionCode.toInteger()
+ versionName flutterVersionName
+ }
+
+ buildTypes {
+ release {
+ // TODO: Add your own signing config for the release build.
+ // Signing with the debug keys for now, so `flutter run --release` works.
+ signingConfig signingConfigs.debug
+ }
+ }
+
+ testOptions {
+ unitTests.returnDefaultValues = true
+ }
+}
+
+flutter {
+ source '../..'
+}
+
+dependencies {
+ implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
+ testImplementation 'junit:junit:4.+'
+ testImplementation "io.mockk:mockk:1.12.4"
+}
+
+
+//test {
+// useJUnitPlatform()
+//}
\ No newline at end of file
diff --git a/packages/pigeon/platform_tests/android_kotlin_unit_tests/android/app/src/debug/AndroidManifest.xml b/packages/pigeon/platform_tests/android_kotlin_unit_tests/android/app/src/debug/AndroidManifest.xml
new file mode 100644
index 0000000..fac2664
--- /dev/null
+++ b/packages/pigeon/platform_tests/android_kotlin_unit_tests/android/app/src/debug/AndroidManifest.xml
@@ -0,0 +1,7 @@
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="com.example.android_kotlin_unit_tests">
+ <!-- Flutter needs it to communicate with the running application
+ to allow setting breakpoints, to provide hot reload, etc.
+ -->
+ <uses-permission android:name="android.permission.INTERNET"/>
+</manifest>
diff --git a/packages/pigeon/platform_tests/android_kotlin_unit_tests/android/app/src/main/AndroidManifest.xml b/packages/pigeon/platform_tests/android_kotlin_unit_tests/android/app/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..27ed867
--- /dev/null
+++ b/packages/pigeon/platform_tests/android_kotlin_unit_tests/android/app/src/main/AndroidManifest.xml
@@ -0,0 +1,41 @@
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="com.example.android_kotlin_unit_tests">
+ <application
+ android:label="android_kotlin_unit_tests"
+ android:icon="@mipmap/ic_launcher">
+ <activity
+ android:name=".MainActivity"
+ android:launchMode="singleTop"
+ android:theme="@style/LaunchTheme"
+ android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
+ android:hardwareAccelerated="true"
+ android:windowSoftInputMode="adjustResize">
+ <!-- Specifies an Android theme to apply to this Activity as soon as
+ the Android process has started. This theme is visible to the user
+ while the Flutter UI initializes. After that, this theme continues
+ to determine the Window background behind the Flutter UI. -->
+ <meta-data
+ android:name="io.flutter.embedding.android.NormalTheme"
+ android:resource="@style/NormalTheme"
+ />
+ <!-- Displays an Android View that continues showing the launch screen
+ Drawable until Flutter paints its first frame, then this splash
+ screen fades out. A splash screen is useful to avoid any visual
+ gap between the end of Android's launch screen and the painting of
+ Flutter's first frame. -->
+ <meta-data
+ android:name="io.flutter.embedding.android.SplashScreenDrawable"
+ android:resource="@drawable/launch_background"
+ />
+ <intent-filter>
+ <action android:name="android.intent.action.MAIN"/>
+ <category android:name="android.intent.category.LAUNCHER"/>
+ </intent-filter>
+ </activity>
+ <!-- Don't delete the meta-data below.
+ This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
+ <meta-data
+ android:name="flutterEmbedding"
+ android:value="2" />
+ </application>
+</manifest>
diff --git a/packages/pigeon/platform_tests/android_kotlin_unit_tests/android/app/src/main/kotlin/com/example/android_kotlin_unit_tests/.gitignore b/packages/pigeon/platform_tests/android_kotlin_unit_tests/android/app/src/main/kotlin/com/example/android_kotlin_unit_tests/.gitignore
new file mode 100644
index 0000000..78d4294
--- /dev/null
+++ b/packages/pigeon/platform_tests/android_kotlin_unit_tests/android/app/src/main/kotlin/com/example/android_kotlin_unit_tests/.gitignore
@@ -0,0 +1 @@
+*.kt
diff --git a/packages/pigeon/platform_tests/android_kotlin_unit_tests/android/app/src/main/res/drawable-v21/launch_background.xml b/packages/pigeon/platform_tests/android_kotlin_unit_tests/android/app/src/main/res/drawable-v21/launch_background.xml
new file mode 100644
index 0000000..f74085f
--- /dev/null
+++ b/packages/pigeon/platform_tests/android_kotlin_unit_tests/android/app/src/main/res/drawable-v21/launch_background.xml
@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Modify this file to customize your launch splash screen -->
+<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
+ <item android:drawable="?android:colorBackground" />
+
+ <!-- You can insert your own image assets here -->
+ <!-- <item>
+ <bitmap
+ android:gravity="center"
+ android:src="@mipmap/launch_image" />
+ </item> -->
+</layer-list>
diff --git a/packages/pigeon/platform_tests/android_kotlin_unit_tests/android/app/src/main/res/drawable/launch_background.xml b/packages/pigeon/platform_tests/android_kotlin_unit_tests/android/app/src/main/res/drawable/launch_background.xml
new file mode 100644
index 0000000..304732f
--- /dev/null
+++ b/packages/pigeon/platform_tests/android_kotlin_unit_tests/android/app/src/main/res/drawable/launch_background.xml
@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Modify this file to customize your launch splash screen -->
+<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
+ <item android:drawable="@android:color/white" />
+
+ <!-- You can insert your own image assets here -->
+ <!-- <item>
+ <bitmap
+ android:gravity="center"
+ android:src="@mipmap/launch_image" />
+ </item> -->
+</layer-list>
diff --git a/packages/pigeon/platform_tests/android_kotlin_unit_tests/android/app/src/main/res/mipmap-hdpi/ic_launcher.png b/packages/pigeon/platform_tests/android_kotlin_unit_tests/android/app/src/main/res/mipmap-hdpi/ic_launcher.png
new file mode 100644
index 0000000..a60e5e3
--- /dev/null
+++ b/packages/pigeon/platform_tests/android_kotlin_unit_tests/android/app/src/main/res/mipmap-hdpi/ic_launcher.png
Binary files differ
diff --git a/packages/pigeon/platform_tests/android_kotlin_unit_tests/android/app/src/main/res/mipmap-mdpi/ic_launcher.png b/packages/pigeon/platform_tests/android_kotlin_unit_tests/android/app/src/main/res/mipmap-mdpi/ic_launcher.png
new file mode 100644
index 0000000..a60e5e3
--- /dev/null
+++ b/packages/pigeon/platform_tests/android_kotlin_unit_tests/android/app/src/main/res/mipmap-mdpi/ic_launcher.png
Binary files differ
diff --git a/packages/pigeon/platform_tests/android_kotlin_unit_tests/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/packages/pigeon/platform_tests/android_kotlin_unit_tests/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png
new file mode 100644
index 0000000..a60e5e3
--- /dev/null
+++ b/packages/pigeon/platform_tests/android_kotlin_unit_tests/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png
Binary files differ
diff --git a/packages/pigeon/platform_tests/android_kotlin_unit_tests/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/packages/pigeon/platform_tests/android_kotlin_unit_tests/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
new file mode 100644
index 0000000..a60e5e3
--- /dev/null
+++ b/packages/pigeon/platform_tests/android_kotlin_unit_tests/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
Binary files differ
diff --git a/packages/pigeon/platform_tests/android_kotlin_unit_tests/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/packages/pigeon/platform_tests/android_kotlin_unit_tests/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
new file mode 100644
index 0000000..a60e5e3
--- /dev/null
+++ b/packages/pigeon/platform_tests/android_kotlin_unit_tests/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
Binary files differ
diff --git a/packages/pigeon/platform_tests/android_kotlin_unit_tests/android/app/src/main/res/values-night/styles.xml b/packages/pigeon/platform_tests/android_kotlin_unit_tests/android/app/src/main/res/values-night/styles.xml
new file mode 100644
index 0000000..449a9f9
--- /dev/null
+++ b/packages/pigeon/platform_tests/android_kotlin_unit_tests/android/app/src/main/res/values-night/styles.xml
@@ -0,0 +1,18 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Theme applied to the Android Window while the process is starting when the OS's Dark Mode setting is on -->
+ <style name="LaunchTheme" parent="@android:style/Theme.Black.NoTitleBar">
+ <!-- Show a splash screen on the activity. Automatically removed when
+ Flutter draws its first frame -->
+ <item name="android:windowBackground">@drawable/launch_background</item>
+ </style>
+ <!-- Theme applied to the Android Window as soon as the process has started.
+ This theme determines the color of the Android Window while your
+ Flutter UI initializes, as well as behind your Flutter UI while its
+ running.
+
+ This Theme is only used starting with V2 of Flutter's Android embedding. -->
+ <style name="NormalTheme" parent="@android:style/Theme.Black.NoTitleBar">
+ <item name="android:windowBackground">?android:colorBackground</item>
+ </style>
+</resources>
diff --git a/packages/pigeon/platform_tests/android_kotlin_unit_tests/android/app/src/main/res/values/styles.xml b/packages/pigeon/platform_tests/android_kotlin_unit_tests/android/app/src/main/res/values/styles.xml
new file mode 100644
index 0000000..d74aa35
--- /dev/null
+++ b/packages/pigeon/platform_tests/android_kotlin_unit_tests/android/app/src/main/res/values/styles.xml
@@ -0,0 +1,18 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Theme applied to the Android Window while the process is starting when the OS's Dark Mode setting is off -->
+ <style name="LaunchTheme" parent="@android:style/Theme.Light.NoTitleBar">
+ <!-- Show a splash screen on the activity. Automatically removed when
+ Flutter draws its first frame -->
+ <item name="android:windowBackground">@drawable/launch_background</item>
+ </style>
+ <!-- Theme applied to the Android Window as soon as the process has started.
+ This theme determines the color of the Android Window while your
+ Flutter UI initializes, as well as behind your Flutter UI while its
+ running.
+
+ This Theme is only used starting with V2 of Flutter's Android embedding. -->
+ <style name="NormalTheme" parent="@android:style/Theme.Light.NoTitleBar">
+ <item name="android:windowBackground">?android:colorBackground</item>
+ </style>
+</resources>
diff --git a/packages/pigeon/platform_tests/android_kotlin_unit_tests/android/app/src/profile/AndroidManifest.xml b/packages/pigeon/platform_tests/android_kotlin_unit_tests/android/app/src/profile/AndroidManifest.xml
new file mode 100644
index 0000000..fac2664
--- /dev/null
+++ b/packages/pigeon/platform_tests/android_kotlin_unit_tests/android/app/src/profile/AndroidManifest.xml
@@ -0,0 +1,7 @@
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="com.example.android_kotlin_unit_tests">
+ <!-- Flutter needs it to communicate with the running application
+ to allow setting breakpoints, to provide hot reload, etc.
+ -->
+ <uses-permission android:name="android.permission.INTERNET"/>
+</manifest>
diff --git a/packages/pigeon/platform_tests/android_kotlin_unit_tests/android/app/src/test/java/com/example/android_kotlin_unit_tests/AllDatatypesTest.kt b/packages/pigeon/platform_tests/android_kotlin_unit_tests/android/app/src/test/java/com/example/android_kotlin_unit_tests/AllDatatypesTest.kt
new file mode 100644
index 0000000..87a94ca
--- /dev/null
+++ b/packages/pigeon/platform_tests/android_kotlin_unit_tests/android/app/src/test/java/com/example/android_kotlin_unit_tests/AllDatatypesTest.kt
@@ -0,0 +1,112 @@
+// Copyright 2013 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+package com.example.android_kotlin_unit_tests
+
+import io.flutter.plugin.common.BinaryMessenger
+import io.mockk.every
+import io.mockk.mockk
+import junit.framework.TestCase
+import org.junit.Test
+import java.nio.ByteBuffer
+import java.util.ArrayList
+
+
+internal class AllDatatypesTest: TestCase() {
+ @Test
+ fun testNullValues() {
+ val everything = Everything()
+ val binaryMessenger = mockk<BinaryMessenger>()
+ val api = FlutterEverything(binaryMessenger)
+
+ every { binaryMessenger.send(any(), any(), any()) } answers {
+ val codec = FlutterEverything.codec
+ val message = arg<ByteBuffer>(1)
+ val reply = arg<BinaryMessenger.BinaryReply>(2)
+ message.position(0)
+ val args = codec.decodeMessage(message) as ArrayList<*>
+ val replyData = codec.encodeMessage(args[0])
+ replyData?.position(0)
+ reply.reply(replyData)
+ }
+
+ var didCall = false
+ api.echo(everything) {
+ didCall = true
+ assertNull(it.aBool)
+ assertNull(it.anInt)
+ assertNull(it.aDouble)
+ assertNull(it.aString)
+ assertNull(it.aByteArray)
+ assertNull(it.a4ByteArray)
+ assertNull(it.a8ByteArray)
+ assertNull(it.aFloatArray)
+ assertNull(it.aList)
+ assertNull(it.aMap)
+ assertNull(it.mapWithObject)
+ }
+
+ assertTrue(didCall)
+ }
+
+ @Test
+ fun testHasValues() {
+ val everything = Everything(
+ aBool = false,
+ anInt = 1234L,
+ aDouble = 2.0,
+ aString = "hello",
+ aByteArray = byteArrayOf(1, 2, 3, 4),
+ a4ByteArray = intArrayOf(1, 2, 3, 4),
+ a8ByteArray = longArrayOf(1, 2, 3, 4),
+ aFloatArray = doubleArrayOf(0.5, 0.25, 1.5, 1.25),
+ aList = listOf(1, 2, 3),
+ aMap = mapOf("hello" to 1234),
+ mapWithObject = mapOf("hello" to 1234)
+ )
+ val binaryMessenger = mockk<BinaryMessenger>()
+ val api = FlutterEverything(binaryMessenger)
+
+ every { binaryMessenger.send(any(), any(), any()) } answers {
+ val codec = FlutterEverything.codec
+ val message = arg<ByteBuffer>(1)
+ val reply = arg<BinaryMessenger.BinaryReply>(2)
+ message.position(0)
+ val args = codec.decodeMessage(message) as ArrayList<*>
+ val replyData = codec.encodeMessage(args[0])
+ replyData?.position(0)
+ reply.reply(replyData)
+ }
+
+ var didCall = false
+ api.echo(everything) {
+ didCall = true
+ assertEquals(everything.aBool, it.aBool)
+ assertEquals(everything.anInt, it.anInt)
+ assertEquals(everything.aDouble, it.aDouble)
+ assertEquals(everything.aString, it.aString)
+ assertTrue(everything.aByteArray.contentEquals(it.aByteArray))
+ assertTrue(everything.a4ByteArray.contentEquals(it.a4ByteArray))
+ assertTrue(everything.a8ByteArray.contentEquals(it.a8ByteArray))
+ assertTrue(everything.aFloatArray.contentEquals(it.aFloatArray))
+ assertEquals(everything.aList, it.aList)
+ assertEquals(everything.aMap, it.aMap)
+ assertEquals(everything.mapWithObject, it.mapWithObject)
+ }
+
+ assertTrue(didCall)
+ }
+
+ @Test
+ fun testIntegerToLong() {
+ val everything = Everything(anInt = 123L)
+ val map = everything.toMap()
+ assertTrue(map.containsKey("anInt"))
+
+ val map2 = hashMapOf("anInt" to 123)
+ val everything2 = Everything.fromMap(map2)
+
+ assertEquals(everything.anInt, everything2.anInt)
+ }
+}
diff --git a/packages/pigeon/platform_tests/android_kotlin_unit_tests/android/app/src/test/java/com/example/android_kotlin_unit_tests/AsyncHandlersTest.kt b/packages/pigeon/platform_tests/android_kotlin_unit_tests/android/app/src/test/java/com/example/android_kotlin_unit_tests/AsyncHandlersTest.kt
new file mode 100644
index 0000000..04937c0
--- /dev/null
+++ b/packages/pigeon/platform_tests/android_kotlin_unit_tests/android/app/src/test/java/com/example/android_kotlin_unit_tests/AsyncHandlersTest.kt
@@ -0,0 +1,114 @@
+// Copyright 2013 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+package com.example.android_kotlin_unit_tests
+
+import io.flutter.plugin.common.BinaryMessenger
+import io.mockk.every
+import io.mockk.mockk
+import io.mockk.slot
+import io.mockk.verify
+import junit.framework.TestCase
+import org.junit.Test
+import java.nio.ByteBuffer
+
+
+internal class AsyncHandlersTest: TestCase() {
+ @Test
+ fun testAsyncHost2Flutter() {
+ val binaryMessenger = mockk<BinaryMessenger>()
+ val api = Api2Flutter(binaryMessenger)
+
+ val input = Value(1)
+ val output = Value(2)
+
+ every { binaryMessenger.send(any(), any(), any()) } answers {
+ val codec = Api2Flutter.codec
+ val message = arg<ByteBuffer>(1)
+ val reply = arg<BinaryMessenger.BinaryReply>(2)
+ message.position(0)
+ val replyData = codec.encodeMessage(output)
+ replyData?.position(0)
+ reply.reply(replyData)
+ }
+
+ var didCall = false
+ api.calculate(input) {
+ didCall = true
+ assertEquals(it, output)
+ }
+
+ assertTrue(didCall)
+
+ verify { binaryMessenger.send("dev.flutter.pigeon.Api2Flutter.calculate", any(), any()) }
+ }
+
+ @Test
+ fun testAsyncFlutter2HostCalculate() {
+ val binaryMessenger = mockk<BinaryMessenger>()
+ val api = mockk<Api2Host>()
+
+ val handlerSlot = slot<BinaryMessenger.BinaryMessageHandler>()
+
+ val input = Value(1)
+ val output = Value(2)
+ val channelName = "dev.flutter.pigeon.Api2Host.calculate"
+
+ every { binaryMessenger.setMessageHandler("dev.flutter.pigeon.Api2Host.voidVoid", any()) } returns Unit
+ every { binaryMessenger.setMessageHandler(channelName, capture(handlerSlot)) } returns Unit
+ every { api.calculate(any(), any()) } answers {
+ val callback = arg<(Value) -> Unit>(1)
+ callback(output)
+ }
+
+ Api2Host.setUp(binaryMessenger, api)
+
+ val codec = Api2Host.codec
+ val message = codec.encodeMessage(listOf(input))
+ message?.rewind()
+ handlerSlot.captured.onMessage(message) {
+ it?.rewind()
+ @Suppress("UNCHECKED_CAST")
+ val wrapped = codec.decodeMessage(it) as HashMap<String, Any>?
+ assertNotNull(wrapped)
+ wrapped?.let {
+ assertEquals(output, wrapped["result"])
+ }
+ }
+
+ verify { binaryMessenger.setMessageHandler(channelName, handlerSlot.captured) }
+ verify { api.calculate(input, any()) }
+ }
+
+ @Test
+ fun asyncFlutter2HostVoidVoid() {
+ val binaryMessenger = mockk<BinaryMessenger>()
+ val api = mockk<Api2Host>()
+
+ val handlerSlot = slot<BinaryMessenger.BinaryMessageHandler>()
+
+ val channelName = "dev.flutter.pigeon.Api2Host.voidVoid"
+
+ every { binaryMessenger.setMessageHandler(channelName, capture(handlerSlot)) } returns Unit
+ every { binaryMessenger.setMessageHandler("dev.flutter.pigeon.Api2Host.calculate", any()) } returns Unit
+ every { api.voidVoid(any()) } answers {
+ val callback = arg<() -> Unit>(0)
+ callback()
+ }
+
+ Api2Host.setUp(binaryMessenger, api)
+
+ val codec = Api2Host.codec
+ val message = codec.encodeMessage(null)
+ handlerSlot.captured.onMessage(message) {
+ it?.rewind()
+ @Suppress("UNCHECKED_CAST")
+ val wrapped = codec.decodeMessage(it) as HashMap<String, Any>?
+ assertNull(wrapped)
+ }
+
+ verify { binaryMessenger.setMessageHandler(channelName, handlerSlot.captured) }
+ verify { api.voidVoid(any()) }
+ }
+}
diff --git a/packages/pigeon/platform_tests/android_kotlin_unit_tests/android/app/src/test/java/com/example/android_kotlin_unit_tests/EchoBinaryMessenger.kt b/packages/pigeon/platform_tests/android_kotlin_unit_tests/android/app/src/test/java/com/example/android_kotlin_unit_tests/EchoBinaryMessenger.kt
new file mode 100644
index 0000000..652cb13
--- /dev/null
+++ b/packages/pigeon/platform_tests/android_kotlin_unit_tests/android/app/src/test/java/com/example/android_kotlin_unit_tests/EchoBinaryMessenger.kt
@@ -0,0 +1,38 @@
+// Copyright 2013 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+package com.example.android_kotlin_unit_tests
+
+import io.flutter.plugin.common.BinaryMessenger
+import io.flutter.plugin.common.MessageCodec
+import io.flutter.plugin.common.StandardMessageCodec
+import java.nio.ByteBuffer
+import java.util.ArrayList
+
+
+internal class EchoBinaryMessenger(private val codec: MessageCodec<Any?>): BinaryMessenger {
+ override fun send(channel: String, message: ByteBuffer?) {
+ // Method not implemented because this messenger is just for echoing.
+ }
+
+ override fun send(
+ channel: String,
+ message: ByteBuffer?,
+ callback: BinaryMessenger.BinaryReply?
+ ) {
+ message?.rewind()
+ val args = codec.decodeMessage(message) as ArrayList<*>
+ val replyData = codec.encodeMessage(args[0])
+ replyData?.position(0)
+ callback?.reply(replyData)
+ }
+
+ override fun setMessageHandler(
+ channel: String,
+ handler: BinaryMessenger.BinaryMessageHandler?
+ ) {
+ // Method not implemented because this messenger is just for echoing.
+ }
+
+}
diff --git a/packages/pigeon/platform_tests/android_kotlin_unit_tests/android/app/src/test/java/com/example/android_kotlin_unit_tests/EnumTest.kt b/packages/pigeon/platform_tests/android_kotlin_unit_tests/android/app/src/test/java/com/example/android_kotlin_unit_tests/EnumTest.kt
new file mode 100644
index 0000000..1246e2f
--- /dev/null
+++ b/packages/pigeon/platform_tests/android_kotlin_unit_tests/android/app/src/test/java/com/example/android_kotlin_unit_tests/EnumTest.kt
@@ -0,0 +1,77 @@
+// Copyright 2013 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+package com.example.android_kotlin_unit_tests
+
+import io.flutter.plugin.common.BinaryMessenger
+import io.mockk.every
+import io.mockk.mockk
+import io.mockk.slot
+import io.mockk.verify
+import junit.framework.TestCase
+import org.junit.Test
+import java.nio.ByteBuffer
+import java.util.ArrayList
+
+internal class EnumTest: TestCase() {
+ @Test
+ fun testEchoHost() {
+ val binaryMessenger = mockk<BinaryMessenger>()
+ val api = mockk<EnumApi2Host>()
+
+ val channelName = "dev.flutter.pigeon.EnumApi2Host.echo"
+ val input = DataWithEnum(EnumState.SUCCESS)
+
+ val handlerSlot = slot<BinaryMessenger.BinaryMessageHandler>()
+
+ every { binaryMessenger.setMessageHandler(channelName, capture(handlerSlot)) } returns Unit
+ every { api.echo(any()) } returnsArgument 0
+
+ EnumApi2Host.setUp(binaryMessenger, api)
+
+ val codec = EnumApi2Host.codec
+ val message = codec.encodeMessage(listOf(input))
+ message?.rewind()
+ handlerSlot.captured.onMessage(message) {
+ it?.rewind()
+ @Suppress("UNCHECKED_CAST")
+ val wrapped = codec.decodeMessage(it) as HashMap<String, Any>?
+ assertNotNull(wrapped)
+ wrapped?.let {
+ assertTrue(wrapped.containsKey("result"))
+ assertEquals(input, wrapped["result"])
+ }
+ }
+
+ verify { binaryMessenger.setMessageHandler(channelName, handlerSlot.captured) }
+ verify { api.echo(input) }
+ }
+
+ @Test
+ fun testEchoFlutter() {
+ val binaryMessenger = mockk<BinaryMessenger>()
+ val api = EnumApi2Flutter(binaryMessenger)
+
+ val input = DataWithEnum(EnumState.SUCCESS)
+
+ every { binaryMessenger.send(any(), any(), any()) } answers {
+ val codec = EnumApi2Flutter.codec
+ val message = arg<ByteBuffer>(1)
+ val reply = arg<BinaryMessenger.BinaryReply>(2)
+ message.position(0)
+ val args = codec.decodeMessage(message) as ArrayList<*>
+ val replyData = codec.encodeMessage(args[0])
+ replyData?.position(0)
+ reply.reply(replyData)
+ }
+
+ var didCall = false
+ api.echo(input) {
+ didCall = true
+ assertEquals(input, it)
+ }
+
+ assertTrue(didCall)
+ }
+}
diff --git a/packages/pigeon/platform_tests/android_kotlin_unit_tests/android/app/src/test/java/com/example/android_kotlin_unit_tests/ListTest.kt b/packages/pigeon/platform_tests/android_kotlin_unit_tests/android/app/src/test/java/com/example/android_kotlin_unit_tests/ListTest.kt
new file mode 100644
index 0000000..c8d1669
--- /dev/null
+++ b/packages/pigeon/platform_tests/android_kotlin_unit_tests/android/app/src/test/java/com/example/android_kotlin_unit_tests/ListTest.kt
@@ -0,0 +1,43 @@
+// Copyright 2013 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+package com.example.android_kotlin_unit_tests
+
+import io.flutter.plugin.common.BinaryMessenger
+import io.mockk.every
+import io.mockk.mockk
+import junit.framework.TestCase
+import org.junit.Test
+import java.nio.ByteBuffer
+import java.util.ArrayList
+
+class ListTest: TestCase() {
+ @Test
+ fun testListInList() {
+ val binaryMessenger = mockk<BinaryMessenger>()
+ val api = EchoApi(binaryMessenger)
+
+ val inside = TestMessage(listOf(1, 2, 3))
+ val input = TestMessage(listOf(inside))
+
+ every { binaryMessenger.send(any(), any(), any()) } answers {
+ val codec = EchoApi.codec
+ val message = arg<ByteBuffer>(1)
+ val reply = arg<BinaryMessenger.BinaryReply>(2)
+ message.position(0)
+ val args = codec.decodeMessage(message) as ArrayList<*>
+ val replyData = codec.encodeMessage(args[0])
+ replyData?.position(0)
+ reply.reply(replyData)
+ }
+
+ var didCall = false
+ api.echo(input) {
+ didCall = true
+ assertEquals(input, it)
+ }
+
+ assertTrue(didCall)
+ }
+}
diff --git a/packages/pigeon/platform_tests/android_kotlin_unit_tests/android/app/src/test/java/com/example/android_kotlin_unit_tests/MultipleArityTests.kt b/packages/pigeon/platform_tests/android_kotlin_unit_tests/android/app/src/test/java/com/example/android_kotlin_unit_tests/MultipleArityTests.kt
new file mode 100644
index 0000000..0144ce1
--- /dev/null
+++ b/packages/pigeon/platform_tests/android_kotlin_unit_tests/android/app/src/test/java/com/example/android_kotlin_unit_tests/MultipleArityTests.kt
@@ -0,0 +1,76 @@
+// Copyright 2013 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+package com.example.android_kotlin_unit_tests
+
+import io.flutter.plugin.common.BinaryMessenger
+import io.mockk.every
+import io.mockk.mockk
+import io.mockk.slot
+import junit.framework.TestCase
+import org.junit.Test
+import java.nio.ByteBuffer
+import java.util.ArrayList
+
+class MultipleArityTests: TestCase() {
+ @Test
+ fun testSimpleHost() {
+ val binaryMessenger = mockk<BinaryMessenger>()
+ val api = mockk<MultipleArityHostApi>()
+
+ val inputX = 10L
+ val inputY = 5L
+
+ val channelName = "dev.flutter.pigeon.MultipleArityHostApi.subtract"
+ val handlerSlot = slot<BinaryMessenger.BinaryMessageHandler>()
+
+ every { binaryMessenger.setMessageHandler(channelName, capture(handlerSlot)) } returns Unit
+ every { api.subtract(any(), any()) } answers { firstArg<Long>() - secondArg<Long>() }
+
+ MultipleArityHostApi.setUp(binaryMessenger, api)
+
+ val codec = MultipleArityHostApi.codec
+ val message = codec.encodeMessage(listOf(inputX, inputY))
+ message?.rewind()
+ handlerSlot.captured.onMessage(message) {
+ it?.rewind()
+ @Suppress("UNCHECKED_CAST")
+ val wrapped = codec.decodeMessage(it) as HashMap<String, Any>?
+ assertNotNull(wrapped)
+ wrapped?.let {
+ assertEquals(inputX - inputY, wrapped["result"])
+ }
+ }
+ }
+
+ @Test
+ fun testSimpleFlutter() {
+ val binaryMessenger = mockk<BinaryMessenger>()
+ val api = MultipleArityFlutterApi(binaryMessenger)
+
+ val inputX = 10L
+ val inputY = 5L
+
+ every { binaryMessenger.send(any(), any(), any()) } answers {
+ val codec = MultipleArityFlutterApi.codec
+ val message = arg<ByteBuffer>(1)
+ val reply = arg<BinaryMessenger.BinaryReply>(2)
+ message.position(0)
+ val args = codec.decodeMessage(message) as ArrayList<*>
+ val argX = args[0] as Long
+ val argY = args[1] as Long
+ val replyData = codec.encodeMessage(argX - argY)
+ replyData?.position(0)
+ reply.reply(replyData)
+ }
+
+ var didCall = false
+ api.subtract(inputX, inputY) {
+ didCall = true
+ assertEquals(inputX - inputY, it)
+ }
+
+ assertTrue(didCall)
+ }
+}
diff --git a/packages/pigeon/platform_tests/android_kotlin_unit_tests/android/app/src/test/java/com/example/android_kotlin_unit_tests/NonNullFieldsTests.kt b/packages/pigeon/platform_tests/android_kotlin_unit_tests/android/app/src/test/java/com/example/android_kotlin_unit_tests/NonNullFieldsTests.kt
new file mode 100644
index 0000000..7f89152
--- /dev/null
+++ b/packages/pigeon/platform_tests/android_kotlin_unit_tests/android/app/src/test/java/com/example/android_kotlin_unit_tests/NonNullFieldsTests.kt
@@ -0,0 +1,16 @@
+// Copyright 2013 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+package com.example.android_kotlin_unit_tests
+
+import junit.framework.TestCase
+import org.junit.Test
+
+class NonNullFieldsTests: TestCase() {
+ @Test
+ fun testMake() {
+ val request = NonNullFieldSearchRequest("hello")
+ assertEquals("hello", request.query)
+ }
+}
diff --git a/packages/pigeon/platform_tests/android_kotlin_unit_tests/android/app/src/test/java/com/example/android_kotlin_unit_tests/NullableReturnsTest.kt b/packages/pigeon/platform_tests/android_kotlin_unit_tests/android/app/src/test/java/com/example/android_kotlin_unit_tests/NullableReturnsTest.kt
new file mode 100644
index 0000000..2b8fd2e
--- /dev/null
+++ b/packages/pigeon/platform_tests/android_kotlin_unit_tests/android/app/src/test/java/com/example/android_kotlin_unit_tests/NullableReturnsTest.kt
@@ -0,0 +1,71 @@
+// Copyright 2013 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+package com.example.android_kotlin_unit_tests
+
+import io.flutter.plugin.common.BinaryMessenger
+import io.mockk.every
+import io.mockk.mockk
+import io.mockk.slot
+import io.mockk.verify
+import junit.framework.TestCase
+import org.junit.Test
+
+class NullableReturnsTest: TestCase() {
+ @Test
+ fun testNullableParameterHost() {
+ val binaryMessenger = mockk<BinaryMessenger>(relaxed = true)
+ val api = mockk<NullableReturnHostApi>(relaxed = true)
+
+ val output = 1L
+
+ val channelName = "dev.flutter.pigeon.NullableReturnHostApi.doit"
+ val handlerSlot = slot<BinaryMessenger.BinaryMessageHandler>()
+
+ every { binaryMessenger.setMessageHandler(channelName, capture(handlerSlot)) } returns Unit
+ every { api.doit() } returns output
+
+ NullableReturnHostApi.setUp(binaryMessenger, api)
+
+ val codec = PrimitiveHostApi.codec
+ val message = codec.encodeMessage(null)
+ message?.rewind()
+ handlerSlot.captured.onMessage(message) {
+ it?.rewind()
+ @Suppress("UNCHECKED_CAST")
+ val wrapped = codec.decodeMessage(it) as HashMap<String, Any>?
+ assertNotNull(wrapped)
+ wrapped?.let {
+ assertEquals(output, wrapped["result"])
+ }
+ }
+
+ verify { binaryMessenger.setMessageHandler(channelName, handlerSlot.captured) }
+ verify { api.doit() }
+ }
+
+ @Test
+ fun testNullableParameterFlutter() {
+ val binaryMessenger = mockk<BinaryMessenger>()
+ val api = NullableReturnFlutterApi(binaryMessenger)
+
+ val output = 12L
+
+ every { binaryMessenger.send(any(), any(), any()) } answers {
+ val codec = NullableReturnFlutterApi.codec
+ val reply = arg<BinaryMessenger.BinaryReply>(2)
+ val replyData = codec.encodeMessage(output)
+ replyData?.position(0)
+ reply.reply(replyData)
+ }
+
+ var didCall = false
+ api.doit {
+ didCall = true
+ assertEquals(output, it)
+ }
+
+ assertTrue(didCall)
+ }
+}
diff --git a/packages/pigeon/platform_tests/android_kotlin_unit_tests/android/app/src/test/java/com/example/android_kotlin_unit_tests/PrimitiveTest.kt b/packages/pigeon/platform_tests/android_kotlin_unit_tests/android/app/src/test/java/com/example/android_kotlin_unit_tests/PrimitiveTest.kt
new file mode 100644
index 0000000..e4a86b4
--- /dev/null
+++ b/packages/pigeon/platform_tests/android_kotlin_unit_tests/android/app/src/test/java/com/example/android_kotlin_unit_tests/PrimitiveTest.kt
@@ -0,0 +1,434 @@
+// Copyright 2013 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+package com.example.android_kotlin_unit_tests
+
+import io.flutter.plugin.common.BinaryMessenger
+import io.mockk.every
+import io.mockk.mockk
+import io.mockk.slot
+import io.mockk.verify
+import junit.framework.TestCase
+import org.junit.Test
+import java.nio.ByteBuffer
+import java.util.ArrayList
+
+class PrimitiveTest: TestCase() {
+ @Test
+ fun testIntPrimitiveHost() {
+ val binaryMessenger = mockk<BinaryMessenger>(relaxed = true)
+ val api = mockk<PrimitiveHostApi>(relaxed = true)
+
+ val input = 1
+
+ val channelName = "dev.flutter.pigeon.PrimitiveHostApi.anInt"
+ val handlerSlot = slot<BinaryMessenger.BinaryMessageHandler>()
+
+ every { binaryMessenger.setMessageHandler(channelName, capture(handlerSlot)) } returns Unit
+ every { api.anInt(any()) } returnsArgument 0
+
+ PrimitiveHostApi.setUp(binaryMessenger, api)
+
+ val codec = PrimitiveHostApi.codec
+ val message = codec.encodeMessage(listOf(input))
+ message?.rewind()
+ handlerSlot.captured.onMessage(message) {
+ it?.rewind()
+ @Suppress("UNCHECKED_CAST")
+ val wrapped = codec.decodeMessage(it) as HashMap<String, Any>?
+ assertNotNull(wrapped)
+ wrapped?.let {
+ assertEquals(input.toLong(), wrapped["result"])
+ }
+ }
+
+ verify { binaryMessenger.setMessageHandler(channelName, handlerSlot.captured) }
+ verify { api.anInt(input.toLong()) }
+ }
+
+ @Test
+ fun testIntPrimitiveFlutter() {
+ val binaryMessenger = EchoBinaryMessenger(MultipleArityFlutterApi.codec)
+ val api = PrimitiveFlutterApi(binaryMessenger)
+
+ val input = 1L
+
+ var didCall = false
+ api.anInt(input) {
+ didCall = true
+ assertEquals(input, it)
+ }
+
+ assertTrue(didCall)
+ }
+
+ @Test
+ fun testBoolPrimitiveHost() {
+ val binaryMessenger = mockk<BinaryMessenger>(relaxed = true)
+ val api = mockk<PrimitiveHostApi>(relaxed = true)
+
+ val input = true
+
+ val channelName = "dev.flutter.pigeon.PrimitiveHostApi.aBool"
+ val handlerSlot = slot<BinaryMessenger.BinaryMessageHandler>()
+
+ every { binaryMessenger.setMessageHandler(channelName, capture(handlerSlot)) } returns Unit
+ every { api.aBool(any()) } returnsArgument 0
+
+ PrimitiveHostApi.setUp(binaryMessenger, api)
+
+ val codec = PrimitiveHostApi.codec
+ val message = codec.encodeMessage(listOf(input))
+ message?.rewind()
+ handlerSlot.captured.onMessage(message) {
+ it?.rewind()
+ @Suppress("UNCHECKED_CAST")
+ val wrapped = codec.decodeMessage(it) as HashMap<String, Any>?
+ assertNotNull(wrapped)
+ wrapped?.let {
+ assertEquals(input, wrapped["result"])
+ }
+ }
+
+ verify { binaryMessenger.setMessageHandler(channelName, handlerSlot.captured) }
+ verify { api.aBool(input) }
+ }
+
+ @Test
+ fun testBoolPrimitiveFlutter() {
+ val binaryMessenger = EchoBinaryMessenger(MultipleArityFlutterApi.codec)
+ val api = PrimitiveFlutterApi(binaryMessenger)
+
+ val input = true
+
+ var didCall = false
+ api.aBool(input) {
+ didCall = true
+ assertEquals(input, it)
+ }
+
+ assertTrue(didCall)
+ }
+
+ @Test
+ fun testStringPrimitiveHost() {
+ val binaryMessenger = mockk<BinaryMessenger>(relaxed = true)
+ val api = mockk<PrimitiveHostApi>(relaxed = true)
+
+ val input = "Hello"
+
+ val channelName = "dev.flutter.pigeon.PrimitiveHostApi.aString"
+ val handlerSlot = slot<BinaryMessenger.BinaryMessageHandler>()
+
+ every { binaryMessenger.setMessageHandler(channelName, capture(handlerSlot)) } returns Unit
+ every { api.aString(any()) } returnsArgument 0
+
+ PrimitiveHostApi.setUp(binaryMessenger, api)
+
+ val codec = PrimitiveHostApi.codec
+ val message = codec.encodeMessage(listOf(input))
+ message?.rewind()
+ handlerSlot.captured.onMessage(message) {
+ it?.rewind()
+ @Suppress("UNCHECKED_CAST")
+ val wrapped = codec.decodeMessage(it) as HashMap<String, Any>?
+ assertNotNull(wrapped)
+ wrapped?.let {
+ assertEquals(input, wrapped["result"])
+ }
+ }
+
+ verify { binaryMessenger.setMessageHandler(channelName, handlerSlot.captured) }
+ verify { api.aString(input) }
+ }
+
+ @Test
+ fun testDoublePrimitiveHost() {
+ val binaryMessenger = mockk<BinaryMessenger>(relaxed = true)
+ val api = mockk<PrimitiveHostApi>(relaxed = true)
+
+ val input = 1.0
+
+ val channelName = "dev.flutter.pigeon.PrimitiveHostApi.aDouble"
+ val handlerSlot = slot<BinaryMessenger.BinaryMessageHandler>()
+
+ every { binaryMessenger.setMessageHandler(channelName, capture(handlerSlot)) } returns Unit
+ every { api.aDouble(any()) } returnsArgument 0
+
+ PrimitiveHostApi.setUp(binaryMessenger, api)
+
+ val codec = PrimitiveHostApi.codec
+ val message = codec.encodeMessage(listOf(input))
+ message?.rewind()
+ handlerSlot.captured.onMessage(message) {
+ it?.rewind()
+ @Suppress("UNCHECKED_CAST")
+ val wrapped = codec.decodeMessage(it) as HashMap<String, Any>?
+ assertNotNull(wrapped)
+ wrapped?.let {
+ assertEquals(input, wrapped["result"])
+ }
+ }
+
+ verify { binaryMessenger.setMessageHandler(channelName, handlerSlot.captured) }
+ verify { api.aDouble(input) }
+ }
+
+ @Test
+ fun testDoublePrimitiveFlutter() {
+ val binaryMessenger = EchoBinaryMessenger(MultipleArityFlutterApi.codec)
+ val api = PrimitiveFlutterApi(binaryMessenger)
+
+ val input = 1.0
+
+ var didCall = false
+ api.aDouble(input) {
+ didCall = true
+ assertEquals(input, it)
+ }
+
+ assertTrue(didCall)
+ }
+
+ @Test
+ fun testMapPrimitiveHost() {
+ val binaryMessenger = mockk<BinaryMessenger>(relaxed = true)
+ val api = mockk<PrimitiveHostApi>(relaxed = true)
+
+ val input = mapOf<Any, Any?>("a" to 1, "b" to 2)
+
+ val channelName = "dev.flutter.pigeon.PrimitiveHostApi.aMap"
+ val handlerSlot = slot<BinaryMessenger.BinaryMessageHandler>()
+
+ every { binaryMessenger.setMessageHandler(channelName, capture(handlerSlot)) } returns Unit
+ every { api.aMap(any()) } returnsArgument 0
+
+ PrimitiveHostApi.setUp(binaryMessenger, api)
+
+ val codec = PrimitiveHostApi.codec
+ val message = codec.encodeMessage(listOf(input))
+ message?.rewind()
+ handlerSlot.captured.onMessage(message) {
+ it?.rewind()
+ @Suppress("UNCHECKED_CAST")
+ val wrapped = codec.decodeMessage(it) as HashMap<String, Any>?
+ assertNotNull(wrapped)
+ wrapped?.let {
+ assertEquals(input, wrapped["result"])
+ }
+ }
+
+ verify { binaryMessenger.setMessageHandler(channelName, handlerSlot.captured) }
+ verify { api.aMap(input) }
+ }
+
+ @Test
+ fun testMapPrimitiveFlutter() {
+ val binaryMessenger = EchoBinaryMessenger(MultipleArityFlutterApi.codec)
+ val api = PrimitiveFlutterApi(binaryMessenger)
+
+ val input = mapOf<Any, Any?>("a" to 1, "b" to 2)
+
+ var didCall = false
+ api.aMap(input) {
+ didCall = true
+ assertEquals(input, it)
+ }
+
+ assertTrue(didCall)
+ }
+
+ @Test
+ fun testListPrimitiveHost() {
+ val binaryMessenger = mockk<BinaryMessenger>(relaxed = true)
+ val api = mockk<PrimitiveHostApi>(relaxed = true)
+
+ val input = listOf(1, 2, 3)
+
+ val channelName = "dev.flutter.pigeon.PrimitiveHostApi.aList"
+ val handlerSlot = slot<BinaryMessenger.BinaryMessageHandler>()
+
+ every { binaryMessenger.setMessageHandler(channelName, capture(handlerSlot)) } returns Unit
+ every { api.aList(any()) } returnsArgument 0
+
+ PrimitiveHostApi.setUp(binaryMessenger, api)
+
+ val codec = PrimitiveHostApi.codec
+ val message = codec.encodeMessage(listOf(input))
+ message?.rewind()
+ handlerSlot.captured.onMessage(message) {
+ it?.rewind()
+ @Suppress("UNCHECKED_CAST")
+ val wrapped = codec.decodeMessage(it) as HashMap<String, Any>?
+ assertNotNull(wrapped)
+ wrapped?.let {
+ assertEquals(input, wrapped["result"])
+ }
+ }
+
+ verify { binaryMessenger.setMessageHandler(channelName, handlerSlot.captured) }
+ verify { api.aList(input) }
+ }
+
+ @Test
+ fun testListPrimitiveFlutter() {
+ val binaryMessenger = EchoBinaryMessenger(MultipleArityFlutterApi.codec)
+ val api = PrimitiveFlutterApi(binaryMessenger)
+
+ val input = listOf(1, 2, 3)
+
+ var didCall = false
+ api.aList(input) {
+ didCall = true
+ assertEquals(input, it)
+ }
+
+ assertTrue(didCall)
+ }
+
+ @Test
+ fun testInt32ListPrimitiveHost() {
+ val binaryMessenger = mockk<BinaryMessenger>(relaxed = true)
+ val api = mockk<PrimitiveHostApi>(relaxed = true)
+
+ val input = intArrayOf(1, 2, 3)
+
+ val channelName = "dev.flutter.pigeon.PrimitiveHostApi.anInt32List"
+ val handlerSlot = slot<BinaryMessenger.BinaryMessageHandler>()
+
+ every { binaryMessenger.setMessageHandler(channelName, capture(handlerSlot)) } returns Unit
+ every { api.anInt32List(any()) } returnsArgument 0
+
+ PrimitiveHostApi.setUp(binaryMessenger, api)
+
+ val codec = PrimitiveHostApi.codec
+ val message = codec.encodeMessage(listOf(input))
+ message?.rewind()
+ handlerSlot.captured.onMessage(message) {
+ it?.rewind()
+ @Suppress("UNCHECKED_CAST")
+ val wrapped = codec.decodeMessage(it) as HashMap<String, Any>?
+ assertNotNull(wrapped)
+ wrapped?.let {
+ assertTrue(input.contentEquals(wrapped["result"] as IntArray))
+ }
+ }
+
+ verify { binaryMessenger.setMessageHandler(channelName, handlerSlot.captured) }
+ verify { api.anInt32List(input) }
+ }
+
+ @Test
+ fun testInt32ListPrimitiveFlutter() {
+ val binaryMessenger = EchoBinaryMessenger(MultipleArityFlutterApi.codec)
+ val api = PrimitiveFlutterApi(binaryMessenger)
+
+ val input = intArrayOf(1, 2, 3)
+
+ var didCall = false
+ api.anInt32List(input) {
+ didCall = true
+ assertTrue(input.contentEquals(it))
+ }
+
+ assertTrue(didCall)
+ }
+
+ @Test
+ fun testBoolListPrimitiveHost() {
+ val binaryMessenger = mockk<BinaryMessenger>(relaxed = true)
+ val api = mockk<PrimitiveHostApi>(relaxed = true)
+
+ val input = listOf(true, false, true)
+
+ val channelName = "dev.flutter.pigeon.PrimitiveHostApi.aBoolList"
+ val handlerSlot = slot<BinaryMessenger.BinaryMessageHandler>()
+
+ every { binaryMessenger.setMessageHandler(channelName, capture(handlerSlot)) } returns Unit
+ every { api.aBoolList(any()) } returnsArgument 0
+
+ PrimitiveHostApi.setUp(binaryMessenger, api)
+
+ val codec = PrimitiveHostApi.codec
+ val message = codec.encodeMessage(listOf(input))
+ message?.rewind()
+ handlerSlot.captured.onMessage(message) {
+ it?.rewind()
+ @Suppress("UNCHECKED_CAST")
+ val wrapped = codec.decodeMessage(it) as HashMap<String, Any>?
+ assertNotNull(wrapped)
+ wrapped?.let {
+ assertEquals(input, wrapped["result"])
+ }
+ }
+
+ verify { binaryMessenger.setMessageHandler(channelName, handlerSlot.captured) }
+ verify { api.aBoolList(input) }
+ }
+
+ @Test
+ fun testBoolListPrimitiveFlutter() {
+ val binaryMessenger = EchoBinaryMessenger(MultipleArityFlutterApi.codec)
+ val api = PrimitiveFlutterApi(binaryMessenger)
+
+ val input = listOf(true, false, true)
+
+ var didCall = false
+ api.aBoolList(input) {
+ didCall = true
+ assertEquals(input, it)
+ }
+
+ assertTrue(didCall)
+ }
+
+ @Test
+ fun testStringIntMapPrimitiveHost() {
+ val binaryMessenger = mockk<BinaryMessenger>(relaxed = true)
+ val api = mockk<PrimitiveHostApi>(relaxed = true)
+
+ val input = mapOf<String?, Long?>("a" to 1, "b" to 2)
+
+ val channelName = "dev.flutter.pigeon.PrimitiveHostApi.aStringIntMap"
+ val handlerSlot = slot<BinaryMessenger.BinaryMessageHandler>()
+
+ every { binaryMessenger.setMessageHandler(channelName, capture(handlerSlot)) } returns Unit
+ every { api.aStringIntMap(any()) } returnsArgument 0
+
+ PrimitiveHostApi.setUp(binaryMessenger, api)
+
+ val codec = PrimitiveHostApi.codec
+ val message = codec.encodeMessage(listOf(input))
+ message?.rewind()
+ handlerSlot.captured.onMessage(message) {
+ it?.rewind()
+ @Suppress("UNCHECKED_CAST")
+ val wrapped = codec.decodeMessage(it) as HashMap<String, Any>?
+ assertNotNull(wrapped)
+ wrapped?.let {
+ assertEquals(input, wrapped["result"])
+ }
+ }
+
+ verify { binaryMessenger.setMessageHandler(channelName, handlerSlot.captured) }
+ verify { api.aStringIntMap(input) }
+ }
+
+ @Test
+ fun testStringIntMapPrimitiveFlutter() {
+ val binaryMessenger = EchoBinaryMessenger(MultipleArityFlutterApi.codec)
+ val api = PrimitiveFlutterApi(binaryMessenger)
+
+ val input = mapOf<String?, Long?>("a" to 1, "b" to 2)
+
+ var didCall = false
+ api.aStringIntMap(input) {
+ didCall = true
+ assertEquals(input, it)
+ }
+
+ assertTrue(didCall)
+ }
+
+}
diff --git a/packages/pigeon/platform_tests/android_kotlin_unit_tests/android/build.gradle b/packages/pigeon/platform_tests/android_kotlin_unit_tests/android/build.gradle
new file mode 100644
index 0000000..410e2c7
--- /dev/null
+++ b/packages/pigeon/platform_tests/android_kotlin_unit_tests/android/build.gradle
@@ -0,0 +1,34 @@
+buildscript {
+ ext.kotlin_version = '1.6.0'
+ repositories {
+ google()
+ mavenCentral()
+ }
+
+ dependencies {
+ classpath 'com.android.tools.build:gradle:4.1.0'
+ classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
+ }
+}
+
+allprojects {
+ repositories {
+ google()
+ mavenCentral()
+ }
+ gradle.projectsEvaluated {
+ tasks.withType(JavaCompile) {
+ options.compilerArgs << "-Xlint:unchecked" << "-Xlint:deprecation"
+ }
+ }
+}
+
+rootProject.buildDir = '../build'
+subprojects {
+ project.buildDir = "${rootProject.buildDir}/${project.name}"
+ project.evaluationDependsOn(':app')
+}
+
+task clean(type: Delete) {
+ delete rootProject.buildDir
+}
diff --git a/packages/pigeon/platform_tests/android_kotlin_unit_tests/android/gradle.properties b/packages/pigeon/platform_tests/android_kotlin_unit_tests/android/gradle.properties
new file mode 100644
index 0000000..94adc3a
--- /dev/null
+++ b/packages/pigeon/platform_tests/android_kotlin_unit_tests/android/gradle.properties
@@ -0,0 +1,3 @@
+org.gradle.jvmargs=-Xmx1536M
+android.useAndroidX=true
+android.enableJetifier=true
diff --git a/packages/pigeon/platform_tests/android_kotlin_unit_tests/android/gradle/wrapper/gradle-wrapper.properties b/packages/pigeon/platform_tests/android_kotlin_unit_tests/android/gradle/wrapper/gradle-wrapper.properties
new file mode 100644
index 0000000..bc6a58a
--- /dev/null
+++ b/packages/pigeon/platform_tests/android_kotlin_unit_tests/android/gradle/wrapper/gradle-wrapper.properties
@@ -0,0 +1,6 @@
+#Fri Jun 23 08:50:38 CEST 2017
+distributionBase=GRADLE_USER_HOME
+distributionPath=wrapper/dists
+zipStoreBase=GRADLE_USER_HOME
+zipStorePath=wrapper/dists
+distributionUrl=https\://services.gradle.org/distributions/gradle-6.7-all.zip
diff --git a/packages/pigeon/platform_tests/android_kotlin_unit_tests/android/settings.gradle b/packages/pigeon/platform_tests/android_kotlin_unit_tests/android/settings.gradle
new file mode 100644
index 0000000..44e62bc
--- /dev/null
+++ b/packages/pigeon/platform_tests/android_kotlin_unit_tests/android/settings.gradle
@@ -0,0 +1,11 @@
+include ':app'
+
+def localPropertiesFile = new File(rootProject.projectDir, "local.properties")
+def properties = new Properties()
+
+assert localPropertiesFile.exists()
+localPropertiesFile.withReader("UTF-8") { reader -> properties.load(reader) }
+
+def flutterSdkPath = properties.getProperty("flutter.sdk")
+assert flutterSdkPath != null, "flutter.sdk not set in local.properties"
+apply from: "$flutterSdkPath/packages/flutter_tools/gradle/app_plugin_loader.gradle"
diff --git a/packages/pigeon/platform_tests/android_kotlin_unit_tests/lib/main.dart b/packages/pigeon/platform_tests/android_kotlin_unit_tests/lib/main.dart
new file mode 100644
index 0000000..f9b0dd7
--- /dev/null
+++ b/packages/pigeon/platform_tests/android_kotlin_unit_tests/lib/main.dart
@@ -0,0 +1,5 @@
+// Copyright 2013 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+void main() {}
diff --git a/packages/pigeon/platform_tests/android_kotlin_unit_tests/pubspec.yaml b/packages/pigeon/platform_tests/android_kotlin_unit_tests/pubspec.yaml
new file mode 100644
index 0000000..1b00c7b
--- /dev/null
+++ b/packages/pigeon/platform_tests/android_kotlin_unit_tests/pubspec.yaml
@@ -0,0 +1,20 @@
+name: android_kotlin_unit_tests
+description: Unit tests for Pigeon generated Java code.
+
+publish_to: "none" # Remove this line if you wish to publish to pub.dev
+version: 1.0.0+1
+
+environment:
+ sdk: ">=2.12.0 <3.0.0"
+
+dependencies:
+ cupertino_icons: ^1.0.2
+ flutter:
+ sdk: flutter
+
+dev_dependencies:
+ flutter_test:
+ sdk: flutter
+
+flutter:
+ uses-material-design: true
diff --git a/packages/pigeon/pubspec.yaml b/packages/pigeon/pubspec.yaml
index 3dcef2b..bc7d9d5 100644
--- a/packages/pigeon/pubspec.yaml
+++ b/packages/pigeon/pubspec.yaml
@@ -2,7 +2,7 @@
description: Code generator tool to make communication between Flutter and the host platform type-safe and easier.
repository: https://github.com/flutter/packages/tree/main/packages/pigeon
issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3Apigeon
-version: 4.1.1 # This must match the version in lib/generator_tools.dart
+version: 4.2.0 # This must match the version in lib/generator_tools.dart
environment:
sdk: ">=2.12.0 <3.0.0"
diff --git a/packages/pigeon/run_tests.sh b/packages/pigeon/run_tests.sh
index 1df123b..8428943 100755
--- a/packages/pigeon/run_tests.sh
+++ b/packages/pigeon/run_tests.sh
@@ -195,6 +195,10 @@
dart run tool/run_tests.dart -t mac_swift_unittests
}
+run_android_kotlin_unittests() {
+ dart run tool/run_tests.dart -t android_kotlin_unittests
+}
+
run_dart_compilation_tests() {
local temp_dir=$(mktmpdir)
local flutter_project_dir=$temp_dir/project
@@ -316,6 +320,7 @@
should_run_ios_unittests=true
should_run_mock_handler_tests=true
should_run_macos_swift_unittests=true
+should_run_android_kotlin_unittests=true
while getopts "t:l?h" opt; do
case $opt in
t)
@@ -327,6 +332,7 @@
should_run_ios_unittests=false
should_run_mock_handler_tests=false
should_run_macos_swift_unittests=false
+ should_run_android_kotlin_unittests=false
case $OPTARG in
android_unittests) should_run_android_unittests=true ;;
dart_compilation_tests) should_run_dart_compilation_tests=true ;;
@@ -336,6 +342,7 @@
ios_unittests) should_run_ios_unittests=true ;;
mock_handler_tests) should_run_mock_handler_tests=true ;;
macos_swift_unittests) should_run_macos_swift_unittests=true ;;
+ android_kotlin_unittests) should_run_android_kotlin_unittests=true ;;
*)
echo "unrecognized test: $OPTARG"
exit 1
@@ -344,14 +351,15 @@
;;
l)
echo "available tests for -t:
- android_unittests - Unit tests on generated Java code.
- dart_compilation_tests - Compilation tests on generated Dart code.
- dart_unittests - Unit tests on and analysis on Pigeon's implementation.
- flutter_unittests - Unit tests on generated Dart code.
- ios_e2e_tests - End-to-end objc tests run on iOS Simulator
- ios_unittests - Unit tests on generated Objc code.
- mock_handler_tests - Unit tests on generated Dart mock handler code.
- macos_swift_unittests - Unit tests on generated Swift code on macOS.
+ android_unittests - Unit tests on generated Java code.
+ android_kotlin_unittests - Unit tests on generated Kotlin code on Android.
+ dart_compilation_tests - Compilation tests on generated Dart code.
+ dart_unittests - Unit tests on and analysis on Pigeon's implementation.
+ flutter_unittests - Unit tests on generated Dart code.
+ ios_e2e_tests - End-to-end objc tests run on iOS Simulator
+ ios_unittests - Unit tests on generated Objc code.
+ mock_handler_tests - Unit tests on generated Dart mock handler code.
+ macos_swift_unittests - Unit tests on generated Swift code on macOS.
"
exit 1
;;
@@ -401,3 +409,6 @@
if [ "$should_run_macos_swift_unittests" = true ]; then
run_macos_swift_unittests
fi
+if [ "$should_run_android_kotlin_unittests" = true ]; then
+ run_android_kotlin_unittests
+fi
diff --git a/packages/pigeon/test/kotlin_generator_test.dart b/packages/pigeon/test/kotlin_generator_test.dart
new file mode 100644
index 0000000..d9d5501
--- /dev/null
+++ b/packages/pigeon/test/kotlin_generator_test.dart
@@ -0,0 +1,996 @@
+// Copyright 2013 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'package:pigeon/ast.dart';
+import 'package:pigeon/kotlin_generator.dart';
+import 'package:test/test.dart';
+
+void main() {
+ test('gen one class', () {
+ final Class klass = Class(
+ name: 'Foobar',
+ fields: <NamedType>[
+ NamedType(
+ type: const TypeDeclaration(
+ baseName: 'int',
+ isNullable: true,
+ ),
+ name: 'field1',
+ ),
+ ],
+ );
+ final Root root = Root(
+ apis: <Api>[],
+ classes: <Class>[klass],
+ enums: <Enum>[],
+ );
+ final StringBuffer sink = StringBuffer();
+ const KotlinOptions kotlinOptions = KotlinOptions();
+ generateKotlin(kotlinOptions, root, sink);
+ final String code = sink.toString();
+ expect(code, contains('data class Foobar('));
+ expect(code, contains('val field1: Long? = null'));
+ expect(code, contains('fun fromMap(map: Map<String, Any?>): Foobar'));
+ expect(code, contains('fun toMap(): Map<String, Any?>'));
+ });
+
+ test('gen one enum', () {
+ final Enum anEnum = Enum(
+ name: 'Foobar',
+ members: <String>[
+ 'one',
+ 'two',
+ ],
+ );
+ final Root root = Root(
+ apis: <Api>[],
+ classes: <Class>[],
+ enums: <Enum>[anEnum],
+ );
+ final StringBuffer sink = StringBuffer();
+ const KotlinOptions kotlinOptions = KotlinOptions();
+ generateKotlin(kotlinOptions, root, sink);
+ final String code = sink.toString();
+ expect(code, contains('enum class Foobar(val raw: Int) {'));
+ expect(code, contains('ONE(0)'));
+ expect(code, contains('TWO(1)'));
+ });
+
+ test('primitive enum host', () {
+ final Root root = Root(apis: <Api>[
+ Api(name: 'Bar', location: ApiLocation.host, methods: <Method>[
+ Method(
+ name: 'bar',
+ returnType: const TypeDeclaration.voidDeclaration(),
+ arguments: <NamedType>[
+ NamedType(
+ name: 'foo',
+ type:
+ const TypeDeclaration(baseName: 'Foo', isNullable: false))
+ ])
+ ])
+ ], classes: <Class>[], enums: <Enum>[
+ Enum(name: 'Foo', members: <String>['one', 'two'])
+ ]);
+ final StringBuffer sink = StringBuffer();
+ const KotlinOptions kotlinOptions = KotlinOptions();
+ generateKotlin(kotlinOptions, root, sink);
+ final String code = sink.toString();
+ expect(code, contains('enum class Foo(val raw: Int) {'));
+ expect(code, contains('val fooArg = Foo.ofRaw(args[0] as Int)'));
+ });
+
+ test('gen one host api', () {
+ final Root root = Root(apis: <Api>[
+ Api(name: 'Api', location: ApiLocation.host, methods: <Method>[
+ Method(
+ name: 'doSomething',
+ arguments: <NamedType>[
+ NamedType(
+ type: const TypeDeclaration(
+ baseName: 'Input',
+ isNullable: false,
+ ),
+ name: 'input',
+ )
+ ],
+ returnType:
+ const TypeDeclaration(baseName: 'Output', isNullable: false),
+ )
+ ])
+ ], classes: <Class>[
+ Class(name: 'Input', fields: <NamedType>[
+ NamedType(
+ type: const TypeDeclaration(
+ baseName: 'String',
+ isNullable: true,
+ ),
+ name: 'input',
+ )
+ ]),
+ Class(name: 'Output', fields: <NamedType>[
+ NamedType(
+ type: const TypeDeclaration(
+ baseName: 'String',
+ isNullable: true,
+ ),
+ name: 'output',
+ )
+ ])
+ ], enums: <Enum>[]);
+ final StringBuffer sink = StringBuffer();
+ const KotlinOptions kotlinOptions = KotlinOptions();
+ generateKotlin(kotlinOptions, root, sink);
+ final String code = sink.toString();
+ expect(code, contains('interface Api'));
+ expect(code, contains('fun doSomething(input: Input): Output'));
+ expect(code, contains('channel.setMessageHandler'));
+ });
+
+ test('all the simple datatypes header', () {
+ final Root root = Root(apis: <Api>[], classes: <Class>[
+ Class(name: 'Foobar', fields: <NamedType>[
+ NamedType(
+ type: const TypeDeclaration(
+ baseName: 'bool',
+ isNullable: true,
+ ),
+ name: 'aBool',
+ ),
+ NamedType(
+ type: const TypeDeclaration(
+ baseName: 'int',
+ isNullable: true,
+ ),
+ name: 'aInt',
+ ),
+ NamedType(
+ type: const TypeDeclaration(
+ baseName: 'double',
+ isNullable: true,
+ ),
+ name: 'aDouble',
+ ),
+ NamedType(
+ type: const TypeDeclaration(
+ baseName: 'String',
+ isNullable: true,
+ ),
+ name: 'aString',
+ ),
+ NamedType(
+ type: const TypeDeclaration(
+ baseName: 'Uint8List',
+ isNullable: true,
+ ),
+ name: 'aUint8List',
+ ),
+ NamedType(
+ type: const TypeDeclaration(
+ baseName: 'Int32List',
+ isNullable: true,
+ ),
+ name: 'aInt32List',
+ ),
+ NamedType(
+ type: const TypeDeclaration(
+ baseName: 'Int64List',
+ isNullable: true,
+ ),
+ name: 'aInt64List',
+ ),
+ NamedType(
+ type: const TypeDeclaration(
+ baseName: 'Float64List',
+ isNullable: true,
+ ),
+ name: 'aFloat64List',
+ ),
+ ]),
+ ], enums: <Enum>[]);
+
+ final StringBuffer sink = StringBuffer();
+
+ const KotlinOptions kotlinOptions = KotlinOptions();
+ generateKotlin(kotlinOptions, root, sink);
+ const KotlinOptions swiftOptions = KotlinOptions();
+ generateKotlin(swiftOptions, root, sink);
+ final String code = sink.toString();
+ expect(code, contains('val aBool: Boolean? = null'));
+ expect(code, contains('val aInt: Long? = null'));
+ expect(code, contains('val aDouble: Double? = null'));
+ expect(code, contains('val aString: String? = null'));
+ expect(code, contains('val aUint8List: ByteArray? = null'));
+ expect(code, contains('val aInt32List: IntArray? = null'));
+ expect(code, contains('val aInt64List: LongArray? = null'));
+ expect(code, contains('val aFloat64List: DoubleArray? = null'));
+ });
+
+ test('gen one flutter api', () {
+ final Root root = Root(apis: <Api>[
+ Api(name: 'Api', location: ApiLocation.flutter, methods: <Method>[
+ Method(
+ name: 'doSomething',
+ arguments: <NamedType>[
+ NamedType(
+ type: const TypeDeclaration(
+ baseName: 'Input',
+ isNullable: false,
+ ),
+ name: '',
+ )
+ ],
+ returnType:
+ const TypeDeclaration(baseName: 'Output', isNullable: false),
+ )
+ ])
+ ], classes: <Class>[
+ Class(name: 'Input', fields: <NamedType>[
+ NamedType(
+ type: const TypeDeclaration(
+ baseName: 'String',
+ isNullable: true,
+ ),
+ name: 'input',
+ )
+ ]),
+ Class(name: 'Output', fields: <NamedType>[
+ NamedType(
+ type: const TypeDeclaration(
+ baseName: 'String',
+ isNullable: true,
+ ),
+ name: 'output',
+ )
+ ])
+ ], enums: <Enum>[]);
+ final StringBuffer sink = StringBuffer();
+ const KotlinOptions swiftOptions = KotlinOptions();
+ generateKotlin(swiftOptions, root, sink);
+ final String code = sink.toString();
+ expect(code,
+ contains('class Api(private val binaryMessenger: BinaryMessenger)'));
+ expect(code, matches('fun doSomething.*Input.*Output'));
+ });
+
+ test('gen host void api', () {
+ final Root root = Root(apis: <Api>[
+ Api(name: 'Api', location: ApiLocation.host, methods: <Method>[
+ Method(
+ name: 'doSomething',
+ arguments: <NamedType>[
+ NamedType(
+ type: const TypeDeclaration(
+ baseName: 'Input',
+ isNullable: false,
+ ),
+ name: '',
+ )
+ ],
+ returnType: const TypeDeclaration.voidDeclaration(),
+ )
+ ])
+ ], classes: <Class>[
+ Class(name: 'Input', fields: <NamedType>[
+ NamedType(
+ type: const TypeDeclaration(
+ baseName: 'String',
+ isNullable: true,
+ ),
+ name: 'input',
+ )
+ ]),
+ ], enums: <Enum>[]);
+ final StringBuffer sink = StringBuffer();
+ const KotlinOptions swiftOptions = KotlinOptions();
+ generateKotlin(swiftOptions, root, sink);
+ final String code = sink.toString();
+ expect(code, isNot(matches('.*doSomething(.*) ->')));
+ expect(code, matches('doSomething(.*)'));
+ });
+
+ test('gen flutter void return api', () {
+ final Root root = Root(apis: <Api>[
+ Api(name: 'Api', location: ApiLocation.flutter, methods: <Method>[
+ Method(
+ name: 'doSomething',
+ arguments: <NamedType>[
+ NamedType(
+ type: const TypeDeclaration(
+ baseName: 'Input',
+ isNullable: false,
+ ),
+ name: '',
+ )
+ ],
+ returnType: const TypeDeclaration.voidDeclaration(),
+ )
+ ])
+ ], classes: <Class>[
+ Class(name: 'Input', fields: <NamedType>[
+ NamedType(
+ type: const TypeDeclaration(
+ baseName: 'String',
+ isNullable: true,
+ ),
+ name: 'input',
+ )
+ ]),
+ ], enums: <Enum>[]);
+ final StringBuffer sink = StringBuffer();
+ const KotlinOptions swiftOptions = KotlinOptions();
+ generateKotlin(swiftOptions, root, sink);
+ final String code = sink.toString();
+ expect(code, contains('callback: () -> Unit'));
+ expect(code, contains('callback()'));
+ });
+
+ test('gen host void argument api', () {
+ final Root root = Root(apis: <Api>[
+ Api(name: 'Api', location: ApiLocation.host, methods: <Method>[
+ Method(
+ name: 'doSomething',
+ arguments: <NamedType>[],
+ returnType:
+ const TypeDeclaration(baseName: 'Output', isNullable: false),
+ )
+ ])
+ ], classes: <Class>[
+ Class(name: 'Output', fields: <NamedType>[
+ NamedType(
+ type: const TypeDeclaration(
+ baseName: 'String',
+ isNullable: true,
+ ),
+ name: 'output',
+ )
+ ]),
+ ], enums: <Enum>[]);
+ final StringBuffer sink = StringBuffer();
+ const KotlinOptions swiftOptions = KotlinOptions();
+ generateKotlin(swiftOptions, root, sink);
+ final String code = sink.toString();
+ expect(code, contains('fun doSomething(): Output'));
+ expect(code, contains('wrapped["result"] = api.doSomething()'));
+ expect(code, contains('wrapped["error"] = wrapError(exception)'));
+ expect(code, contains('reply(wrapped)'));
+ });
+
+ test('gen flutter void argument api', () {
+ final Root root = Root(apis: <Api>[
+ Api(name: 'Api', location: ApiLocation.flutter, methods: <Method>[
+ Method(
+ name: 'doSomething',
+ arguments: <NamedType>[],
+ returnType:
+ const TypeDeclaration(baseName: 'Output', isNullable: false),
+ )
+ ])
+ ], classes: <Class>[
+ Class(name: 'Output', fields: <NamedType>[
+ NamedType(
+ type: const TypeDeclaration(
+ baseName: 'String',
+ isNullable: true,
+ ),
+ name: 'output',
+ )
+ ]),
+ ], enums: <Enum>[]);
+ final StringBuffer sink = StringBuffer();
+ const KotlinOptions swiftOptions = KotlinOptions();
+ generateKotlin(swiftOptions, root, sink);
+ final String code = sink.toString();
+ expect(code, contains('fun doSomething(callback: (Output) -> Unit)'));
+ expect(code, contains('channel.send(null)'));
+ });
+
+ test('gen list', () {
+ final Root root = Root(apis: <Api>[], classes: <Class>[
+ Class(name: 'Foobar', fields: <NamedType>[
+ NamedType(
+ type: const TypeDeclaration(
+ baseName: 'List',
+ isNullable: true,
+ ),
+ name: 'field1',
+ )
+ ]),
+ ], enums: <Enum>[]);
+ final StringBuffer sink = StringBuffer();
+ const KotlinOptions swiftOptions = KotlinOptions();
+ generateKotlin(swiftOptions, root, sink);
+ final String code = sink.toString();
+ expect(code, contains('data class Foobar'));
+ expect(code, contains('val field1: List<Any?>? = null'));
+ });
+
+ test('gen map', () {
+ final Root root = Root(apis: <Api>[], classes: <Class>[
+ Class(name: 'Foobar', fields: <NamedType>[
+ NamedType(
+ type: const TypeDeclaration(
+ baseName: 'Map',
+ isNullable: true,
+ ),
+ name: 'field1',
+ )
+ ]),
+ ], enums: <Enum>[]);
+ final StringBuffer sink = StringBuffer();
+ const KotlinOptions swiftOptions = KotlinOptions();
+ generateKotlin(swiftOptions, root, sink);
+ final String code = sink.toString();
+ expect(code, contains('data class Foobar'));
+ expect(code, contains('val field1: Map<Any, Any?>? = null'));
+ });
+
+ test('gen nested', () {
+ final Class klass = Class(
+ name: 'Outer',
+ fields: <NamedType>[
+ NamedType(
+ type: const TypeDeclaration(
+ baseName: 'Nested',
+ isNullable: true,
+ ),
+ name: 'nested',
+ )
+ ],
+ );
+ final Class nestedClass = Class(
+ name: 'Nested',
+ fields: <NamedType>[
+ NamedType(
+ type: const TypeDeclaration(
+ baseName: 'int',
+ isNullable: true,
+ ),
+ name: 'data',
+ )
+ ],
+ );
+ final Root root = Root(
+ apis: <Api>[],
+ classes: <Class>[klass, nestedClass],
+ enums: <Enum>[],
+ );
+ final StringBuffer sink = StringBuffer();
+ const KotlinOptions swiftOptions = KotlinOptions();
+ generateKotlin(swiftOptions, root, sink);
+ final String code = sink.toString();
+ expect(code, contains('data class Outer'));
+ expect(code, contains('data class Nested'));
+ expect(code, contains('val nested: Nested? = null'));
+ expect(code, contains('fun fromMap(map: Map<String, Any?>): Outer'));
+ expect(
+ code,
+ contains(
+ 'val nested: Nested? = (map["nested"] as? Map<String, Any?>)?.let'));
+ expect(code, contains('Nested.fromMap(it)'));
+ expect(code, contains('fun toMap(): Map<String, Any?>'));
+ });
+
+ test('gen one async Host Api', () {
+ final Root root = Root(apis: <Api>[
+ Api(name: 'Api', location: ApiLocation.host, methods: <Method>[
+ Method(
+ name: 'doSomething',
+ arguments: <NamedType>[
+ NamedType(
+ type: const TypeDeclaration(
+ baseName: 'Input',
+ isNullable: false,
+ ),
+ name: 'arg',
+ )
+ ],
+ returnType:
+ const TypeDeclaration(baseName: 'Output', isNullable: false),
+ isAsynchronous: true,
+ )
+ ])
+ ], classes: <Class>[
+ Class(name: 'Input', fields: <NamedType>[
+ NamedType(
+ type: const TypeDeclaration(
+ baseName: 'String',
+ isNullable: true,
+ ),
+ name: 'input',
+ )
+ ]),
+ Class(name: 'Output', fields: <NamedType>[
+ NamedType(
+ type: const TypeDeclaration(
+ baseName: 'String',
+ isNullable: true,
+ ),
+ name: 'output',
+ )
+ ])
+ ], enums: <Enum>[]);
+ final StringBuffer sink = StringBuffer();
+ const KotlinOptions swiftOptions = KotlinOptions();
+ generateKotlin(swiftOptions, root, sink);
+ final String code = sink.toString();
+ expect(code, contains('interface Api'));
+ expect(code, contains('api.doSomething(argArg) {'));
+ expect(code, contains('reply.reply(wrapResult(it))'));
+ });
+
+ test('gen one async Flutter Api', () {
+ final Root root = Root(apis: <Api>[
+ Api(name: 'Api', location: ApiLocation.flutter, methods: <Method>[
+ Method(
+ name: 'doSomething',
+ arguments: <NamedType>[
+ NamedType(
+ type: const TypeDeclaration(
+ baseName: 'Input',
+ isNullable: false,
+ ),
+ name: '',
+ )
+ ],
+ returnType:
+ const TypeDeclaration(baseName: 'Output', isNullable: false),
+ isAsynchronous: true,
+ )
+ ])
+ ], classes: <Class>[
+ Class(name: 'Input', fields: <NamedType>[
+ NamedType(
+ type: const TypeDeclaration(
+ baseName: 'String',
+ isNullable: true,
+ ),
+ name: 'input',
+ )
+ ]),
+ Class(name: 'Output', fields: <NamedType>[
+ NamedType(
+ type: const TypeDeclaration(
+ baseName: 'String',
+ isNullable: true,
+ ),
+ name: 'output',
+ )
+ ])
+ ], enums: <Enum>[]);
+ final StringBuffer sink = StringBuffer();
+ const KotlinOptions swiftOptions = KotlinOptions();
+ generateKotlin(swiftOptions, root, sink);
+ final String code = sink.toString();
+ expect(code, contains('class Api'));
+ expect(code, matches('fun doSomething.*Input.*callback.*Output.*Unit'));
+ });
+
+ test('gen one enum class', () {
+ final Enum anEnum = Enum(
+ name: 'Enum1',
+ members: <String>[
+ 'one',
+ 'two',
+ ],
+ );
+ final Class klass = Class(
+ name: 'EnumClass',
+ fields: <NamedType>[
+ NamedType(
+ type: const TypeDeclaration(
+ baseName: 'Enum1',
+ isNullable: true,
+ ),
+ name: 'enum1',
+ ),
+ ],
+ );
+ final Root root = Root(
+ apis: <Api>[],
+ classes: <Class>[klass],
+ enums: <Enum>[anEnum],
+ );
+ final StringBuffer sink = StringBuffer();
+ const KotlinOptions swiftOptions = KotlinOptions();
+ generateKotlin(swiftOptions, root, sink);
+ final String code = sink.toString();
+ expect(code, contains('enum class Enum1(val raw: Int)'));
+ expect(code, contains('ONE(0)'));
+ expect(code, contains('TWO(1)'));
+ });
+
+ Iterable<String> makeIterable(String string) sync* {
+ yield string;
+ }
+
+ test('header', () {
+ final Root root = Root(apis: <Api>[], classes: <Class>[], enums: <Enum>[]);
+ final StringBuffer sink = StringBuffer();
+ final KotlinOptions swiftOptions = KotlinOptions(
+ copyrightHeader: makeIterable('hello world'),
+ );
+ generateKotlin(swiftOptions, root, sink);
+ final String code = sink.toString();
+ expect(code, startsWith('// hello world'));
+ });
+
+ test('generics - list', () {
+ final Class klass = Class(
+ name: 'Foobar',
+ fields: <NamedType>[
+ NamedType(
+ type: const TypeDeclaration(
+ baseName: 'List',
+ isNullable: true,
+ typeArguments: <TypeDeclaration>[
+ TypeDeclaration(baseName: 'int', isNullable: true)
+ ]),
+ name: 'field1',
+ ),
+ ],
+ );
+ final Root root = Root(
+ apis: <Api>[],
+ classes: <Class>[klass],
+ enums: <Enum>[],
+ );
+ final StringBuffer sink = StringBuffer();
+ const KotlinOptions swiftOptions = KotlinOptions();
+ generateKotlin(swiftOptions, root, sink);
+ final String code = sink.toString();
+ expect(code, contains('data class Foobar'));
+ expect(code, contains('val field1: List<Long?>'));
+ });
+
+ test('generics - maps', () {
+ final Class klass = Class(
+ name: 'Foobar',
+ fields: <NamedType>[
+ NamedType(
+ type: const TypeDeclaration(
+ baseName: 'Map',
+ isNullable: true,
+ typeArguments: <TypeDeclaration>[
+ TypeDeclaration(baseName: 'String', isNullable: true),
+ TypeDeclaration(baseName: 'String', isNullable: true),
+ ]),
+ name: 'field1',
+ ),
+ ],
+ );
+ final Root root = Root(
+ apis: <Api>[],
+ classes: <Class>[klass],
+ enums: <Enum>[],
+ );
+ final StringBuffer sink = StringBuffer();
+ const KotlinOptions swiftOptions = KotlinOptions();
+ generateKotlin(swiftOptions, root, sink);
+ final String code = sink.toString();
+ expect(code, contains('data class Foobar'));
+ expect(code, contains('val field1: Map<String?, String?>'));
+ });
+
+ test('host generics argument', () {
+ final Root root = Root(
+ apis: <Api>[
+ Api(name: 'Api', location: ApiLocation.host, methods: <Method>[
+ Method(
+ name: 'doit',
+ returnType: const TypeDeclaration.voidDeclaration(),
+ arguments: <NamedType>[
+ NamedType(
+ type: const TypeDeclaration(
+ baseName: 'List',
+ isNullable: false,
+ typeArguments: <TypeDeclaration>[
+ TypeDeclaration(baseName: 'int', isNullable: true)
+ ]),
+ name: 'arg',
+ )
+ ])
+ ])
+ ],
+ classes: <Class>[],
+ enums: <Enum>[],
+ );
+ final StringBuffer sink = StringBuffer();
+ const KotlinOptions swiftOptions = KotlinOptions();
+ generateKotlin(swiftOptions, root, sink);
+ final String code = sink.toString();
+ expect(code, contains('fun doit(arg: List<Long?>'));
+ });
+
+ test('flutter generics argument', () {
+ final Root root = Root(
+ apis: <Api>[
+ Api(name: 'Api', location: ApiLocation.flutter, methods: <Method>[
+ Method(
+ name: 'doit',
+ returnType: const TypeDeclaration.voidDeclaration(),
+ arguments: <NamedType>[
+ NamedType(
+ type: const TypeDeclaration(
+ baseName: 'List',
+ isNullable: false,
+ typeArguments: <TypeDeclaration>[
+ TypeDeclaration(baseName: 'int', isNullable: true)
+ ]),
+ name: 'arg',
+ )
+ ])
+ ])
+ ],
+ classes: <Class>[],
+ enums: <Enum>[],
+ );
+ final StringBuffer sink = StringBuffer();
+ const KotlinOptions swiftOptions = KotlinOptions();
+ generateKotlin(swiftOptions, root, sink);
+ final String code = sink.toString();
+ expect(code, contains('fun doit(argArg: List<Long?>'));
+ });
+
+ test('host generics return', () {
+ final Root root = Root(
+ apis: <Api>[
+ Api(name: 'Api', location: ApiLocation.host, methods: <Method>[
+ Method(
+ name: 'doit',
+ returnType: const TypeDeclaration(
+ baseName: 'List',
+ isNullable: false,
+ typeArguments: <TypeDeclaration>[
+ TypeDeclaration(baseName: 'int', isNullable: true)
+ ]),
+ arguments: <NamedType>[])
+ ])
+ ],
+ classes: <Class>[],
+ enums: <Enum>[],
+ );
+ final StringBuffer sink = StringBuffer();
+ const KotlinOptions swiftOptions = KotlinOptions();
+ generateKotlin(swiftOptions, root, sink);
+ final String code = sink.toString();
+ expect(code, contains('fun doit(): List<Long?>'));
+ expect(code, contains('wrapped["result"] = api.doit()'));
+ expect(code, contains('reply.reply(wrapped)'));
+ });
+
+ test('flutter generics return', () {
+ final Root root = Root(
+ apis: <Api>[
+ Api(name: 'Api', location: ApiLocation.flutter, methods: <Method>[
+ Method(
+ name: 'doit',
+ returnType: const TypeDeclaration(
+ baseName: 'List',
+ isNullable: false,
+ typeArguments: <TypeDeclaration>[
+ TypeDeclaration(baseName: 'int', isNullable: true)
+ ]),
+ arguments: <NamedType>[])
+ ])
+ ],
+ classes: <Class>[],
+ enums: <Enum>[],
+ );
+ final StringBuffer sink = StringBuffer();
+ const KotlinOptions swiftOptions = KotlinOptions();
+ generateKotlin(swiftOptions, root, sink);
+ final String code = sink.toString();
+ expect(code, contains('fun doit(callback: (List<Long?>) -> Unit'));
+ expect(code, contains('val result = it as List<Long?>'));
+ expect(code, contains('callback(result)'));
+ });
+
+ test('host multiple args', () {
+ final Root root = Root(apis: <Api>[
+ Api(name: 'Api', location: ApiLocation.host, methods: <Method>[
+ Method(
+ name: 'add',
+ arguments: <NamedType>[
+ NamedType(
+ name: 'x',
+ type:
+ const TypeDeclaration(isNullable: false, baseName: 'int')),
+ NamedType(
+ name: 'y',
+ type:
+ const TypeDeclaration(isNullable: false, baseName: 'int')),
+ ],
+ returnType: const TypeDeclaration(baseName: 'int', isNullable: false),
+ )
+ ])
+ ], classes: <Class>[], enums: <Enum>[]);
+ final StringBuffer sink = StringBuffer();
+ const KotlinOptions swiftOptions = KotlinOptions();
+ generateKotlin(swiftOptions, root, sink);
+ final String code = sink.toString();
+ expect(code, contains('fun add(x: Long, y: Long): Long'));
+ expect(code, contains('val args = message as List<Any?>'));
+ expect(
+ code,
+ contains(
+ 'val xArg = args[0].let { if (it is Int) it.toLong() else it as Long }'));
+ expect(
+ code,
+ contains(
+ 'val yArg = args[1].let { if (it is Int) it.toLong() else it as Long }'));
+ expect(code, contains('wrapped["result"] = api.add(xArg, yArg)'));
+ expect(code, contains('reply.reply(wrapped)'));
+ });
+
+ test('flutter multiple args', () {
+ final Root root = Root(apis: <Api>[
+ Api(name: 'Api', location: ApiLocation.flutter, methods: <Method>[
+ Method(
+ name: 'add',
+ arguments: <NamedType>[
+ NamedType(
+ name: 'x',
+ type:
+ const TypeDeclaration(baseName: 'int', isNullable: false)),
+ NamedType(
+ name: 'y',
+ type:
+ const TypeDeclaration(baseName: 'int', isNullable: false)),
+ ],
+ returnType: const TypeDeclaration(baseName: 'int', isNullable: false),
+ )
+ ])
+ ], classes: <Class>[], enums: <Enum>[]);
+ final StringBuffer sink = StringBuffer();
+ const KotlinOptions swiftOptions = KotlinOptions();
+ generateKotlin(swiftOptions, root, sink);
+ final String code = sink.toString();
+ expect(code, contains('val channel = BasicMessageChannel'));
+ expect(code, contains('val result = it as Long'));
+ expect(code, contains('callback(result)'));
+ expect(code,
+ contains('fun add(xArg: Long, yArg: Long, callback: (Long) -> Unit)'));
+ expect(code, contains('channel.send(listOf(xArg, yArg)) {'));
+ });
+
+ test('return nullable host', () {
+ final Root root = Root(
+ apis: <Api>[
+ Api(name: 'Api', location: ApiLocation.host, methods: <Method>[
+ Method(
+ name: 'doit',
+ returnType: const TypeDeclaration(
+ baseName: 'int',
+ isNullable: true,
+ ),
+ arguments: <NamedType>[])
+ ])
+ ],
+ classes: <Class>[],
+ enums: <Enum>[],
+ );
+ final StringBuffer sink = StringBuffer();
+ const KotlinOptions swiftOptions = KotlinOptions();
+ generateKotlin(swiftOptions, root, sink);
+ final String code = sink.toString();
+ expect(code, contains('fun doit(): Long?'));
+ });
+
+ test('return nullable host async', () {
+ final Root root = Root(
+ apis: <Api>[
+ Api(name: 'Api', location: ApiLocation.host, methods: <Method>[
+ Method(
+ name: 'doit',
+ returnType: const TypeDeclaration(
+ baseName: 'int',
+ isNullable: true,
+ ),
+ isAsynchronous: true,
+ arguments: <NamedType>[])
+ ])
+ ],
+ classes: <Class>[],
+ enums: <Enum>[],
+ );
+ final StringBuffer sink = StringBuffer();
+ const KotlinOptions swiftOptions = KotlinOptions();
+ generateKotlin(swiftOptions, root, sink);
+ final String code = sink.toString();
+ expect(code, contains('fun doit(callback: (Long?) -> Unit'));
+ });
+
+ test('nullable argument host', () {
+ final Root root = Root(
+ apis: <Api>[
+ Api(name: 'Api', location: ApiLocation.host, methods: <Method>[
+ Method(
+ name: 'doit',
+ returnType: const TypeDeclaration.voidDeclaration(),
+ arguments: <NamedType>[
+ NamedType(
+ name: 'foo',
+ type: const TypeDeclaration(
+ baseName: 'int',
+ isNullable: true,
+ )),
+ ])
+ ])
+ ],
+ classes: <Class>[],
+ enums: <Enum>[],
+ );
+ final StringBuffer sink = StringBuffer();
+ const KotlinOptions swiftOptions = KotlinOptions();
+ generateKotlin(swiftOptions, root, sink);
+ final String code = sink.toString();
+ expect(
+ code,
+ contains(
+ 'val fooArg = args[0].let { if (it is Int) it.toLong() else it as? Long }'));
+ });
+
+ test('nullable argument flutter', () {
+ final Root root = Root(
+ apis: <Api>[
+ Api(name: 'Api', location: ApiLocation.flutter, methods: <Method>[
+ Method(
+ name: 'doit',
+ returnType: const TypeDeclaration.voidDeclaration(),
+ arguments: <NamedType>[
+ NamedType(
+ name: 'foo',
+ type: const TypeDeclaration(
+ baseName: 'int',
+ isNullable: true,
+ )),
+ ])
+ ])
+ ],
+ classes: <Class>[],
+ enums: <Enum>[],
+ );
+ final StringBuffer sink = StringBuffer();
+ const KotlinOptions swiftOptions = KotlinOptions();
+ generateKotlin(swiftOptions, root, sink);
+ final String code = sink.toString();
+ expect(code, contains('fun doit(fooArg: Long?, callback: () -> Unit'));
+ });
+
+ test('nonnull fields', () {
+ final Root root = Root(apis: <Api>[
+ Api(name: 'Api', location: ApiLocation.host, methods: <Method>[
+ Method(
+ name: 'doSomething',
+ arguments: <NamedType>[
+ NamedType(
+ type: const TypeDeclaration(
+ baseName: 'Input',
+ isNullable: false,
+ ),
+ name: '',
+ )
+ ],
+ returnType: const TypeDeclaration.voidDeclaration(),
+ )
+ ])
+ ], classes: <Class>[
+ Class(name: 'Input', fields: <NamedType>[
+ NamedType(
+ type: const TypeDeclaration(
+ baseName: 'String',
+ isNullable: false,
+ ),
+ name: 'input',
+ )
+ ]),
+ ], enums: <Enum>[]);
+ final StringBuffer sink = StringBuffer();
+ const KotlinOptions swiftOptions = KotlinOptions();
+ generateKotlin(swiftOptions, root, sink);
+ final String code = sink.toString();
+ expect(code, contains('val input: String\n'));
+ });
+}
diff --git a/packages/pigeon/test/pigeon_lib_test.dart b/packages/pigeon/test/pigeon_lib_test.dart
index 6b28183..120c6a2 100644
--- a/packages/pigeon/test/pigeon_lib_test.dart
+++ b/packages/pigeon/test/pigeon_lib_test.dart
@@ -98,6 +98,18 @@
expect(opts.swiftOut, equals('Foo.swift'));
});
+ test('parse args - kotlin_out', () {
+ final PigeonOptions opts =
+ Pigeon.parseArgs(<String>['--experimental_kotlin_out', 'Foo.kt']);
+ expect(opts.kotlinOut, equals('Foo.kt'));
+ });
+
+ test('parse args - kotlin_package', () {
+ final PigeonOptions opts = Pigeon.parseArgs(
+ <String>['--experimental_kotlin_package', 'com.google.foo']);
+ expect(opts.kotlinOptions?.package, equals('com.google.foo'));
+ });
+
test('parse args - experimental_cpp_header_out', () {
final PigeonOptions opts =
Pigeon.parseArgs(<String>['--experimental_cpp_header_out', 'foo.h']);
diff --git a/packages/pigeon/test/pigeon_test.dart b/packages/pigeon/test/pigeon_test.dart
index 7982681..c3e7aa7 100644
--- a/packages/pigeon/test/pigeon_test.dart
+++ b/packages/pigeon/test/pigeon_test.dart
@@ -20,4 +20,9 @@
const SwiftOptions swiftOptions = SwiftOptions();
expect(swiftOptions, isNotNull);
});
+
+ test('Should be able to import KotlinOptions', () async {
+ const KotlinOptions kotlinOptions = KotlinOptions();
+ expect(kotlinOptions, isNotNull);
+ });
}
diff --git a/packages/pigeon/tool/run_tests.dart b/packages/pigeon/tool/run_tests.dart
index 3eb5833..445744e 100644
--- a/packages/pigeon/tool/run_tests.dart
+++ b/packages/pigeon/tool/run_tests.dart
@@ -19,9 +19,10 @@
exit,
stderr,
stdout;
+import 'dart:math';
+
import 'package:args/args.dart';
import 'package:meta/meta.dart';
-import 'package:pigeon/functional.dart';
const String _testFlag = 'test';
const String _listFlag = 'list';
@@ -40,6 +41,9 @@
'android_unittests': _TestInfo(
function: _runAndroidUnitTests,
description: 'Unit tests on generated Java code.'),
+ 'android_kotlin_unittests': _TestInfo(
+ function: _runAndroidKotlinUnitTests,
+ description: 'Unit tests on generated Kotlin code.'),
'dart_compilation_tests': _TestInfo(
function: _runDartCompilationTests,
description: 'Compilation tests on generated Dart code.'),
@@ -95,6 +99,75 @@
throw UnimplementedError('See run_tests.sh.');
}
+Future<int> _runAndroidKotlinUnitTests() async {
+ const String androidKotlinUnitTestsPath =
+ './platform_tests/android_kotlin_unit_tests';
+ const List<String> tests = <String>[
+ 'all_datatypes',
+ 'all_void',
+ 'android_unittests',
+ 'async_handlers',
+ 'background_platform_channels',
+ 'enum_args',
+ 'enum',
+ 'host2flutter',
+ 'list',
+ 'message',
+ 'multiple_arity',
+ 'non_null_fields',
+ 'null_fields',
+ 'nullable_returns',
+ 'primitive',
+ 'void_arg_flutter',
+ 'void_arg_host',
+ 'voidflutter',
+ 'voidhost'
+ ];
+ int generateCode = 0;
+
+ for (final String test in tests) {
+ generateCode = await _runPigeon(
+ input: './pigeons/$test.dart',
+ kotlinOut:
+ '$androidKotlinUnitTestsPath/android/app/src/main/kotlin/com/example/android_kotlin_unit_tests/${snakeToPascalCase(test)}.kt',
+ kotlinPackage: 'com.example.android_kotlin_unit_tests',
+ );
+ if (generateCode != 0) {
+ return generateCode;
+ }
+ }
+
+ final Process gradlewExists = await _streamOutput(Process.start(
+ './gradlew',
+ <String>[],
+ workingDirectory: '$androidKotlinUnitTestsPath/android',
+ runInShell: true,
+ ));
+ final int gradlewExistsCode = await gradlewExists.exitCode;
+ if (gradlewExistsCode != 0) {
+ final Process compile = await _streamOutput(Process.start(
+ 'flutter',
+ <String>['build', 'apk', '--debug'],
+ workingDirectory: androidKotlinUnitTestsPath,
+ runInShell: true,
+ ));
+ final int compileCode = await compile.exitCode;
+ if (compileCode != 0) {
+ return compileCode;
+ }
+ }
+
+ final Process run = await _streamOutput(Process.start(
+ './gradlew',
+ <String>[
+ 'test',
+ ],
+ workingDirectory: '$androidKotlinUnitTestsPath/android',
+ ));
+
+ return run.exitCode;
+}
+
Future<int> _runDartCompilationTests() async {
throw UnimplementedError('See run_tests.sh.');
}
@@ -315,6 +388,8 @@
Future<int> _runPigeon(
{required String input,
+ String? kotlinOut,
+ String? kotlinPackage,
String? iosSwiftOut,
String? cppHeaderOut,
String? cppSourceOut,
@@ -331,6 +406,12 @@
'--copyright_header',
'./copyright_header.txt',
];
+ if (kotlinOut != null) {
+ args.addAll(<String>['--experimental_kotlin_out', kotlinOut]);
+ }
+ if (kotlinPackage != null) {
+ args.addAll(<String>['--experimental_kotlin_package', kotlinPackage]);
+ }
if (iosSwiftOut != null) {
args.addAll(<String>['--experimental_swift_out', iosSwiftOut]);
}
@@ -436,10 +517,12 @@
List<String> testsToRun = <String>[];
if (argResults.wasParsed(_listFlag)) {
print('available tests:');
+
+ final int columnWidth =
+ _tests.keys.map((String key) => key.length).reduce(max) + 4;
+
for (final MapEntry<String, _TestInfo> info in _tests.entries) {
- final int tabCount = (4 - info.key.length / 8).toInt();
- final String tabs = repeat('\t', tabCount).join();
- print('${info.key}$tabs- ${info.value.description}');
+ print('${info.key.padRight(columnWidth)}- ${info.value.description}');
}
exit(0);
} else if (argResults.wasParsed('help')) {