Skip to content

Commit

Permalink
Use cinterop with stub headers instead of Flutter cocoapod dependency
Browse files Browse the repository at this point in the history
This way a KMP project using this library does not have to declare Flutter as a pod dependency.
The Flutter symbols are then eventually resolved when building the final Flutter app which has the Flutter binaries.
  • Loading branch information
erksch committed Feb 28, 2025
1 parent c99002b commit c6bd77f
Show file tree
Hide file tree
Showing 11 changed files with 123 additions and 68 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

## unreleased

- Use cinterop with stub headers instead of Flutter cocoapod dependency

## v0.1.0-rc.3

- Fix conflicts with same method names across different modules on iOS by prefixing method names
Expand Down
4 changes: 2 additions & 2 deletions example/FlutterKmpExample.podspec
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@ Pod::Spec.new do |spec|
spec.summary = 'Shared Kotlin code for flutter-kmp example'
spec.vendored_frameworks = 'build/cocoapods/framework/flutterkmpexample.framework'
spec.libraries = 'c++'
spec.ios.deployment_target = '11.0'
spec.dependency 'Flutter'

if !Dir.exist?('build/cocoapods/framework/flutterkmpexample.framework') || Dir.empty?('build/cocoapods/framework/flutterkmpexample.framework')
raise "
Expand Down
13 changes: 4 additions & 9 deletions example/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -33,16 +33,11 @@ kotlin {
homepage = "https://github.com/voize-gmbh/flutter-kmp"
summary = "Shared Kotlin code for flutter-kmp example"
baseName = "flutterkmpexample"
}

// We can not use a specific version here, because the podspec generated by this KMP project
// will be referenced by the Flutter plugin podspec and therefore the Flutter target app.
// The target app also depends on Flutter but the Flutter.podspec it uses has a stub version (1.0.0).
// So specifying a version here will create a conflict and instead we have to rely on the assumption
// that the interop for the Flutter in the Cocoapods registry is compatible with the Flutter version used in the target app.
pod("Flutter")

ios.deploymentTarget = "11.0"
// without setting the framework to static, you will get errors during linking
// that symbols e.g. for _OBJC_CLASS_$_FlutterError are missing
isStatic = true
}
}

sourceSets {
Expand Down
5 changes: 4 additions & 1 deletion example/flutter/example/ios/Podfile
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,10 @@ pod 'FlutterKmpExample', :path => '../../../FlutterKmpExample.podspec'
flutter_ios_podfile_setup

target 'Runner' do
use_frameworks!
# use_frameworks needs to be disabled because the KMP project produces a static library.
# Otherwise you get the following error:
# [!] The 'Pods-Runner' target has transitive dependencies that include statically linked binaries
# use_frameworks!
use_modular_headers!

flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__))
Expand Down
7 changes: 3 additions & 4 deletions example/flutter/example/ios/Podfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,7 @@ PODS:
- flutter_kmp_example (0.0.1):
- Flutter
- FlutterKmpExample
- FlutterKmpExample (0.1.0):
- Flutter
- FlutterKmpExample (0.1.0)
- integration_test (0.0.1):
- Flutter

Expand All @@ -27,9 +26,9 @@ EXTERNAL SOURCES:
SPEC CHECKSUMS:
Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7
flutter_kmp_example: cef86282548c3343df655b50da93fb23981c5cfd
FlutterKmpExample: 602f302c485b58e590be3d655f462b4962cdc4e3
FlutterKmpExample: 353b38380378eaf5b71cee500a84dae03fa23f7d
integration_test: 4a889634ef21a45d28d50d622cf412dc6d9f586e

PODFILE CHECKSUM: 53af147c3b0053711becea2df6d246d410fe58ba
PODFILE CHECKSUM: f0f94c29f034beaf74fec4fb487a5217cd1d58cc

COCOAPODS: 1.16.2
34 changes: 8 additions & 26 deletions example/flutter/example/ios/Runner.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,15 @@
objects = {

/* Begin PBXBuildFile section */
006073E245D621291EBBB998 /* Pods_RunnerTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 0CDE66A62BBA07E3AE5C5429 /* Pods_RunnerTests.framework */; };
1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; };
206231127C31445336DDE1DB /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 8F1AD645DB26993E19F0D8C8 /* Pods_Runner.framework */; };
331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C807B294A618700263BE5 /* RunnerTests.swift */; };
3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; };
74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; };
97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; };
97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; };
97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; };
C1936D0039B65844EC5CA124 /* libPods-Runner.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 685F6077958C4350FF9DFF6A /* libPods-Runner.a */; };
C9E959614061D6948972AB93 /* libPods-RunnerTests.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 3B383076F90ABE4E6305BF5A /* libPods-RunnerTests.a */; };
/* End PBXBuildFile section */

