Skip to content

Commit

Permalink
Support for flows
Browse files Browse the repository at this point in the history
  • Loading branch information
erksch committed Feb 19, 2025
1 parent df0940d commit db28ed3
Show file tree
Hide file tree
Showing 7 changed files with 149 additions and 70 deletions.
2 changes: 1 addition & 1 deletion example/flutter/example/ios/Podfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,6 @@ SPEC CHECKSUMS:
FlutterKmpExample: 602f302c485b58e590be3d655f462b4962cdc4e3
integration_test: 4a889634ef21a45d28d50d622cf412dc6d9f586e

PODFILE CHECKSUM: 5f87884a088ccef5303b5a817b583138a43ec3e0
PODFILE CHECKSUM: e7404f0a25823bf8f7692369a15b9da345aa36ad

COCOAPODS: 1.16.2
2 changes: 1 addition & 1 deletion example/flutter/ios/Classes/FlutterKmpExamplePlugin.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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)
Expand All @@ -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()
)
Expand Down Expand Up @@ -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")
7 changes: 7 additions & 0 deletions flutter-kmp/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
plugins {
kotlin("multiplatform")
kotlin("plugin.serialization")
kotlin("native.cocoapods")
id("com.android.library")
}

Expand Down Expand Up @@ -29,6 +30,12 @@ kotlin {
publishLibraryVariants("release")
}

cocoapods {
pod("Flutter")
noPodspec()
}


iosX64()
iosArm64()
iosSimulatorArm64()
Expand Down
39 changes: 0 additions & 39 deletions flutter-kmp/src/androidMain/kotlin/de/voize/flutterkmp/utils.kt
Original file line number Diff line number Diff line change
Expand Up @@ -13,45 +13,6 @@ import kotlinx.serialization.json.*
import kotlinx.serialization.modules.serializersModuleOf
import kotlinx.serialization.serializer

private class DynamicLookupSerializer : KSerializer<Any> {
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 <reified T> Flow<T>.toEventStreamHandler(): EventChannel.StreamHandler =
toEventStreamHandler(serializer<T>())

Expand Down
54 changes: 54 additions & 0 deletions flutter-kmp/src/commonMain/kotlin/de/voize/flutterkmp/utils.kt
Original file line number Diff line number Diff line change
@@ -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<Any> {
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
}
48 changes: 48 additions & 0 deletions flutter-kmp/src/iosMain/kotlin/utils.kt
Original file line number Diff line number Diff line change
@@ -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 <reified T> Flow<T>.toEventStreamHandler(): NSObject =
toEventStreamHandler(serializer<T>())

fun <T> Flow<T>.toEventStreamHandler(serializer: SerializationStrategy<T>): 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
}
}
}

0 comments on commit db28ed3

Please sign in to comment.