Skip to content

Commit

Permalink
[pigeon] adds event channel support for kotlin and swift (#7892)
Browse files Browse the repository at this point in the history
adds event channel support for kotlin and swift 
work towards flutter/flutter#66711
adds sealed classes with extensions (empty base classes only)
fixes flutter/flutter#155859 (Fix a small inconsistency with Pigeon docs)
adds some convenience methods to Root
fixes generation/format tests to include test pigeons
Makes swift codec class names upper camel case
  • Loading branch information
tarrinneal authored Dec 5, 2024
1 parent ea90218 commit 71a2e70
Show file tree
Hide file tree
Showing 48 changed files with 4,144 additions and 283 deletions.
6 changes: 6 additions & 0 deletions packages/pigeon/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
## 22.7.0

* [swift, kotlin] Adds event channel support.
* [swift, kotlin] Adds `sealed` class inheritance support.
* [swift] Updates codec class names to be upper camel case.

## 22.6.4

* [swift] Fixes the channel names of the named constructors of ProxyApis.
Expand Down
9 changes: 6 additions & 3 deletions packages/pigeon/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ Pigeon uses the `StandardMessageCodec` so it supports

Custom classes, nested datatypes, and enums are also supported.

Basic inheritance with empty `sealed` parent classes is allowed only in the Swift, Kotlin, and Dart generators.

Nullable enums in Objective-C generated code will be wrapped in a class to allow for nullability.

By default, custom classes in Swift are defined as structs.
Expand Down Expand Up @@ -104,9 +106,10 @@ to the api to allow for multiple instances to be created and operate in parallel
1) Method declarations on the API classes should have arguments and a return
value whose types are defined in the file, are supported datatypes, or are
`void`.
1) Generics are supported, but can currently only be used with nullable types
(example: `List<int?>`).
1) Objc and Swift have special naming conventions that can be utilized with the
1) Event channels are supported only on the Swift, Kotlin, and Dart generators.
1) Event channel methods should be wrapped in an `abstract class` with the metadata `@EventChannelApi`.
1) Event channel definitions should not include the `Stream` return type, just the type that is being streamed.
1) Objective-C and Swift have special naming conventions that can be utilized with the
`@ObjCSelector` and `@SwiftFunction` respectively.

