Repository: flutter
Issue: [pigeon]Support EventChannelApi in multiple interface files
Labels: package,c: proposal,team-ecosystem,p: pigeon,P2,triaged-ecosystem

### Use case

In my project, I organize interface definitions across multiple files. I want to define EventChannelApi in multiple files using the `@EventChannelApi()` annotation.
However, this results in the generation of duplicate classes (e.g., `PigeonEventChannelWrapper` and `PigeonEventSink` in Swift), which causes build errors.

<details><summary>Code Example</summary>

## Input files

a.dart

```dart
import 'package:pigeon/pigeon.dart';

@ConfigurePigeon(PigeonOptions(
  dartOut: 'lib/src/a.g.dart',
  dartOptions: DartOptions(),
  kotlinOut: 'android/app/src/main/kotlin/dev/flutter/pigeon_sample/A.g.kt',
  kotlinOptions: KotlinOptions(),
  swiftOut: 'ios/Runner/A.g.swift',
  swiftOptions: SwiftOptions(),
))
@EventChannelApi()
abstract class EventA {
  int streamA();
}
```

b.dart

```dart
import 'package:pigeon/pigeon.dart';

@ConfigurePigeon(PigeonOptions(
  dartOut: 'lib/src/b.g.dart',
  dartOptions: DartOptions(),
  kotlinOut: 'android/app/src/main/kotlin/dev/flutter/pigeon_sample/B.g.kt',
  kotlinOptions: KotlinOptions(includeErrorClass: false),
  swiftOut: 'ios/Runner/B.g.swift',
  swiftOptions: SwiftOptions(includeErrorClass: false),
))
@EventChannelApi()
abstract class EventB {
  int streamB();
}
```

## Output files

A.g.swift

```swift
// Autogenerated from Pigeon (v22.7.2), do not edit directly.
// See also: https://pub.dev/packages/pigeon

import Foundation

#if os(iOS)
  import Flutter
#elseif os(macOS)
  import FlutterMacOS
#else
  #error("Unsupported platform.")
#endif

/// Error class for passing custom error details to Dart side.
final class PigeonError: Error {
  let code: String
  let message: String?
  let details: Any?

  init(code: String, message: String?, details: Any?) {
    self.code = code
    self.message = message
    self.details = details
  }

  var localizedDescription: String {
    return
      "PigeonError(code: \(code), message: \(message ?? "<nil>"), details: \(details ?? "<nil>")"
      }
}

private func isNullish(_ value: Any?) -> Bool {
  return value is NSNull || value == nil
}

private func nilOrValue<T>(_ value: Any?) -> T? {
  if value is NSNull { return nil }
  return value as! T?
}

private class APigeonCodecReader: FlutterStandardReader {
}

private class APigeonCodecWriter: FlutterStandardWriter {
}

private class APigeonCodecReaderWriter: FlutterStandardReaderWriter {
  override func reader(with data: Data) -> FlutterStandardReader {
    return APigeonCodecReader(data: data)
  }

  override func writer(with data: NSMutableData) -> FlutterStandardWriter {
    return APigeonCodecWriter(data: data)
  }
}

class APigeonCodec: FlutterStandardMessageCodec, @unchecked Sendable {
  static let shared = APigeonCodec(readerWriter: APigeonCodecReaderWriter())
}

var aPigeonMethodCodec = FlutterStandardMethodCodec(readerWriter: APigeonCodecReaderWriter());


private class PigeonStreamHandler<ReturnType>: NSObject, FlutterStreamHandler {
  private let wrapper: PigeonEventChannelWrapper<ReturnType>
  private var pigeonSink: PigeonEventSink<ReturnType>? = nil

  init(wrapper: PigeonEventChannelWrapper<ReturnType>) {
    self.wrapper = wrapper
  }

  func onListen(withArguments arguments: Any?, eventSink events: @escaping FlutterEventSink)
    -> FlutterError?
  {
    pigeonSink = PigeonEventSink<ReturnType>(events)
    wrapper.onListen(withArguments: arguments, sink: pigeonSink!)
    return nil
  }

  func onCancel(withArguments arguments: Any?) -> FlutterError? {
    pigeonSink = nil
    wrapper.onCancel(withArguments: arguments)
    return nil
  }
}

class PigeonEventChannelWrapper<ReturnType> {
  func onListen(withArguments arguments: Any?, sink: PigeonEventSink<ReturnType>) {}
  func onCancel(withArguments arguments: Any?) {}
}

class PigeonEventSink<ReturnType> {
  private let sink: FlutterEventSink

  init(_ sink: @escaping FlutterEventSink) {
    self.sink = sink
  }

  func success(_ value: ReturnType) {
    sink(value)
  }

  func error(code: String, message: String?, details: Any?) {
    sink(FlutterError(code: code, message: message, details: details))
  }

  func endOfStream() {
    sink(FlutterEndOfEventStream)
  }

}

class StreamAStreamHandler: PigeonEventChannelWrapper<Int64> {
  static func register(with messenger: FlutterBinaryMessenger, 
                      instanceName: String = "",
                      streamHandler: StreamAStreamHandler) {
    var channelName = "dev.flutter.pigeon.pigeon_sample.EventA.streamA"
    if !instanceName.isEmpty {
      channelName += ".\(instanceName)"
    }
    let internalStreamHandler = PigeonStreamHandler<Int64>(wrapper: streamHandler)
    let channel = FlutterEventChannel(name: channelName, binaryMessenger: messenger, codec: aPigeonMethodCodec)
    channel.setStreamHandler(internalStreamHandler)
  }
}
      

```

