From 1d925d7e1d006940a5124d9b912723e6959a56c9 Mon Sep 17 00:00:00 2001 From: Erik Ziegler Date: Sat, 22 Feb 2025 16:17:20 +0100 Subject: [PATCH] Fix conflicts with same method names across different modules on iOS This is done by prefixig method names with the module name --- CHANGELOG.md | 2 + example/flutter/example/lib/main.dart | 5 + example/flutter/lib/MySecondTestModule.dart | 17 ++- example/flutter/lib/MyTestModule.dart | 85 ++++++----- .../example/flutterkmpexample/MyTestModule.kt | 6 + .../ksp/processor/AndroidModuleGenerator.kt | 125 ++------------- .../flutterkmp/ksp/processor/DartGenerator.kt | 16 +- .../ksp/processor/IOSKotlinModuleGenerator.kt | 134 ++-------------- .../voize/flutterkmp/ksp/processor/shared.kt | 143 +++++++++++++++++- 9 files changed, 251 insertions(+), 282 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 664b1be..22646d0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,8 @@ ## unreleased +- Fix conflicts with same method names across different modules on iOS by prefixing method names + ## v0.1.0-rc.2 - Fix errors with boolean types in method return and flow types on iOS diff --git a/example/flutter/example/lib/main.dart b/example/flutter/example/lib/main.dart index a68dc7c..198ffe3 100644 --- a/example/flutter/example/lib/main.dart +++ b/example/flutter/example/lib/main.dart @@ -192,6 +192,11 @@ class _MyAppState extends State { final mySecondTestModule = MySecondTestModule(); + print( + "myTestModule.methodWithSameNameAsInOtherModule: ${await myTestModule.methodWithSameNameAsInOtherModule("abc")}"); + print( + "mySecondTestModule.methodWithSameNameAsInOtherModule: ${await mySecondTestModule.methodWithSameNameAsInOtherModule(123)}"); + print( "mySecondTestModule.testMethod: ${await mySecondTestModule.testMethod()}"); diff --git a/example/flutter/lib/MySecondTestModule.dart b/example/flutter/lib/MySecondTestModule.dart index b224ef9..be00893 100644 --- a/example/flutter/lib/MySecondTestModule.dart +++ b/example/flutter/lib/MySecondTestModule.dart @@ -20,7 +20,7 @@ final Stream dataClassEvents = const EventChannel('MySecondTestModu Future testMethod() async { final invokeResult = await methodChannelToNative.invokeMethod( - 'testMethod', + 'MySecondTestModule_testMethod', [], ); @@ -32,4 +32,19 @@ final Stream dataClassEvents = const EventChannel('MySecondTestModu return result; } +Future methodWithSameNameAsInOtherModule(int value) async { + + final invokeResult = await methodChannelToNative.invokeMethod( + 'MySecondTestModule_methodWithSameNameAsInOtherModule', + [value], + ); + + if (invokeResult == null) { + throw PlatformException(code: '1', message: 'Method methodWithSameNameAsInOtherModule failed'); + } + + final result = invokeResult; + + return result; +} } \ No newline at end of file diff --git a/example/flutter/lib/MyTestModule.dart b/example/flutter/lib/MyTestModule.dart index 924b82f..d74340e 100644 --- a/example/flutter/lib/MyTestModule.dart +++ b/example/flutter/lib/MyTestModule.dart @@ -25,7 +25,7 @@ final Stream dataClassEvents = const EventChannel('MyTestModule_dat Future next(int? previous) async { return await methodChannelToNative.invokeMethod( - 'intState', + 'MyTestModule_intState', [previous] ); } @@ -60,7 +60,7 @@ final Stream dataClassEvents = const EventChannel('MyTestModule_dat Future next(String? previous) async { return await methodChannelToNative.invokeMethod( - 'dataClassState', + 'MyTestModule_dataClassState', [previous] ); } @@ -95,7 +95,7 @@ final Stream dataClassEvents = const EventChannel('MyTestModule_dat Future next(String? previous) async { return await methodChannelToNative.invokeMethod( - 'parameterizedDataClassState', + 'MyTestModule_parameterizedDataClassState', [previous, dataSerialized] ); } @@ -130,7 +130,7 @@ final Stream dataClassEvents = const EventChannel('MyTestModule_dat Future next(String? previous) async { return await methodChannelToNative.invokeMethod( - 'boolState', + 'MyTestModule_boolState', [previous] ); } @@ -165,7 +165,7 @@ final Stream dataClassEvents = const EventChannel('MyTestModule_dat Future next(int? previous) async { return await methodChannelToNative.invokeMethod( - 'intStateAdd', + 'MyTestModule_intStateAdd', [previous, num] ); } @@ -196,12 +196,12 @@ final Stream dataClassEvents = const EventChannel('MyTestModule_dat } Future unitMethod() async { - await methodChannelToNative.invokeMethod('unitMethod', []); + await methodChannelToNative.invokeMethod('MyTestModule_unitMethod', []); } Future simpleMethod() async { final invokeResult = await methodChannelToNative.invokeMethod( - 'simpleMethod', + 'MyTestModule_simpleMethod', [], ); @@ -216,7 +216,7 @@ Future simpleMethod() async { Future stringMethod(String value) async { final invokeResult = await methodChannelToNative.invokeMethod( - 'stringMethod', + 'MyTestModule_stringMethod', [value], ); @@ -231,7 +231,7 @@ Future stringMethod(String value) async { Future nullableStringMethod(String? value) async { final invokeResult = await methodChannelToNative.invokeMethod( - 'nullableStringMethod', + 'MyTestModule_nullableStringMethod', [value], ); final result = invokeResult; @@ -240,7 +240,7 @@ Future nullableStringMethod(String? value) async { Future intMethod(int value) async { final invokeResult = await methodChannelToNative.invokeMethod( - 'intMethod', + 'MyTestModule_intMethod', [value], ); @@ -255,7 +255,7 @@ Future intMethod(int value) async { Future doubleMethod(double value) async { final invokeResult = await methodChannelToNative.invokeMethod( - 'doubleMethod', + 'MyTestModule_doubleMethod', [value], ); @@ -270,7 +270,7 @@ Future doubleMethod(double value) async { Future boolMethod(bool value) async { final valueSerialized = value.toString(); final invokeResult = await methodChannelToNative.invokeMethod( - 'boolMethod', + 'MyTestModule_boolMethod', [valueSerialized], ); @@ -282,15 +282,30 @@ Future boolMethod(bool value) async { return result; } +Future methodWithSameNameAsInOtherModule(String value) async { + + final invokeResult = await methodChannelToNative.invokeMethod( + 'MyTestModule_methodWithSameNameAsInOtherModule', + [value], + ); + + if (invokeResult == null) { + throw PlatformException(code: '1', message: 'Method methodWithSameNameAsInOtherModule failed'); + } + + final result = invokeResult; + + return result; +} Future parameterizedMethod(String a, int b, bool c, double d) async { final cSerialized = c.toString(); - await methodChannelToNative.invokeMethod('parameterizedMethod', [a, b, cSerialized, d]); + await methodChannelToNative.invokeMethod('MyTestModule_parameterizedMethod', [a, b, cSerialized, d]); } Future localDateTimeMethod(DateTime localDateTime) async { if (localDateTime.isUtc) throw ArgumentError('localDateTime must not be in UTC'); final localDateTimeSerialized = localDateTime.toIso8601String(); final invokeResult = await methodChannelToNative.invokeMethod( - 'localDateTimeMethod', + 'MyTestModule_localDateTimeMethod', [localDateTimeSerialized], ); @@ -305,7 +320,7 @@ final localDateTimeSerialized = localDateTime.toIso8601String(); Future localTimeMethod(TimeOfDay localTime) async { final localTimeSerialized = "${localTime.hour.toString().padLeft(2, '0')}:${localTime.minute.toString().padLeft(2, '0')}"; final invokeResult = await methodChannelToNative.invokeMethod( - 'localTimeMethod', + 'MyTestModule_localTimeMethod', [localTimeSerialized], ); @@ -321,7 +336,7 @@ Future localDateMethod(DateTime localDate) async { if (localDate.isUtc) throw ArgumentError('localDate must not be in UTC'); final localDateSerialized = localDate.toIso8601String().split('T').first; final invokeResult = await methodChannelToNative.invokeMethod( - 'localDateMethod', + 'MyTestModule_localDateMethod', [localDateSerialized], ); @@ -336,7 +351,7 @@ final localDateSerialized = localDate.toIso8601String().split('T').first; Future durationMethod(Duration duration) async { final durationSerialized = duration.toIso8601String(); final invokeResult = await methodChannelToNative.invokeMethod( - 'durationMethod', + 'MyTestModule_durationMethod', [durationSerialized], ); @@ -352,7 +367,7 @@ Future instantMethod(DateTime instant) async { if (!instant.isUtc) throw ArgumentError('instant must be in UTC'); final instantSerialized = instant.toIso8601String(); final invokeResult = await methodChannelToNative.invokeMethod( - 'instantMethod', + 'MyTestModule_instantMethod', [instantSerialized], ); @@ -367,7 +382,7 @@ final instantSerialized = instant.toIso8601String(); Future> stringListMethod(List list) async { final listSerialized = jsonEncode(list.map((e) => e).toList()); final invokeResult = await methodChannelToNative.invokeMethod( - 'stringListMethod', + 'MyTestModule_stringListMethod', [listSerialized], ); @@ -384,7 +399,7 @@ return element as String; Future>> nestedListMethod(List> list) async { final listSerialized = jsonEncode(list.map((e) => e.map((e) => e).toList()).toList()); final invokeResult = await methodChannelToNative.invokeMethod( - 'nestedListMethod', + 'MyTestModule_nestedListMethod', [listSerialized], ); @@ -403,7 +418,7 @@ return element as String; Future> dataClassListMethod(List list) async { final listSerialized = jsonEncode(list.map((e) => e).toList()); final invokeResult = await methodChannelToNative.invokeMethod( - 'dataClassListMethod', + 'MyTestModule_dataClassListMethod', [listSerialized], ); @@ -420,7 +435,7 @@ return MyDataClass.fromJson(element); Future>> nestedDataClassListMethod(List> list) async { final listSerialized = jsonEncode(list.map((e) => e.map((e) => e).toList()).toList()); final invokeResult = await methodChannelToNative.invokeMethod( - 'nestedDataClassListMethod', + 'MyTestModule_nestedDataClassListMethod', [listSerialized], ); @@ -439,7 +454,7 @@ return MyDataClass.fromJson(element); Future> mapMethod(Map map) async { final mapSerialized = jsonEncode(map.map((k, v) => MapEntry(k, v))); final invokeResult = await methodChannelToNative.invokeMethod( - 'mapMethod', + 'MyTestModule_mapMethod', [mapSerialized], ); @@ -456,7 +471,7 @@ return MapEntry(key, value as int); Future objectMethod(MyDataObject obj) async { final objSerialized = jsonEncode(obj.toJson()); final invokeResult = await methodChannelToNative.invokeMethod( - 'objectMethod', + 'MyTestModule_objectMethod', [objSerialized], ); @@ -471,7 +486,7 @@ Future objectMethod(MyDataObject obj) async { Future sealedMethod(MySealedData obj) async { final objSerialized = jsonEncode(MySealedData.toJson(obj)); final invokeResult = await methodChannelToNative.invokeMethod( - 'sealedMethod', + 'MyTestModule_sealedMethod', [objSerialized], ); @@ -486,7 +501,7 @@ Future sealedMethod(MySealedData obj) async { Future dateClassMethod(MyDateClass obj) async { final objSerialized = jsonEncode(obj.toJson()); final invokeResult = await methodChannelToNative.invokeMethod( - 'dateClassMethod', + 'MyTestModule_dateClassMethod', [objSerialized], ); @@ -501,7 +516,7 @@ Future dateClassMethod(MyDateClass obj) async { Future sealedWithPropsMethod(MySealedDataWithProps obj) async { final objSerialized = jsonEncode(MySealedDataWithProps.toJson(obj)); final invokeResult = await methodChannelToNative.invokeMethod( - 'sealedWithPropsMethod', + 'MyTestModule_sealedWithPropsMethod', [objSerialized], ); @@ -516,7 +531,7 @@ Future sealedWithPropsMethod(MySealedDataWithProps obj) a Future classWithSealedPropMethod(MyDataClassWithSealed obj) async { final objSerialized = jsonEncode(obj.toJson()); final invokeResult = await methodChannelToNative.invokeMethod( - 'classWithSealedPropMethod', + 'MyTestModule_classWithSealedPropMethod', [objSerialized], ); @@ -531,7 +546,7 @@ Future classWithSealedPropMethod(MyDataClassWithSealed ob Future enumMethod(MyEnum entry) async { final entrySerialized = jsonEncode(entry.name);; final invokeResult = await methodChannelToNative.invokeMethod( - 'enumMethod', + 'MyTestModule_enumMethod', [entrySerialized], ); @@ -546,7 +561,7 @@ Future enumMethod(MyEnum entry) async { Future> enumListMethod(List entries) async { final entriesSerialized = jsonEncode(entries.map((e) => e.name).toList()); final invokeResult = await methodChannelToNative.invokeMethod( - 'enumListMethod', + 'MyTestModule_enumListMethod', [entriesSerialized], ); @@ -563,7 +578,7 @@ return MyEnum.values.byName(element); Future> enumMapMethod(Map entries) async { final entriesSerialized = jsonEncode(entries.map((k, v) => MapEntry(k, v.name))); final invokeResult = await methodChannelToNative.invokeMethod( - 'enumMapMethod', + 'MyTestModule_enumMapMethod', [entriesSerialized], ); @@ -580,7 +595,7 @@ return MapEntry(key, MyEnum.values.byName(value)); Future>>> mixedMethod(Map>> map) async { final mapSerialized = jsonEncode(map.map((k, v) => MapEntry(k, v.map((e) => e.map((k, v) => MapEntry(k, v))).toList()))); final invokeResult = await methodChannelToNative.invokeMethod( - 'mixedMethod', + 'MyTestModule_mixedMethod', [mapSerialized], ); @@ -601,7 +616,7 @@ return MapEntry(key, MyDataClass.fromJson(value)); Future dataClassMethod(MyDataClass data) async { final dataSerialized = jsonEncode(data.toJson()); final invokeResult = await methodChannelToNative.invokeMethod( - 'dataClassMethod', + 'MyTestModule_dataClassMethod', [dataSerialized], ); @@ -615,12 +630,12 @@ Future dataClassMethod(MyDataClass data) async { } Future suspendUnitMethod() async { - await methodChannelToNative.invokeMethod('suspendUnitMethod', []); + await methodChannelToNative.invokeMethod('MyTestModule_suspendUnitMethod', []); } Future suspendStringMethod() async { final invokeResult = await methodChannelToNative.invokeMethod( - 'suspendStringMethod', + 'MyTestModule_suspendStringMethod', [], ); diff --git a/example/src/commonMain/kotlin/com/example/flutterkmpexample/MyTestModule.kt b/example/src/commonMain/kotlin/com/example/flutterkmpexample/MyTestModule.kt index 1b773a5..4c7665e 100644 --- a/example/src/commonMain/kotlin/com/example/flutterkmpexample/MyTestModule.kt +++ b/example/src/commonMain/kotlin/com/example/flutterkmpexample/MyTestModule.kt @@ -64,6 +64,9 @@ class MyTestModule(coroutineScope: CoroutineScope) { @FlutterMethod fun boolMethod(value: Boolean): Boolean = value + @FlutterMethod + fun methodWithSameNameAsInOtherModule(value: String): String = value + @FlutterMethod fun parameterizedMethod( a: String, @@ -241,6 +244,9 @@ class MySecondTestModule(coroutineScope: CoroutineScope) { @FlutterMethod fun testMethod(): String = "Hello from Kotlin!!!" + @FlutterMethod + fun methodWithSameNameAsInOtherModule(value: Int): Int = value + @FlutterFlow val intEvents: Flow = _intState diff --git a/flutter-kmp-ksp/src/main/kotlin/de/voize/flutterkmp/ksp/processor/AndroidModuleGenerator.kt b/flutter-kmp-ksp/src/main/kotlin/de/voize/flutterkmp/ksp/processor/AndroidModuleGenerator.kt index 89f91fa..c1a1516 100644 --- a/flutter-kmp-ksp/src/main/kotlin/de/voize/flutterkmp/ksp/processor/AndroidModuleGenerator.kt +++ b/flutter-kmp-ksp/src/main/kotlin/de/voize/flutterkmp/ksp/processor/AndroidModuleGenerator.kt @@ -94,10 +94,18 @@ class AndroidModuleGenerator { CodeBlock.builder().apply { beginControlFlow("when (call.method)") flutterModule.flutterMethods.forEach { method -> - addMethodCodeBlock(method, wrappedModuleVarName) + addMethodCodeBlock( + method = method, + moduleName = flutterModule.moduleName, + wrappedModuleVarName = wrappedModuleVarName, + ) } flutterModule.flutterStateFlows.forEach { stateFlow -> - addStateFlowCodeBlock(stateFlow, wrappedModuleVarName) + addStateFlowCodeBlock( + stateFlow = stateFlow, + moduleName = flutterModule.moduleName, + wrappedModuleVarName = wrappedModuleVarName, + ) } addStatement("else -> result.notImplemented()") endControlFlow() @@ -115,119 +123,6 @@ class AndroidModuleGenerator { fileSpec.writeTo(codeGenerator, false) } - /** - * ``` - * "myFlow" -> { - * val arguments = call.arguments as List<*> - * val previous = arguments[0] as Int - * - * CoroutineScope(Dispatchers.Default).launch { - * val next = wrappedModule.counter.first { - * it != previous - * } - * result.success(next) - * } - * } - * ``` - */ - private fun CodeBlock.Builder.addStateFlowCodeBlock( - stateFlow: KSDeclaration, - wrappedModuleVarName: String, - resultStatement: (resultParameter: String) -> String = { "result.success($it)" }, - ) { - val flowTypeArgument = stateFlow.getStateFlowDeclarationFlowTypeArgument() - val parameters = when (stateFlow) { - is KSPropertyDeclaration -> emptyList() - is KSFunctionDeclaration -> stateFlow.parameters - else -> error("only property and function declaration allowed for @FlutterStateFlow") - } - - beginControlFlow("%S ->", stateFlow.simpleName.asString()) - - addStatement("val arguments = call.arguments as List<*>") - - if ( - flowTypeArgument.declaration.requiresSerialization() || - listOf( - "kotlinx.datetime.Instant", - "kotlinx.datetime.LocalDateTime", - "kotlinx.datetime.LocalDate", - "kotlinx.datetime.LocalTime", - "kotlin.time.Duration", - ).contains(flowTypeArgument.declaration.qualifiedName?.asString()) - ) { - addStatement("val previous = arguments[0] as String?") - } else { - addStatement( - "val previous = arguments[0] as %T", - flowTypeArgument.makeNullable().toTypeName(), - ) - } - - parameters.forEachIndexed { index, parameter -> - getKotlinDeserialization( - parameter.type.resolve(), - "arguments[${index + 1}]", - "param$index", - ) - } - - beginControlFlow( - "%T(%T.Default).%M", - CoroutineScope, - Dispatchers, - launch, - ) - - if (flowTypeArgument.declaration.requiresSerialization()) { - addStatement("val json = %T { encodeDefaults = true }", JsonClassName) - } - - when (stateFlow) { - is KSPropertyDeclaration -> beginControlFlow( - "val next = %N.%L.%M", - wrappedModuleVarName, - stateFlow.simpleName.asString(), - first, - ) - is KSFunctionDeclaration -> beginControlFlow( - "val next = %N.%L(%L).%M", - wrappedModuleVarName, - stateFlow.simpleName.asString(), - List(parameters.size) { index -> "param$index" }.joinToString(", "), - first, - ) - else -> error("only property and function declaration allowed for @FlutterStateFlow") - } - - if (flowTypeArgument.declaration.requiresSerialization()) { - addStatement("%L.encodeToString(it) != previous", "json") - }else if ( - listOf( - "kotlinx.datetime.Instant", - "kotlinx.datetime.LocalDateTime", - "kotlinx.datetime.LocalDate", - "kotlinx.datetime.LocalTime", - "kotlin.time.Duration", - ).contains(flowTypeArgument.declaration.qualifiedName?.asString()) - ) { - addStatement("%T.encodeToString(it) != previous", JsonClassName) - } else { - addStatement("it != previous") - } - endControlFlow() - - getKotlinSerialization( - flowTypeArgument.declaration, - "next", - "serializedNext", - "json" - ) - addStatement(resultStatement("serializedNext")) - - endControlFlow() - endControlFlow() - } } private val MethodCallHandler = ClassName("io.flutter.plugin.common.MethodChannel", "MethodCallHandler") diff --git a/flutter-kmp-ksp/src/main/kotlin/de/voize/flutterkmp/ksp/processor/DartGenerator.kt b/flutter-kmp-ksp/src/main/kotlin/de/voize/flutterkmp/ksp/processor/DartGenerator.kt index 845df8f..14224b1 100644 --- a/flutter-kmp-ksp/src/main/kotlin/de/voize/flutterkmp/ksp/processor/DartGenerator.kt +++ b/flutter-kmp-ksp/src/main/kotlin/de/voize/flutterkmp/ksp/processor/DartGenerator.kt @@ -54,7 +54,7 @@ final Stream<${flowTypeArgument.toTypeName()}> $propertyName = const EventChanne * * Future next(bool? previous) async { * return await methodChannelToNative.invokeMethod( - * 'myFlow', + * 'myModule_myFlow', * [subscriptionId, previous] * ); * } @@ -80,9 +80,9 @@ final Stream<${flowTypeArgument.toTypeName()}> $propertyName = const EventChanne * return streamController.stream.listen(onData); * } */ - private fun KSDeclaration.generateStateFlowMethod(): String { + private fun KSDeclaration.generateStateFlowMethod(moduleName: String): String { val propertyName = simpleName.asString() - val channelName = propertyName + val methodName = "${moduleName}_$propertyName" val flowTypeArgument = getStateFlowDeclarationFlowTypeArgument().toDartType() val parameters = when (this) { @@ -122,7 +122,7 @@ final Stream<${flowTypeArgument.toTypeName()}> $propertyName = const EventChanne Future<${intermediateType.nullable().toTypeName()}> next(${intermediateType.nullable().toTypeName()} previous) async { return await methodChannelToNative.invokeMethod<${intermediateType.toTypeName()}>( - '$channelName', + '$methodName', [${invokeMethodArguments.joinToString(", ")}] ); } @@ -155,9 +155,9 @@ final Stream<${flowTypeArgument.toTypeName()}> $propertyName = const EventChanne } - private fun KSFunctionDeclaration.generateMethod(): String { + private fun KSFunctionDeclaration.generateMethod(moduleName: String): String { val methodName = simpleName.asString() - val eventName = simpleName.asString() + val eventName = "${moduleName}_$methodName" val returnTypeNotNull = returnType?.resolve() ?: error("return type is not set") val dartParameters = parameters.map { (it.name?.asString() ?: error("parameter has no name")) to it.type.resolve().toDartType() @@ -245,8 +245,8 @@ class ${module.moduleName} { final methodChannelToNative = const MethodChannel("${module.moduleName}"); ${module.flutterFlows.joinToString("\n") { it.generateFlowMember(module.moduleName) }} - ${module.flutterStateFlows.joinToString("\n") { it.generateStateFlowMethod() }} - ${module.flutterMethods.joinToString("\n") { it.generateMethod() }} + ${module.flutterStateFlows.joinToString("\n") { it.generateStateFlowMethod(module.moduleName) }} + ${module.flutterMethods.joinToString("\n") { it.generateMethod(module.moduleName) }} } """.trimIndent() 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 0ed9c0a..f2de61d 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 @@ -98,8 +98,9 @@ class IOSKotlinModuleGenerator { beginControlFlow("return when (call.method)") flutterModule.flutterMethods.forEach { method -> addMethodCodeBlock( - method, - wrappedModuleVarName, + method = method, + moduleName = flutterModule.moduleName, + wrappedModuleVarName = wrappedModuleVarName, resultStatement = { resultParameter -> "result!!($resultParameter)" }, @@ -107,7 +108,15 @@ class IOSKotlinModuleGenerator { ) } flutterModule.flutterStateFlows.forEach { stateFlow -> - addStateFlowCodeBlock(stateFlow, wrappedModuleVarName) + addStateFlowCodeBlock( + stateFlow = stateFlow, + wrappedModuleVarName = wrappedModuleVarName, + moduleName = flutterModule.moduleName, + resultStatement = { resultParameter -> + "result!!($resultParameter)" + }, + append = { addStatement("true") } + ) } addStatement("else -> false") endControlFlow() @@ -124,125 +133,6 @@ class IOSKotlinModuleGenerator { fileSpec.writeTo(codeGenerator, false) } - - /** - * ``` - * "myFlow" -> { - * val arguments = call.arguments as List<*> - * val previous = arguments[0] as Int - * - * CoroutineScope(Dispatchers.Default).launch { - * val next = wrappedModule.counter.first { - * it != previous - * } - * result!!(next) - * } - * - * true - * } - * ``` - */ - private fun CodeBlock.Builder.addStateFlowCodeBlock( - stateFlow: KSDeclaration, - wrappedModuleVarName: String, - resultStatement: (resultParameter: String) -> String = { "result!!($it)" }, - ) { - val flowTypeArgument = stateFlow.getStateFlowDeclarationFlowTypeArgument() - val parameters = when (stateFlow) { - is KSPropertyDeclaration -> emptyList() - is KSFunctionDeclaration -> stateFlow.parameters - else -> error("only property and function declaration allowed for @FlutterStateFlow") - } - - beginControlFlow("%S ->", stateFlow.simpleName.asString()) - - addStatement("val arguments = call.arguments as List<*>") - - if ( - flowTypeArgument.declaration.requiresSerialization() || - listOf( - "kotlinx.datetime.Instant", - "kotlinx.datetime.LocalDateTime", - "kotlinx.datetime.LocalDate", - "kotlinx.datetime.LocalTime", - "kotlin.time.Duration", - ).contains(flowTypeArgument.declaration.qualifiedName?.asString()) - ) { - addStatement("val previous = arguments[0] as String?") - } else { - addStatement( - "val previous = arguments[0] as %T", - flowTypeArgument.makeNullable().toTypeName(), - ) - } - - parameters.forEachIndexed { index, parameter -> - getKotlinDeserialization( - parameter.type.resolve(), - "arguments[${index + 1}]", - "param$index", - ) - } - - beginControlFlow( - "%T(%T.Default).%M", - CoroutineScope, - Dispatchers, - launch, - ) - - if (flowTypeArgument.declaration.requiresSerialization()) { - addStatement("val json = %T { encodeDefaults = true }", JsonClassName) - } - - when (stateFlow) { - is KSPropertyDeclaration -> beginControlFlow( - "val next = %N.%L.%M", - wrappedModuleVarName, - stateFlow.simpleName.asString(), - first, - ) - is KSFunctionDeclaration -> beginControlFlow( - "val next = %N.%L(%L).%M", - wrappedModuleVarName, - stateFlow.simpleName.asString(), - List(parameters.size) { index -> "param$index" }.joinToString(", "), - first, - ) - else -> error("only property and function declaration allowed for @FlutterStateFlow") - } - - if (flowTypeArgument.declaration.requiresSerialization()) { - addStatement("%L.encodeToString(it) != previous", "json") - }else if ( - listOf( - "kotlinx.datetime.Instant", - "kotlinx.datetime.LocalDateTime", - "kotlinx.datetime.LocalDate", - "kotlinx.datetime.LocalTime", - "kotlin.time.Duration", - ).contains(flowTypeArgument.declaration.qualifiedName?.asString()) - ) { - addStatement("%T.encodeToString(it) != previous", JsonClassName) - } else { - addStatement("it != previous") - } - endControlFlow() - - getKotlinSerialization( - flowTypeArgument.declaration, - "next", - "serializedNext", - "json" - ) - addStatement(resultStatement("serializedNext")) - - endControlFlow() - - addStatement("true") - - endControlFlow() - } } private val ExperimentalForeignApi = ClassName("kotlinx.cinterop", "ExperimentalForeignApi") diff --git a/flutter-kmp-ksp/src/main/kotlin/de/voize/flutterkmp/ksp/processor/shared.kt b/flutter-kmp-ksp/src/main/kotlin/de/voize/flutterkmp/ksp/processor/shared.kt index c8dc6c1..5b3395d 100644 --- a/flutter-kmp-ksp/src/main/kotlin/de/voize/flutterkmp/ksp/processor/shared.kt +++ b/flutter-kmp-ksp/src/main/kotlin/de/voize/flutterkmp/ksp/processor/shared.kt @@ -2,6 +2,7 @@ package de.voize.flutterkmp.ksp.processor 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.KSType import com.google.devtools.ksp.symbol.KSValueParameter import com.google.devtools.ksp.symbol.Modifier @@ -11,13 +12,28 @@ import com.squareup.kotlinpoet.MemberName import com.squareup.kotlinpoet.ParameterSpec import com.squareup.kotlinpoet.ksp.toTypeName +/** + * ``` + * "moduleName_myMethod" -> { + * val resultData = wrappedModule.testMethod() + * val json = Json { encodeDefaults = true } + * val serializedResultData = resultData + * (serializedResultData) + * + * } + * ``` + */ internal fun CodeBlock.Builder.addMethodCodeBlock( method: KSFunctionDeclaration, + moduleName: String, wrappedModuleVarName: String, resultStatement: (resultParameter: String) -> String = { "result.success($it)" }, append: CodeBlock.Builder.() -> Unit = {}, ) { - beginControlFlow("%S ->", method.simpleName.asString()) + beginControlFlow( + "%S ->", + "${moduleName}_${method.simpleName.asString()}", + ) if (method.parameters.isNotEmpty()) { // @@ -123,6 +139,131 @@ internal fun CodeBlock.Builder.addMethodCodeBlock( endControlFlow() } + +/** + * ``` + * "myModule_myFlow" -> { + * val arguments = call.arguments as List<*> + * val previous = arguments[0] as Int + * + * CoroutineScope(Dispatchers.Default).launch { + * val next = wrappedModule.counter.first { + * it != previous + * } + * (next) + * } + * + * + * } + * ``` + */ +internal fun CodeBlock.Builder.addStateFlowCodeBlock( + stateFlow: KSDeclaration, + moduleName: String, + wrappedModuleVarName: String, + resultStatement: (resultParameter: String) -> String = { "result.success($it)" }, + append: CodeBlock.Builder.() -> Unit = {}, +) { + val flowTypeArgument = stateFlow.getStateFlowDeclarationFlowTypeArgument() + val parameters = when (stateFlow) { + is KSPropertyDeclaration -> emptyList() + is KSFunctionDeclaration -> stateFlow.parameters + else -> error("only property and function declaration allowed for @FlutterStateFlow") + } + + beginControlFlow( + "%S ->", + "${moduleName}_${stateFlow.simpleName.asString()}", + ) + + addStatement("val arguments = call.arguments as List<*>") + + if ( + flowTypeArgument.declaration.requiresSerialization() || + listOf( + "kotlinx.datetime.Instant", + "kotlinx.datetime.LocalDateTime", + "kotlinx.datetime.LocalDate", + "kotlinx.datetime.LocalTime", + "kotlin.time.Duration", + ).contains(flowTypeArgument.declaration.qualifiedName?.asString()) + ) { + addStatement("val previous = arguments[0] as String?") + } else { + addStatement( + "val previous = arguments[0] as %T", + flowTypeArgument.makeNullable().toTypeName(), + ) + } + + parameters.forEachIndexed { index, parameter -> + getKotlinDeserialization( + parameter.type.resolve(), + "arguments[${index + 1}]", + "param$index", + ) + } + + beginControlFlow( + "%T(%T.Default).%M", + CoroutineScope, + Dispatchers, + launch, + ) + + if (flowTypeArgument.declaration.requiresSerialization()) { + addStatement("val json = %T { encodeDefaults = true }", JsonClassName) + } + + when (stateFlow) { + is KSPropertyDeclaration -> beginControlFlow( + "val next = %N.%L.%M", + wrappedModuleVarName, + stateFlow.simpleName.asString(), + first, + ) + is KSFunctionDeclaration -> beginControlFlow( + "val next = %N.%L(%L).%M", + wrappedModuleVarName, + stateFlow.simpleName.asString(), + List(parameters.size) { index -> "param$index" }.joinToString(", "), + first, + ) + else -> error("only property and function declaration allowed for @FlutterStateFlow") + } + + if (flowTypeArgument.declaration.requiresSerialization()) { + addStatement("%L.encodeToString(it) != previous", "json") + } else if ( + listOf( + "kotlinx.datetime.Instant", + "kotlinx.datetime.LocalDateTime", + "kotlinx.datetime.LocalDate", + "kotlinx.datetime.LocalTime", + "kotlin.time.Duration", + ).contains(flowTypeArgument.declaration.qualifiedName?.asString()) + ) { + addStatement("%T.encodeToString(it) != previous", JsonClassName) + } else { + addStatement("it != previous") + } + endControlFlow() + + getKotlinSerialization( + flowTypeArgument.declaration, + "next", + "serializedNext", + "json" + ) + addStatement(resultStatement("serializedNext")) + + endControlFlow() + + append() + + endControlFlow() +} + internal fun CodeBlock.Builder.getKotlinSerialization( declaration: KSDeclaration, varName: String,