From db28ed3d0b3920a0b0d8012007f7f0b9828f1b29 Mon Sep 17 00:00:00 2001 From: Erik Ziegler Date: Wed, 19 Feb 2025 18:42:49 +0100 Subject: [PATCH] Support for flows --- example/flutter/example/ios/Podfile.lock | 2 +- .../ios/Classes/FlutterKmpExamplePlugin.swift | 2 +- .../ksp/processor/IOSKotlinModuleGenerator.kt | 67 +++++++++++-------- flutter-kmp/build.gradle.kts | 7 ++ .../kotlin/de/voize/flutterkmp/utils.kt | 39 ----------- .../kotlin/de/voize/flutterkmp/utils.kt | 54 +++++++++++++++ flutter-kmp/src/iosMain/kotlin/utils.kt | 48 +++++++++++++ 7 files changed, 149 insertions(+), 70 deletions(-) create mode 100644 flutter-kmp/src/commonMain/kotlin/de/voize/flutterkmp/utils.kt create mode 100644 flutter-kmp/src/iosMain/kotlin/utils.kt diff --git a/example/flutter/example/ios/Podfile.lock b/example/flutter/example/ios/Podfile.lock index 72e2202..838c0d0 100644 --- a/example/flutter/example/ios/Podfile.lock +++ b/example/flutter/example/ios/Podfile.lock @@ -30,6 +30,6 @@ SPEC CHECKSUMS: FlutterKmpExample: 602f302c485b58e590be3d655f462b4962cdc4e3 integration_test: 4a889634ef21a45d28d50d622cf412dc6d9f586e -PODFILE CHECKSUM: 5f87884a088ccef5303b5a817b583138a43ec3e0 +PODFILE CHECKSUM: e7404f0a25823bf8f7692369a15b9da345aa36ad COCOAPODS: 1.16.2 diff --git a/example/flutter/ios/Classes/FlutterKmpExamplePlugin.swift b/example/flutter/ios/Classes/FlutterKmpExamplePlugin.swift index 4a2b139..fb46761 100644 --- a/example/flutter/ios/Classes/FlutterKmpExamplePlugin.swift +++ b/example/flutter/ios/Classes/FlutterKmpExamplePlugin.swift @@ -9,7 +9,7 @@ public class FlutterKmpExamplePlugin: NSObject, FlutterPlugin { public static func register(with registrar: FlutterPluginRegistrar) { let instance = FlutterKmpExamplePlugin() - MyTestClassIOS.companion.register( + myTestClass.register( registrar: registrar, pluginInstance: instance ) diff --git a/flutter-kmp-ksp/src/main/kotlin/de/voize/flutterkmp/ksp/processor/IOSKotlinModuleGenerator.kt b/flutter-kmp-ksp/src/main/kotlin/de/voize/flutterkmp/ksp/processor/IOSKotlinModuleGenerator.kt index a96bfd6..0ed9c0a 100644 --- a/flutter-kmp-ksp/src/main/kotlin/de/voize/flutterkmp/ksp/processor/IOSKotlinModuleGenerator.kt +++ b/flutter-kmp-ksp/src/main/kotlin/de/voize/flutterkmp/ksp/processor/IOSKotlinModuleGenerator.kt @@ -5,17 +5,16 @@ import com.google.devtools.ksp.symbol.KSDeclaration import com.google.devtools.ksp.symbol.KSFunctionDeclaration import com.google.devtools.ksp.symbol.KSPropertyDeclaration import com.google.devtools.ksp.symbol.KSValueParameter -import com.google.devtools.ksp.symbol.Modifier import com.squareup.kotlinpoet.ClassName import com.squareup.kotlinpoet.CodeBlock import com.squareup.kotlinpoet.FileSpec import com.squareup.kotlinpoet.FunSpec import com.squareup.kotlinpoet.KModifier +import com.squareup.kotlinpoet.MemberName import com.squareup.kotlinpoet.PropertySpec import com.squareup.kotlinpoet.TypeSpec import com.squareup.kotlinpoet.ksp.toTypeName import com.squareup.kotlinpoet.ksp.writeTo -import io.outfoxx.swiftpoet.DeclaredTypeName class IOSKotlinModuleGenerator { private fun String.iosModuleClassName() = this + "IOS" @@ -32,7 +31,7 @@ class IOSKotlinModuleGenerator { val wrappedModuleVarName = "wrappedModule" val pluginInstanceConstructorArgName = "pluginInstance" val registrarConstructorArgName = "registrar" - val channelVarName = "channel" + val methodChannelVarName = "methodChannel" val classSpec = TypeSpec.classBuilder(className).apply { addAnnotation(ExperimentalForeignApi) @@ -50,34 +49,42 @@ class IOSKotlinModuleGenerator { ClassName(packageName, wrappedClassName) ).addModifiers(KModifier.PRIVATE).initializer(constructorInvocation).build() ) - .addType( - TypeSpec.companionObjectBuilder() - .addFunction( - FunSpec.builder("register") - .addParameter( - registrarConstructorArgName, - FlutterPluginRegistrar, + .addFunction( + FunSpec.builder("register") + .addParameter( + registrarConstructorArgName, + FlutterPluginRegistrar, + ) + .addParameter(pluginInstanceConstructorArgName, FlutterPlugin) + .addCode( + CodeBlock.builder().apply { + addStatement( + "val $methodChannelVarName = %T(%S, %L, %T.sharedInstance())", + FlutterMethodChannel, + flutterModule.moduleName, + "$registrarConstructorArgName.messenger()", + FlutterStandardMethodCodec ) - .addParameter(pluginInstanceConstructorArgName, FlutterPlugin) - .addCode( - CodeBlock.builder().apply { - addStatement( - "val $channelVarName = %T(%S, %L, %T.sharedInstance())", - FlutterMethodChannel, - flutterModule.moduleName, - "$registrarConstructorArgName.messenger()", - FlutterStandardMethodCodec - ) - addStatement( - "%L.addMethodCallDelegate(%L as %T, %L)", - registrarConstructorArgName, - pluginInstanceConstructorArgName, - NSObject, - channelVarName, - ) - }.build() + addStatement( + "%L.addMethodCallDelegate(%L as %T, %L)", + registrarConstructorArgName, + pluginInstanceConstructorArgName, + NSObject, + methodChannelVarName, ) - .build() + flutterModule.flutterFlows.forEach { + addStatement( + "%T(%S, %L, %T.sharedInstance()).setStreamHandler(%N.%M.%M())", + FlutterEventChannel, + "${flutterModule.moduleName}_${it.simpleName.asString()}", + "$registrarConstructorArgName.messenger()", + FlutterStandardMethodCodec, + wrappedModuleVarName, + MemberName(packageName, it.simpleName.asString()), + toEventStreamHandler, + ) + } + }.build() ) .build() ) @@ -244,6 +251,8 @@ private val NSObject = ClassName("platform.darwin", "NSObject") private val FlutterResult = ClassName("cocoapods.Flutter", "FlutterResult") private val FlutterMethodCall = ClassName("cocoapods.Flutter", "FlutterMethodCall") private val FlutterMethodChannel = ClassName("cocoapods.Flutter", "FlutterMethodChannel") +private val FlutterEventChannel = ClassName("cocoapods.Flutter", "FlutterEventChannel") private val FlutterPluginRegistrar = ClassName("cocoapods.Flutter", "FlutterPluginRegistrarProtocol") private val FlutterPlugin = ClassName("cocoapods.Flutter", "FlutterPluginProtocol") private val FlutterStandardMethodCodec = ClassName("cocoapods.Flutter", "FlutterStandardMethodCodec") +private val toEventStreamHandler = MemberName(flutterKmpPackageName, "toEventStreamHandler") \ No newline at end of file diff --git a/flutter-kmp/build.gradle.kts b/flutter-kmp/build.gradle.kts index bc3b500..3cd0316 100644 --- a/flutter-kmp/build.gradle.kts +++ b/flutter-kmp/build.gradle.kts @@ -1,6 +1,7 @@ plugins { kotlin("multiplatform") kotlin("plugin.serialization") + kotlin("native.cocoapods") id("com.android.library") } @@ -29,6 +30,12 @@ kotlin { publishLibraryVariants("release") } + cocoapods { + pod("Flutter") + noPodspec() + } + + iosX64() iosArm64() iosSimulatorArm64() diff --git a/flutter-kmp/src/androidMain/kotlin/de/voize/flutterkmp/utils.kt b/flutter-kmp/src/androidMain/kotlin/de/voize/flutterkmp/utils.kt index a4b0890..a3b8d52 100644 --- a/flutter-kmp/src/androidMain/kotlin/de/voize/flutterkmp/utils.kt +++ b/flutter-kmp/src/androidMain/kotlin/de/voize/flutterkmp/utils.kt @@ -13,45 +13,6 @@ import kotlinx.serialization.json.* import kotlinx.serialization.modules.serializersModuleOf import kotlinx.serialization.serializer -private class DynamicLookupSerializer : KSerializer { - override val descriptor: SerialDescriptor = buildClassSerialDescriptor("Any") - - override fun serialize(encoder: Encoder, value: Any) { - val jsonEncoder = encoder as JsonEncoder - val jsonElement = serializeAny(value) - jsonEncoder.encodeJsonElement(jsonElement) - } - - private fun serializeAny(value: Any?): JsonElement = when (value) { - null -> JsonNull - is Map<*, *> -> { - val mapContents = value.entries.associate { mapEntry -> - mapEntry.key.toString() to serializeAny(mapEntry.value) - } - JsonObject(mapContents) - } - - is List<*> -> { - val arrayContents = value.map { listEntry -> serializeAny(listEntry) } - JsonArray(arrayContents) - } - - is Number -> JsonPrimitive(value) - is Boolean -> JsonPrimitive(value) - is String -> JsonPrimitive(value) - else -> error("Unsupported type ${value::class}") - } - - override fun deserialize(decoder: Decoder): Any { - error("Unsupported") - } -} - -private val flowToFlutterJson = Json { - serializersModule = serializersModuleOf(DynamicLookupSerializer()) - encodeDefaults = true -} - inline fun Flow.toEventStreamHandler(): EventChannel.StreamHandler = toEventStreamHandler(serializer()) diff --git a/flutter-kmp/src/commonMain/kotlin/de/voize/flutterkmp/utils.kt b/flutter-kmp/src/commonMain/kotlin/de/voize/flutterkmp/utils.kt new file mode 100644 index 0000000..2db5940 --- /dev/null +++ b/flutter-kmp/src/commonMain/kotlin/de/voize/flutterkmp/utils.kt @@ -0,0 +1,54 @@ +package de.voize.flutterkmp + +import kotlinx.serialization.KSerializer +import kotlinx.serialization.descriptors.SerialDescriptor +import kotlinx.serialization.descriptors.buildClassSerialDescriptor +import kotlinx.serialization.encoding.Decoder +import kotlinx.serialization.encoding.Encoder +import kotlinx.serialization.json.Json +import kotlinx.serialization.json.JsonArray +import kotlinx.serialization.json.JsonElement +import kotlinx.serialization.json.JsonEncoder +import kotlinx.serialization.json.JsonNull +import kotlinx.serialization.json.JsonObject +import kotlinx.serialization.json.JsonPrimitive +import kotlinx.serialization.modules.serializersModuleOf + +internal class DynamicLookupSerializer : KSerializer { + override val descriptor: SerialDescriptor = buildClassSerialDescriptor("Any") + + override fun serialize(encoder: Encoder, value: Any) { + val jsonEncoder = encoder as JsonEncoder + val jsonElement = serializeAny(value) + jsonEncoder.encodeJsonElement(jsonElement) + } + + private fun serializeAny(value: Any?): JsonElement = when (value) { + null -> JsonNull + is Map<*, *> -> { + val mapContents = value.entries.associate { mapEntry -> + mapEntry.key.toString() to serializeAny(mapEntry.value) + } + JsonObject(mapContents) + } + + is List<*> -> { + val arrayContents = value.map { listEntry -> serializeAny(listEntry) } + JsonArray(arrayContents) + } + + is Number -> JsonPrimitive(value) + is Boolean -> JsonPrimitive(value) + is String -> JsonPrimitive(value) + else -> error("Unsupported type ${value::class}") + } + + override fun deserialize(decoder: Decoder): Any { + error("Unsupported") + } +} + +internal val flowToFlutterJson = Json { + serializersModule = serializersModuleOf(DynamicLookupSerializer()) + encodeDefaults = true +} \ No newline at end of file diff --git a/flutter-kmp/src/iosMain/kotlin/utils.kt b/flutter-kmp/src/iosMain/kotlin/utils.kt new file mode 100644 index 0000000..fb60e9f --- /dev/null +++ b/flutter-kmp/src/iosMain/kotlin/utils.kt @@ -0,0 +1,48 @@ +package de.voize.flutterkmp + +import cocoapods.Flutter.FlutterError +import cocoapods.Flutter.FlutterEventSink +import kotlinx.coroutines.flow.Flow +import cocoapods.Flutter.FlutterStreamHandlerProtocol +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import kotlinx.serialization.SerializationStrategy +import kotlinx.serialization.serializer +import platform.darwin.NSObject + + +inline fun Flow.toEventStreamHandler(): NSObject = + toEventStreamHandler(serializer()) + +fun Flow.toEventStreamHandler(serializer: SerializationStrategy): NSObject { + return object : FlutterStreamHandlerProtocol, NSObject() { + var job: Job? = null + + override fun onListenWithArguments( + arguments: Any?, + eventSink: FlutterEventSink, + ): FlutterError? { + if (eventSink == null) { + return FlutterError.errorWithCode("no_event_sink", "No event sink available", null) + } + + job = CoroutineScope(Dispatchers.Default).launch { + this@toEventStreamHandler.collect { + withContext(Dispatchers.Main) { + eventSink(flowToFlutterJson.encodeToString(serializer, it)) + } + } + } + return null + } + + override fun onCancelWithArguments(arguments: Any?): FlutterError? { + job?.cancel() + job = null + return null + } + } +} \ No newline at end of file