### Flutter calling into iOS steps
Expand Down
113 changes: 113 additions & 0 deletions packages/pigeon/example/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -360,6 +360,119 @@ pigeon_example_package_message_flutter_api_flutter_method(
self->flutter_api, "hello", nullptr, flutter_method_cb, self);
```
## Event Channel Example
This example gives a basic overview of how to use Pigeon to set up an event channel.
### Dart input
<?code-excerpt "pigeons/event_channel_messages.dart (event-definitions)"?>
```dart
@EventChannelApi()
abstract class EventChannelMethods {
PlatformEvent streamEvents();
}
```

### Dart

The generated Dart code will include a method that returns a `Stream` when invoked.

<?code-excerpt "lib/main.dart (main-dart-event)"?>
```dart
Stream<String> getEventStream() async* {
final Stream<PlatformEvent> events = streamEvents();
await for (final PlatformEvent event in events) {
switch (event) {
case IntEvent():
final int intData = event.data;
yield '$intData, ';
case StringEvent():
final String stringData = event.data;
yield '$stringData, ';
}
}
}
```

### Swift

Define the stream handler class that will handle the events.

<?code-excerpt "ios/Runner/AppDelegate.swift (swift-class-event)"?>
```swift
class EventListener: StreamEventsStreamHandler {
var eventSink: PigeonEventSink<PlatformEvent>?

override func onListen(withArguments arguments: Any?, sink: PigeonEventSink<PlatformEvent>) {
eventSink = sink
}

func onIntEvent(event: Int64) {
if let eventSink = eventSink {
eventSink.success(IntEvent(data: event))
}
}

func onStringEvent(event: String) {
if let eventSink = eventSink {
eventSink.success(StringEvent(data: event))
}
}

func onEventsDone() {
eventSink?.endOfStream()
eventSink = nil
}
}
```

Register the handler with the generated method.

<?code-excerpt "ios/Runner/AppDelegate.swift (swift-init-event)"?>
```swift
let eventListener = EventListener()
StreamEventsStreamHandler.register(
with: controller.binaryMessenger, streamHandler: eventListener)
```

### Kotlin

Define the stream handler class that will handle the events.

<?code-excerpt "android/app/src/main/kotlin/dev/flutter/pigeon_example_app/MainActivity.kt (kotlin-class-event)"?>
```kotlin
class EventListener : StreamEventsStreamHandler() {
private var eventSink: PigeonEventSink<PlatformEvent>? = null

override fun onListen(p0: Any?, sink: PigeonEventSink<PlatformEvent>) {
eventSink = sink
}

fun onIntEvent(event: Long) {
eventSink?.success(IntEvent(data = event))
}

fun onStringEvent(event: String) {
eventSink?.success(StringEvent(data = event))
}

fun onEventsDone() {
eventSink?.endOfStream()
eventSink = null
}
}
```


Register the handler with the generated method.

<?code-excerpt "android/app/src/main/kotlin/dev/flutter/pigeon_example_app/MainActivity.kt (kotlin-init-event)"?>
```kotlin
val eventListener = EventListener()
StreamEventsStreamHandler.register(flutterEngine.dartExecutor.binaryMessenger, eventListener)
```

## Swift / Kotlin Plugin Example

A downloadable example of using Pigeon to create a Flutter Plugin with Swift and
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
// 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
@file:Suppress("UNCHECKED_CAST", "ArrayInDataClass")

import io.flutter.plugin.common.BinaryMessenger
import io.flutter.plugin.common.EventChannel
import io.flutter.plugin.common.StandardMessageCodec
import io.flutter.plugin.common.StandardMethodCodec
import java.io.ByteArrayOutputStream
import java.nio.ByteBuffer

/**
* Generated class from Pigeon that represents data sent in messages. This class should not be
* extended by any user class outside of the generated file.
*/
sealed class PlatformEvent
/** Generated class from Pigeon that represents data sent in messages. */
data class IntEvent(val data: Long) : PlatformEvent() {
companion object {
fun fromList(pigeonVar_list: List<Any?>): IntEvent {
val data = pigeonVar_list[0] as Long
return IntEvent(data)
}
}

fun toList(): List<Any?> {
return listOf(
data,
)
}
}

/** Generated class from Pigeon that represents data sent in messages. */
data class StringEvent(val data: String) : PlatformEvent() {
companion object {
fun fromList(pigeonVar_list: List<Any?>): StringEvent {
val data = pigeonVar_list[0] as String
return StringEvent(data)
}
}

fun toList(): List<Any?> {
return listOf(
data,
)
}
}

private open class EventChannelMessagesPigeonCodec : StandardMessageCodec() {
override fun readValueOfType(type: Byte, buffer: ByteBuffer): Any? {
return when (type) {
129.toByte() -> {
return (readValue(buffer) as? List<Any?>)?.let { IntEvent.fromList(it) }
}
130.toByte() -> {
return (readValue(buffer) as? List<Any?>)?.let { StringEvent.fromList(it) }
}
else -> super.readValueOfType(type, buffer)
}
}

override fun writeValue(stream: ByteArrayOutputStream, value: Any?) {
when (value) {
is IntEvent -> {
stream.write(129)
writeValue(stream, value.toList())
}
is StringEvent -> {
stream.write(130)
writeValue(stream, value.toList())
}
else -> super.writeValue(stream, value)
}
}
}

val EventChannelMessagesPigeonMethodCodec = StandardMethodCodec(EventChannelMessagesPigeonCodec())

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

override fun onListen(p0: Any?, sink: EventChannel.EventSink) {
pigeonSink = PigeonEventSink<T>(sink)
wrapper.onListen(p0, pigeonSink!!)
}

override fun onCancel(p0: Any?) {
pigeonSink = null
wrapper.onCancel(p0)
}
}

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

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

class PigeonEventSink<T>(private val sink: EventChannel.EventSink) {
fun success(value: T) {
sink.success(value)
}

fun error(errorCode: String, errorMessage: String?, errorDetails: Any?) {
sink.error(errorCode, errorMessage, errorDetails)
}

fun endOfStream() {
sink.endOfStream()
}
}

abstract class StreamEventsStreamHandler : PigeonEventChannelWrapper<PlatformEvent> {
companion object {
fun register(
messenger: BinaryMessenger,
streamHandler: StreamEventsStreamHandler,
instanceName: String = ""
) {
var channelName: String =
"dev.flutter.pigeon.pigeon_example_package.EventChannelMethods.streamEvents"
if (instanceName.isNotEmpty()) {
channelName += ".$instanceName"
}
val internalStreamHandler = PigeonStreamHandler<PlatformEvent>(streamHandler)
EventChannel(messenger, channelName, EventChannelMessagesPigeonMethodCodec)
.setStreamHandler(internalStreamHandler)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,15 @@ package dev.flutter.pigeon_example_app

import ExampleHostApi
import FlutterError
import IntEvent
import MessageData
import MessageFlutterApi
import PigeonEventSink
import PlatformEvent
import StreamEventsStreamHandler
import StringEvent
import android.os.Handler
import android.os.Looper
import io.flutter.embedding.android.FlutterActivity
import io.flutter.embedding.engine.FlutterEngine
import io.flutter.embedding.engine.plugins.FlutterPlugin
Expand Down Expand Up @@ -49,11 +56,66 @@ private class PigeonFlutterApi(binding: FlutterPlugin.FlutterPluginBinding) {
}
// #enddocregion kotlin-class-flutter

// #docregion kotlin-class-event
class EventListener : StreamEventsStreamHandler() {
private var eventSink: PigeonEventSink<PlatformEvent>? = null

override fun onListen(p0: Any?, sink: PigeonEventSink<PlatformEvent>) {
eventSink = sink
}

fun onIntEvent(event: Long) {
eventSink?.success(IntEvent(data = event))
}

fun onStringEvent(event: String) {
eventSink?.success(StringEvent(data = event))
}

fun onEventsDone() {
eventSink?.endOfStream()
eventSink = null
}
}
// #enddocregion kotlin-class-event

fun sendEvents(eventListener: EventListener) {
val handler = Handler(Looper.getMainLooper())
var count: Int = 0
val r: Runnable =
object : Runnable {
override fun run() {
if (count >= 100) {
handler.post { eventListener.onEventsDone() }
} else {
if (count % 2 == 0) {
handler.post {
eventListener.onIntEvent(count.toLong())
count++
}
} else {
handler.post {
eventListener.onStringEvent(count.toString())
count++
}
}
handler.postDelayed(this, 1000)
}
}
}
handler.post(r)
}

class MainActivity : FlutterActivity() {
override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
super.configureFlutterEngine(flutterEngine)

val api = PigeonApiImplementation()
ExampleHostApi.setUp(flutterEngine.dartExecutor.binaryMessenger, api)
// #docregion kotlin-init-event
val eventListener = EventListener()
StreamEventsStreamHandler.register(flutterEngine.dartExecutor.binaryMessenger, eventListener)
// #enddocregion kotlin-init-event
sendEvents(eventListener)
}
}
Loading

0 comments on commit 71a2e70

Please sign in to comment.