B.g.swift

```swift
// Autogenerated from Pigeon (v22.7.2), do not edit directly.
// See also: https://pub.dev/packages/pigeon

import Foundation

#if os(iOS)
  import Flutter
#elseif os(macOS)
  import FlutterMacOS
#else
  #error("Unsupported platform.")
#endif

private func isNullish(_ value: Any?) -> Bool {
  return value is NSNull || value == nil
}

private func nilOrValue<T>(_ value: Any?) -> T? {
  if value is NSNull { return nil }
  return value as! T?
}

private class BPigeonCodecReader: FlutterStandardReader {
}

private class BPigeonCodecWriter: FlutterStandardWriter {
}

private class BPigeonCodecReaderWriter: FlutterStandardReaderWriter {
  override func reader(with data: Data) -> FlutterStandardReader {
    return BPigeonCodecReader(data: data)
  }

  override func writer(with data: NSMutableData) -> FlutterStandardWriter {
    return BPigeonCodecWriter(data: data)
  }
}

class BPigeonCodec: FlutterStandardMessageCodec, @unchecked Sendable {
  static let shared = BPigeonCodec(readerWriter: BPigeonCodecReaderWriter())
}

var bPigeonMethodCodec = FlutterStandardMethodCodec(readerWriter: BPigeonCodecReaderWriter());


private class PigeonStreamHandler<ReturnType>: NSObject, FlutterStreamHandler {
  private let wrapper: PigeonEventChannelWrapper<ReturnType>
  private var pigeonSink: PigeonEventSink<ReturnType>? = nil

  init(wrapper: PigeonEventChannelWrapper<ReturnType>) {
    self.wrapper = wrapper
  }

  func onListen(withArguments arguments: Any?, eventSink events: @escaping FlutterEventSink)
    -> FlutterError?
  {
    pigeonSink = PigeonEventSink<ReturnType>(events)
    wrapper.onListen(withArguments: arguments, sink: pigeonSink!)
    return nil
  }

  func onCancel(withArguments arguments: Any?) -> FlutterError? {
    pigeonSink = nil
    wrapper.onCancel(withArguments: arguments)
    return nil
  }
}

class PigeonEventChannelWrapper<ReturnType> {
  func onListen(withArguments arguments: Any?, sink: PigeonEventSink<ReturnType>) {}
  func onCancel(withArguments arguments: Any?) {}
}

class PigeonEventSink<ReturnType> {
  private let sink: FlutterEventSink

  init(_ sink: @escaping FlutterEventSink) {
    self.sink = sink
  }

  func success(_ value: ReturnType) {
    sink(value)
  }

  func error(code: String, message: String?, details: Any?) {
    sink(FlutterError(code: code, message: message, details: details))
  }

  func endOfStream() {
    sink(FlutterEndOfEventStream)
  }

}

class StreamBStreamHandler: PigeonEventChannelWrapper<Int64> {
  static func register(with messenger: FlutterBinaryMessenger, 
                      instanceName: String = "",
                      streamHandler: StreamBStreamHandler) {
    var channelName = "dev.flutter.pigeon.pigeon_sample.EventB.streamB"
    if !instanceName.isEmpty {
      channelName += ".\(instanceName)"
    }
    let internalStreamHandler = PigeonStreamHandler<Int64>(wrapper: streamHandler)
    let channel = FlutterEventChannel(name: channelName, binaryMessenger: messenger, codec: bPigeonMethodCodec)
    channel.setStreamHandler(internalStreamHandler)
  }
}
      

```

</p>
</details>



### Proposal

I propose introducing a new configuration option in `SwiftOptions` and `KotlinOptions` to control the generation of shared utility classes (e.g., `PigeonEventChannelWrapper` and `PigeonEventSink`)  similar to the `includeErrorClass` option.