/* Begin PBXContainerItemProxy section */
Expand Down Expand Up @@ -44,16 +44,16 @@
/* Begin PBXFileReference section */
00218362643D29004B31737F /* Pods-RunnerTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.release.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.release.xcconfig"; sourceTree = "<group>"; };
0A32F207720B77AD414F285A /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = "<group>"; };
0CDE66A62BBA07E3AE5C5429 /* Pods_RunnerTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_RunnerTests.framework; sourceTree = BUILT_PRODUCTS_DIR; };
1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = "<group>"; };
1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = "<group>"; };
331C807B294A618700263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = "<group>"; };
331C8081294A63A400263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
3B383076F90ABE4E6305BF5A /* libPods-RunnerTests.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-RunnerTests.a"; sourceTree = BUILT_PRODUCTS_DIR; };
3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = "<group>"; };
685F6077958C4350FF9DFF6A /* libPods-Runner.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-Runner.a"; sourceTree = BUILT_PRODUCTS_DIR; };
74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = "<group>"; };
74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = "<group>"; };
8F1AD645DB26993E19F0D8C8 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; };
9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = "<group>"; };
9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = "<group>"; };
97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; };
Expand All @@ -72,15 +72,15 @@
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
006073E245D621291EBBB998 /* Pods_RunnerTests.framework in Frameworks */,
C9E959614061D6948972AB93 /* libPods-RunnerTests.a in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
97C146EB1CF9000F007C117D /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
206231127C31445336DDE1DB /* Pods_Runner.framework in Frameworks */,
C1936D0039B65844EC5CA124 /* libPods-Runner.a in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
Expand All @@ -90,8 +90,8 @@
11B9E75F12A0C9BACF0999C2 /* Frameworks */ = {
isa = PBXGroup;
children = (
8F1AD645DB26993E19F0D8C8 /* Pods_Runner.framework */,
0CDE66A62BBA07E3AE5C5429 /* Pods_RunnerTests.framework */,
685F6077958C4350FF9DFF6A /* libPods-Runner.a */,
3B383076F90ABE4E6305BF5A /* libPods-RunnerTests.a */,
);
name = Frameworks;
sourceTree = "<group>";
Expand Down Expand Up @@ -198,7 +198,6 @@
97C146EC1CF9000F007C117D /* Resources */,
9705A1C41CF9048500538489 /* Embed Frameworks */,
3B06AD1E1E4923F5004D2608 /* Thin Binary */,
51D6551D7D0BBD8C3EB7BB8F /* [CP] Embed Pods Frameworks */,
);
buildRules = (
);
Expand Down Expand Up @@ -308,23 +307,6 @@
shellPath = /bin/sh;
shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin";
};
51D6551D7D0BBD8C3EB7BB8F /* [CP] Embed Pods Frameworks */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist",
);
name = "[CP] Embed Pods Frameworks";
outputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist",
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n";
showEnvVarsInLog = 0;
};
74898FE0835FA79AEE6DF9BF /* [CP] Check Pods Manifest.lock */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -138,11 +138,11 @@ class IOSKotlinModuleGenerator {
private val ExperimentalForeignApi = ClassName("kotlinx.cinterop", "ExperimentalForeignApi")
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 FlutterResult = ClassName("flutter", "FlutterResult")
private val FlutterMethodCall = ClassName("flutter", "FlutterMethodCall")
private val FlutterMethodChannel = ClassName("flutter", "FlutterMethodChannel")
private val FlutterEventChannel = ClassName("flutter", "FlutterEventChannel")
private val FlutterPluginRegistrar = ClassName("flutter", "FlutterPluginRegistrarProtocol")
private val FlutterPlugin = ClassName("flutter", "FlutterPluginProtocol")
private val FlutterStandardMethodCodec = ClassName("flutter", "FlutterStandardMethodCodec")
private val toEventStreamHandler = MemberName(flutterKmpPackageName, "toEventStreamHandler")
26 changes: 12 additions & 14 deletions flutter-kmp/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinNativeTarget

plugins {
kotlin("multiplatform")
kotlin("plugin.serialization")
kotlin("native.cocoapods")
id("com.android.library")
}

Expand Down Expand Up @@ -30,21 +31,18 @@ kotlin {
publishLibraryVariants("release")
}

cocoapods {
// this cocoapods dependency is needed for the iOS EventStreamHandler utils
pod("Flutter")

// we do not need to create a podspec file because the KMP projects that use this library
// must produce a podspec that contains the Flutter cocoapod dependency
noPodspec()

ios.deploymentTarget = "11.0"
fun KotlinNativeTarget.configureFlutterInterop() {
val main by compilations.getting {
val flutter by cinterops.creating {
includeDirs("src/nativeInterop/cinterop/")
packageName("flutter")
}
}
}


iosX64()
iosArm64()
iosSimulatorArm64()
iosX64 { configureFlutterInterop() }
iosArm64 { configureFlutterInterop() }
iosSimulatorArm64 { configureFlutterInterop() }
wasmJs { nodejs() }

applyDefaultHierarchyTemplate()
Expand Down
9 changes: 4 additions & 5 deletions flutter-kmp/src/iosMain/kotlin/EventStreamHandlerUtils.kt
Original file line number Diff line number Diff line change
@@ -1,9 +1,6 @@
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
Expand All @@ -12,7 +9,9 @@ import kotlinx.coroutines.withContext
import kotlinx.serialization.SerializationStrategy
import kotlinx.serialization.serializer
import platform.darwin.NSObject

import flutter.FlutterStreamHandlerProtocol
import flutter.FlutterEventSink
import flutter.FlutterError

inline fun <reified T> Flow<T>.toEventStreamHandler(): NSObject =
toEventStreamHandler(serializer<T>())
Expand Down Expand Up @@ -45,4 +44,4 @@ fun <T> Flow<T>.toEventStreamHandler(serializer: SerializationStrategy<T>): NSOb
return null
}
}
}
}
74 changes: 74 additions & 0 deletions flutter-kmp/src/nativeInterop/cinterop/Flutter/FlutterPlugin.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@

