Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

[pigeon] Adds annotation options to omit shared classes used in Event Channels #8566

Merged
merged 6 commits into from
Feb 6, 2025
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension


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

* [kotlin, swift] Adds annotation options to omit shared classes used in Event Channels.

## 24.0.0

* **Breaking Change** Relocates some files in `lib` that were not intended for direct client use to `lib/src`.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -79,8 +79,9 @@ private open class EventChannelMessagesPigeonCodec : StandardMessageCodec() {

val EventChannelMessagesPigeonMethodCodec = StandardMethodCodec(EventChannelMessagesPigeonCodec())

private class PigeonStreamHandler<T>(val wrapper: PigeonEventChannelWrapper<T>) :
EventChannel.StreamHandler {
private class EventChannelMessagesPigeonStreamHandler<T>(
val wrapper: EventChannelMessagesPigeonEventChannelWrapper<T>
) : EventChannel.StreamHandler {
var pigeonSink: PigeonEventSink<T>? = null

override fun onListen(p0: Any?, sink: EventChannel.EventSink) {
Expand All @@ -94,7 +95,7 @@ private class PigeonStreamHandler<T>(val wrapper: PigeonEventChannelWrapper<T>)
}
}

interface PigeonEventChannelWrapper<T> {
interface EventChannelMessagesPigeonEventChannelWrapper<T> {
open fun onListen(p0: Any?, sink: PigeonEventSink<T>) {}

open fun onCancel(p0: Any?) {}
Expand All @@ -114,7 +115,8 @@ class PigeonEventSink<T>(private val sink: EventChannel.EventSink) {
}
}

abstract class StreamEventsStreamHandler : PigeonEventChannelWrapper<PlatformEvent> {
abstract class StreamEventsStreamHandler :
EventChannelMessagesPigeonEventChannelWrapper<PlatformEvent> {
companion object {
fun register(
messenger: BinaryMessenger,
Expand All @@ -126,7 +128,8 @@ abstract class StreamEventsStreamHandler : PigeonEventChannelWrapper<PlatformEve
if (instanceName.isNotEmpty()) {
channelName += ".$instanceName"
}
val internalStreamHandler = PigeonStreamHandler<PlatformEvent>(streamHandler)
val internalStreamHandler =
EventChannelMessagesPigeonStreamHandler<PlatformEvent>(streamHandler)
EventChannel(messenger, channelName, EventChannelMessagesPigeonMethodCodec)
.setStreamHandler(internalStreamHandler)
}
Expand Down
5 changes: 3 additions & 2 deletions packages/pigeon/lib/pigeon.dart
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@ export 'src/dart/dart_generator.dart' show DartOptions;
export 'src/gobject/gobject_generator.dart' show GObjectOptions;
export 'src/java/java_generator.dart' show JavaOptions;
export 'src/kotlin/kotlin_generator.dart'
show KotlinOptions, KotlinProxyApiOptions;
show KotlinEventChannelOptions, KotlinOptions, KotlinProxyApiOptions;
export 'src/objc/objc_generator.dart' show ObjcOptions;
export 'src/pigeon_lib.dart';
export 'src/swift/swift_generator.dart' show SwiftOptions, SwiftProxyApiOptions;
export 'src/swift/swift_generator.dart'
show SwiftEventChannelOptions, SwiftOptions, SwiftProxyApiOptions;
14 changes: 12 additions & 2 deletions packages/pigeon/lib/src/ast.dart
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,11 @@ import 'package:collection/collection.dart' show ListEquality;
import 'package:meta/meta.dart';

import 'generator_tools.dart';
import 'kotlin/kotlin_generator.dart' show KotlinProxyApiOptions;
import 'kotlin/kotlin_generator.dart'
show KotlinEventChannelOptions, KotlinProxyApiOptions;
import 'pigeon_lib.dart';
import 'swift/swift_generator.dart' show SwiftProxyApiOptions;
import 'swift/swift_generator.dart'
show SwiftEventChannelOptions, SwiftProxyApiOptions;

typedef _ListEquals = bool Function(List<Object?>, List<Object?>);

Expand Down Expand Up @@ -347,9 +349,17 @@ class AstEventChannelApi extends Api {
AstEventChannelApi({
required super.name,
required super.methods,
this.kotlinOptions,
this.swiftOptions,
super.documentationComments = const <String>[],
});

/// Options for Kotlin generated code for Event Channels.
final KotlinEventChannelOptions? kotlinOptions;

/// Options for Swift generated code for Event Channels.
final SwiftEventChannelOptions? swiftOptions;

@override
String toString() {
return '(EventChannelApi name:$name methods:$methods documentationComments:$documentationComments)';
Expand Down
2 changes: 1 addition & 1 deletion packages/pigeon/lib/src/generator_tools.dart
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import 'ast.dart';
/// The current version of pigeon.
///
/// This must match the version in pubspec.yaml.
const String pigeonVersion = '24.0.0';
const String pigeonVersion = '24.1.0';

/// Read all the content from [stdin] to a String.
String readStdin() {
Expand Down
26 changes: 21 additions & 5 deletions packages/pigeon/lib/src/kotlin/kotlin_generator.dart
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,18 @@ class KotlinProxyApiOptions {
final int? minAndroidApi;
}

/// Options for Kotlin implementation of Event Channels.
class KotlinEventChannelOptions {
/// Construct a [KotlinEventChannelOptions].
const KotlinEventChannelOptions({this.includeSharedClasses = true});

/// Whether to include the shared Event Channel classes in generation.
///
/// This should only ever be set to false if you have another generated
/// Kotlin file with Event Channels in the same directory.
final bool includeSharedClasses;
}

/// Class that manages all Kotlin code generation.
class KotlinGenerator extends StructuredGenerator<KotlinOptions> {
/// Instantiates a Kotlin Generator.
Expand Down Expand Up @@ -1018,8 +1030,8 @@ if (wrapped == null) {
}) {
indent.newln();
indent.format('''
private class PigeonStreamHandler<T>(
val wrapper: PigeonEventChannelWrapper<T>
private class ${generatorOptions.fileSpecificClassNameComponent}PigeonStreamHandler<T>(
val wrapper: ${generatorOptions.fileSpecificClassNameComponent}PigeonEventChannelWrapper<T>
stuartmorgan marked this conversation as resolved.
Show resolved Hide resolved
) : EventChannel.StreamHandler {
var pigeonSink: PigeonEventSink<T>? = null

Expand All @@ -1034,12 +1046,15 @@ if (wrapped == null) {
}
}

interface PigeonEventChannelWrapper<T> {
interface ${generatorOptions.fileSpecificClassNameComponent}PigeonEventChannelWrapper<T> {
open fun onListen(p0: Any?, sink: PigeonEventSink<T>) {}

open fun onCancel(p0: Any?) {}
}
''');

if (api.kotlinOptions?.includeSharedClasses ?? true) {
indent.format('''
class PigeonEventSink<T>(private val sink: EventChannel.EventSink) {
fun success(value: T) {
sink.success(value)
Expand All @@ -1054,18 +1069,19 @@ if (wrapped == null) {
}
}
''');
}
addDocumentationComments(
indent, api.documentationComments, _docCommentSpec);
for (final Method func in api.methods) {
indent.format('''
abstract class ${toUpperCamelCase(func.name)}StreamHandler : PigeonEventChannelWrapper<${_kotlinTypeForDartType(func.returnType)}> {
abstract class ${toUpperCamelCase(func.name)}StreamHandler : ${generatorOptions.fileSpecificClassNameComponent}PigeonEventChannelWrapper<${_kotlinTypeForDartType(func.returnType)}> {
companion object {
fun register(messenger: BinaryMessenger, streamHandler: ${toUpperCamelCase(func.name)}StreamHandler, instanceName: String = "") {
var channelName: String = "${makeChannelName(api, func, dartPackageName)}"
if (instanceName.isNotEmpty()) {
channelName += ".\$instanceName"
}
val internalStreamHandler = PigeonStreamHandler<${_kotlinTypeForDartType(func.returnType)}>(streamHandler)
val internalStreamHandler = ${generatorOptions.fileSpecificClassNameComponent}PigeonStreamHandler<${_kotlinTypeForDartType(func.returnType)}>(streamHandler)
EventChannel(messenger, channelName, ${generatorOptions.fileSpecificClassNameComponent}$_pigeonMethodChannelCodec).setStreamHandler(internalStreamHandler)
}
}
Expand Down
53 changes: 52 additions & 1 deletion packages/pigeon/lib/src/pigeon_lib.dart
Original file line number Diff line number Diff line change
Expand Up @@ -169,7 +169,13 @@ class ProxyApi {
/// defined return type of the method definition.
class EventChannelApi {
/// Constructor.
const EventChannelApi();
const EventChannelApi({this.kotlinOptions, this.swiftOptions});

/// Options for Kotlin generated code for Event Channels.
final KotlinEventChannelOptions? kotlinOptions;

/// Options for Swift generated code for Event Channels.
final SwiftEventChannelOptions? swiftOptions;
}

/// Metadata to annotation methods to control the selector used for objc output.
Expand Down Expand Up @@ -1082,6 +1088,8 @@ List<Error> _validateAst(Root root, String source) {
}
}

bool containsEventChannelApi = false;

for (final Api api in root.apis) {
final String? matchingPrefix = _findMatchingPrefixOrNull(
api.name,
Expand All @@ -1093,6 +1101,15 @@ List<Error> _validateAst(Root root, String source) {
'API name must not begin with "$matchingPrefix" in API "${api.name}"',
));
}
if (api is AstEventChannelApi) {
if (containsEventChannelApi) {
result.add(Error(
message:
'Event Channel methods must all be included in a single EventChannelApi',
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we have an issue tracking this? It's definitely low priority, but it's an unintuitive restriction.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I can fix it in this pr, it's not super complex. The main issue is that naming collisions aren't managed properly, since all of the methods end up top level when generated.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It doesn't need to be in this PR, just seemed worth filing so we had a tracker.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

));
}
containsEventChannelApi = true;
}
if (api is AstProxyApi) {
result.addAll(_validateProxyApi(
api,
Expand Down Expand Up @@ -1885,9 +1902,43 @@ class _RootBuilder extends dart_ast_visitor.RecursiveAstVisitor<Object?> {
_documentationCommentsParser(node.documentationComment?.tokens),
);
} else if (_hasMetadata(node.metadata, 'EventChannelApi')) {
final dart_ast.Annotation annotation = node.metadata.firstWhere(
(dart_ast.Annotation element) =>
element.name.name == 'EventChannelApi',
);

final Map<String, Object?> annotationMap = <String, Object?>{};
for (final dart_ast.Expression expression
in annotation.arguments!.arguments) {
if (expression is dart_ast.NamedExpression) {
annotationMap[expression.name.label.name] =
_expressionToMap(expression.expression);
}
}

SwiftEventChannelOptions? swiftOptions;
KotlinEventChannelOptions? kotlinOptions;
final Map<String, Object?>? swiftOptionsMap =
annotationMap['swiftOptions'] as Map<String, Object?>?;
if (swiftOptionsMap != null) {
swiftOptions = SwiftEventChannelOptions(
includeSharedClasses:
swiftOptionsMap['includeSharedClasses'] as bool? ?? true,
);
}
final Map<String, Object?>? kotlinOptionsMap =
annotationMap['kotlinOptions'] as Map<String, Object?>?;
if (kotlinOptionsMap != null) {
kotlinOptions = KotlinEventChannelOptions(
includeSharedClasses:
kotlinOptionsMap['includeSharedClasses'] as bool? ?? true,
);
}
_currentApi = AstEventChannelApi(
name: node.name.lexeme,
methods: <Method>[],
swiftOptions: swiftOptions,
kotlinOptions: kotlinOptions,
documentationComments:
_documentationCommentsParser(node.documentationComment?.tokens),
);
Expand Down
17 changes: 16 additions & 1 deletion packages/pigeon/lib/src/swift/swift_generator.dart
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,18 @@ class SwiftProxyApiOptions {
final bool supportsMacos;
}

/// Options for Swift implementation of Event Channels.
class SwiftEventChannelOptions {
/// Constructs a [SwiftEventChannelOptions].
const SwiftEventChannelOptions({this.includeSharedClasses = true});

/// Whether to include the error class in generation.
///
/// This should only ever be set to false if you have another generated
/// Swift file with Event Channels in the same directory.
final bool includeSharedClasses;
}

/// Class that manages all Swift code generation.
class SwiftGenerator extends StructuredGenerator<SwiftOptions> {
/// Instantiates a Swift Generator.
Expand Down Expand Up @@ -1362,7 +1374,9 @@ private func nilOrValue<T>(_ value: Any?) -> T? {
wrapper.onCancel(withArguments: arguments)
return nil
}
}
}''');
if (api.swiftOptions?.includeSharedClasses ?? true) {
indent.format('''

class PigeonEventChannelWrapper<ReturnType> {
func onListen(withArguments arguments: Any?, sink: PigeonEventSink<ReturnType>) {}
Expand Down Expand Up @@ -1390,6 +1404,7 @@ private func nilOrValue<T>(_ value: Any?) -> T? {

}
''');
}
addDocumentationComments(
indent, api.documentationComments, _docCommentSpec);
for (final Method func in api.methods) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
// 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/pigeon.dart';

// This file exists to test compilation for multi file event channel usage.
tarrinneal marked this conversation as resolved.
Show resolved Hide resolved

@EventChannelApi(
swiftOptions: SwiftEventChannelOptions(includeSharedClasses: false),
kotlinOptions: KotlinEventChannelOptions(includeSharedClasses: false))
abstract class EventChannelMethods {
int streamIntsAgain();
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
// 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.
//
// Autogenerated from Pigeon, do not edit directly.
// See also: https://pub.dev/packages/pigeon
// ignore_for_file: public_member_api_docs, non_constant_identifier_names, avoid_as, unused_import, unnecessary_parenthesis, prefer_null_aware_operators, omit_local_variable_types, unused_shown_name, unnecessary_import, no_leading_underscores_for_local_identifiers

import 'dart:async';
import 'dart:typed_data' show Float64List, Int32List, Int64List, Uint8List;

import 'package:flutter/foundation.dart' show ReadBuffer, WriteBuffer;
import 'package:flutter/services.dart';

class _PigeonCodec extends StandardMessageCodec {
const _PigeonCodec();
@override
void writeValue(WriteBuffer buffer, Object? value) {
if (value is int) {
buffer.putUint8(4);
buffer.putInt64(value);
} else {
super.writeValue(buffer, value);
}
}

@override
Object? readValueOfType(int type, ReadBuffer buffer) {
switch (type) {
default:
return super.readValueOfType(type, buffer);
}
}
}

const StandardMethodCodec pigeonMethodCodec =
StandardMethodCodec(_PigeonCodec());

Stream<int> streamIntsAgain({String instanceName = ''}) {
if (instanceName.isNotEmpty) {
instanceName = '.$instanceName';
}
final EventChannel streamIntsAgainChannel = EventChannel(
'dev.flutter.pigeon.pigeon_integration_tests.EventChannelMethods.streamIntsAgain$instanceName',
pigeonMethodCodec);
return streamIntsAgainChannel.receiveBroadcastStream().map((dynamic event) {
return event as int;
});
}
Loading