// https://github.com/flutter/engine/blob/3.22.2/shell/platform/darwin/ios/framework/Headers/FlutterPlugin.h

#import <Foundation/Foundation.h>

// https://github.com/flutter/engine/blob/3.22.2/shell/platform/darwin/common/framework/Headers/FlutterBinaryMessenger.h#L49C1-L49C44
@protocol FlutterBinaryMessenger <NSObject>
@end

// https://github.com/flutter/engine/blob/3.22.2/shell/platform/darwin/common/framework/Headers/FlutterChannels.h#L194
typedef void (^FlutterResult)(id _Nullable result);

// https://github.com/flutter/engine/blob/3.22.2/shell/platform/darwin/common/framework/Headers/FlutterCodecs.h#L391
@protocol FlutterMethodCodec
+ (instancetype)sharedInstance;
@end

// https://github.com/flutter/engine/blob/3.22.2/shell/platform/darwin/common/framework/Headers/FlutterCodecs.h#L18C1-L18C30
@protocol FlutterMessageCodec
+ (instancetype)sharedInstance;
@end

// https://github.com/flutter/engine/blob/3.22.2/shell/platform/darwin/common/framework/Headers/FlutterChannels.h#L220
@interface FlutterMethodChannel : NSObject
- (instancetype)initWithName:(NSString*)name
binaryMessenger:(NSObject<FlutterBinaryMessenger>*)messenger
codec:(NSObject<FlutterMessageCodec>*)codec;
@end

// https://github.com/flutter/engine/blob/3.22.2/shell/platform/darwin/common/framework/Headers/FlutterChannels.h#L350
typedef void (^FlutterEventSink)(id _Nullable event);

// https://github.com/flutter/engine/blob/3.22.2/shell/platform/darwin/common/framework/Headers/FlutterCodecs.h#L246C1-L246C35
@interface FlutterError : NSObject
+ (instancetype)errorWithCode:(NSString*)code
message:(NSString* _Nullable)message
details:(id _Nullable)details;
@end

// https://github.com/flutter/engine/blob/3.22.2/shell/platform/darwin/common/framework/Headers/FlutterChannels.h#L356
@protocol FlutterStreamHandler <NSObject>
- (FlutterError* _Nullable)onListenWithArguments:(id _Nullable)arguments
eventSink:(FlutterEventSink)events;
- (FlutterError* _Nullable)onCancelWithArguments:(id _Nullable)arguments;
@end

// https://github.com/flutter/engine/blob/3.22.2/shell/platform/darwin/common/framework/Headers/FlutterChannels.h#L400C1-L400C42
@interface FlutterEventChannel : NSObject
- (instancetype)initWithName:(NSString*)name
binaryMessenger:(NSObject<FlutterBinaryMessenger>*)messenger
codec:(NSObject<FlutterMethodCodec>*)codec;
- (void)setStreamHandler:(NSObject<FlutterStreamHandler>* _Nullable)handler;
@end

// https://github.com/flutter/engine/blob/3.22.2/shell/platform/darwin/ios/framework/Headers/FlutterPlugin.h#L189
@protocol FlutterPlugin <NSObject>
@end

// https://github.com/flutter/engine/blob/3.22.2/shell/platform/darwin/ios/framework/Headers/FlutterPlugin.h#L283
@protocol FlutterPluginRegistrar <NSObject>
- (NSObject<FlutterBinaryMessenger>*)messenger;
- (void)addMethodCallDelegate:(NSObject<FlutterPlugin>*)delegate
channel:(FlutterMethodChannel*)channel;
@end

// https://github.com/flutter/engine/blob/3.22.2/shell/platform/darwin/common/framework/Headers/FlutterCodecs.h#L220
@interface FlutterMethodCall : NSObject
@property(readonly, nonatomic) NSString* method;
@property(readonly, nonatomic, nullable) id arguments;
@end

// https://github.com/flutter/engine/blob/3.22.2/shell/platform/darwin/common/framework/Headers/FlutterCodecs.h#L469
@interface FlutterStandardMethodCodec : NSObject <FlutterMethodCodec>
@end
3 changes: 3 additions & 0 deletions flutter-kmp/src/nativeInterop/cinterop/flutter.def
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
language = Objective-C
headers = Flutter/FlutterPlugin.h
headerFilter = Flutter/*

0 comments on commit c6bd77f

Please sign in to comment.