2023 最新版将 Flutter module 集成到 iOS 项目

1,635 阅读11分钟

简介

本文记录我在 2023.09 对将 Flutter module 集成到 iOS项目的探索过程,结合中文官方文档及网络搜索的结果后的实践内容。共有四种方法,包含官方文档的三种和开发者们扩展出的第四种。

系统要求

你的开发环境必须满足 Flutter 对 macOS 系统的版本要求 并 已经安装 Xcode,Flutter 支持 iOS 11 及以上。此外,你还需要 1.10 或以上版本的 CocoaPods

准备

由于要进行多种方式测试,避免混乱,先创建一个父文件夹 iOS_flutter_mix ,再建四个子文件夹 a、b、c、d 分别进行测试,不至于在同一个项目上测试出现混乱,或到最后只能看到最终测试的结果,前边的都被覆盖了。

开始实验

方式 A,使用 CocoaPods 和 Flutter SDK 集成

这个方式需要项目的所有开发者本地都有 Flutter 环境。你的工程在每次构建的的时候,都将会从源码里编译 Flutter 模块。

  1. 在 a 文件夹下创建 iOS 工程和 Flutter module,使用命令行或者IDE操作都可以
  2. 在 iOS 工程下创建 Podfile 文件
cd iOS_flutter_mix/a/MyApp
pod init

此时路径目录如下:

a
├── MyApp
│   ├── MyApp
│   ├── MyApp.xcodeproj
│   ├── MyAppTests
│   ├── MyAppUITests
│   └── Podfile
└── my_flutter
    ├── .android
    ├── .dart_tool
    ├── .gitignore
    ├── .idea
    ├── .ios
    ├── .metadata
    ├── README.md
    ├── analysis_options.yaml
    ├── lib
    ├── my_flutter.iml
    ├── my_flutter_android.iml
    ├── pubspec.lock
    ├── pubspec.yaml
    └── test
  1. 在 Podfile 中添加下面代码:
flutter_application_path = '../my_flutter'
load File.join(flutter_application_path, '.ios', 'Flutter', 'podhelper.rb')
  1. 每个需要集成 Flutter 的 Podfile target,执行 install_all_flutter_pods(flutter_application_path)
target 'MyApp' do
  install_all_flutter_pods(flutter_application_path)
end
  1. 在 Podfile 的 post_install 部分调用 flutter_post_install(installer)
post_install do |installer|
  flutter_post_install(installer) if defined?(flutter_post_install)
end

此时 Podfile 文件内容如下

# Uncomment the next line to define a global platform for your project
platform :ios, '12.0'

flutter_application_path = '../my_flutter'
load File.join(flutter_application_path, '.ios', 'Flutter', 'podhelper.rb')

target 'MyApp' do
  # Comment the next line if you don't want to use dynamic frameworks
  use_frameworks!

  install_all_flutter_pods(flutter_application_path)

  post_install do |installer|
    flutter_post_install(installer) if defined?(flutter_post_install)
  end

end

flutter_post_install 方法(Flutter 3.1.0 中新增的)增加了原生 Apple Silicon arm64 iOS 模拟器的支持。它包括 if defined?(flutter_post_install) 的检查以确保你的 Podfile 在旧版本的没有该方法的 Flutter 上也能正常运行。

  1. 运行 pod install。Flutter module 就会通过 pod 链接到 iOS 工程中,如图:

截屏2023-09-21 17.46.44.png

当你在 my_flutter/pubspec.yaml 改变了 Flutter plugin 依赖,需要在 Flutter module 目录运行 flutter pub get,来更新会被podhelper.rb 脚本用到的 plugin 列表,然后再次在你的应用目录 /MyApp 运行 pod install.

  1. 打开/MyApp/MyApp.xcworkspace,在iOS项目中测试效果,编写如下代码(非官方推荐标准写法,只为测试是否集成成功)

截屏2023-09-21 18.35.19.png

方式 B,在 Xcode 中集成 frameworks

除了上面的方法,你也可以创建必备的 frameworks,手动修改既有 Xcode 项目,将他们集成进去。当你组内其它成员们不能在本地安装 Flutter SDK 和 CocoaPods,或者你不想使用 CocoaPods 作为既有应用的依赖管理时,这种方法会比较合适。但是每当你在 Flutter module 中改变了代码,都必须运行 flutter build ios-framework

  1. 在 b 文件夹下分别创建 iOS 工程、Flutter module,再在 iOS 工程目录下加一个 flutter_lib 文件夹备用。 目录如下所示:
.
├── MyApp
│   ├── MyApp
│   │   ├── AppDelegate.swift
│   │   ├── Assets.xcassets
│   │   ├── Base.lproj
│   │   ├── Info.plist
│   │   ├── SceneDelegate.swift
│   │   └── ViewController.swift
│   ├── MyApp.xcodeproj
│   │   ├── project.pbxproj
│   │   ├── project.xcworkspace
│   │   └── xcuserdata
│   ├── MyAppTests
│   │   └── MyAppTests.swift
│   ├── MyAppUITests
│   │   ├── MyAppUITests.swift
│   │   └── MyAppUITestsLaunchTests.swift
│   └── flutter_lib
└── my_flutter
    ├── README.md
    ├── analysis_options.yaml
    ├── build
    │   └── ios
    ├── lib
    │   └── main.dart
    ├── my_flutter.iml
    ├── my_flutter_android.iml
    ├── pubspec.lock
    ├── pubspec.yaml
    └── test
        └── widget_test.dart
  1. 在 Flutter module 目录下运行 flutter build ios-framework
cd iOS_flutter_mix/b/my_flutter
flutter build ios-framework

编译产物在/b/my_flutter/build/ios/framework中,也可以使用 flutter build ios-framework --output=b/MyApp/flutter_lib/ 把编译产物输出到指定文件夹

flutter build ios-framework --output=../MyApp/flutter_lib/
  1. 在 Xcode 中将生成的 frameworks 集成到你的既有应用中。例如,你可以在 some/path/MyApp/Flutter/Debug/ 目录拖拽 frameworks 到你的应用 target 编译设置的 General > Frameworks, Libraries, and Embedded Content 下,然后在 Embed 下拉列表中选择 “Embed & Sign”。
  2. 打开/MyApp/MyApp.xcodeproj,编写如方式 A 中的代码运行,会看到一样的效果。
  3. 由于编译 flutter 得到的产物既有动态框架,也有静态框架,这两种集成到 iOS 项目中有不同的方式,具体可查看官方文档选择链接到框架 或者 内嵌框架

方式 C,使用 CocoaPods 在 Xcode 和 Flutter 框架中内嵌应用和插件框架

除了将一个很大的 Flutter.framework 分发给其他开发者、机器或者持续集成 (CI) 系统之外,你可以加入一个参数 --cocoapods 将 Flutter 框架作为一个 CocoaPods 的 podspec 文件分发。这将会生成一个 Flutter.podspec 文件而不再生成 Flutter.framework 引擎文件。

  1. 在 c 文件夹下分别创建 iOS 工程、Flutter module,再在 iOS 工程目录下加一个 flutter_lib 文件夹备用。
.
├── MyApp
│   ├── MyApp
│   │   ├── AppDelegate.swift
│   │   ├── Assets.xcassets
│   │   ├── Base.lproj
│   │   ├── Info.plist
│   │   ├── SceneDelegate.swift
│   │   └── ViewController.swift
│   ├── MyApp.xcodeproj
│   │   ├── project.pbxproj
│   │   ├── project.xcworkspace
│   │   └── xcuserdata
│   ├── MyAppTests
│   │   └── MyAppTests.swift
│   ├── MyAppUITests
│   │   ├── MyAppUITests.swift
│   │   └── MyAppUITestsLaunchTests.swift
│   └── flutter_lib
└── my_flutter
    ├── .android
    │   ├── Flutter
    │   ├── app
    │   ├── build.gradle
    │   ├── gradle
    │   ├── gradle.properties
    │   ├── gradlew
    │   ├── gradlew.bat
    │   ├── include_flutter.groovy
    │   ├── local.properties
    │   ├── settings.gradle
    │   └── src
    ├── .dart_tool
    │   ├── package_config.json
    │   ├── package_config_subset
    │   └── version
    ├── .gitignore
    ├── .idea
    │   ├── libraries
    │   ├── modules.xml
    │   └── workspace.xml
    ├── .ios
    │   ├── Config
    │   ├── Flutter
    │   ├── Runner
    │   ├── Runner.xcodeproj
    │   └── Runner.xcworkspace
    ├── .metadata
    ├── README.md
    ├── analysis_options.yaml
    ├── lib
    │   └── main.dart
    ├── my_flutter.iml
    ├── my_flutter_android.iml
    ├── pubspec.lock
    ├── pubspec.yaml
    └── test
        └── widget_test.dart
  1. 这次为了演示与之前不同的情况,在 flutter 的 pubspec.yaml 中添加其他三方库的引用。
dependencies:
  flutter:
    sdk: flutter

  # The following adds the Cupertino Icons font to your application.
  # Use with the CupertinoIcons class for iOS style icons.
  cupertino_icons: ^1.0.2

  # 添加数据持久化插件  https://pub.flutter-io.cn/packages/shared_preferences
  shared_preferences: ^0.5.4+3

然后执行 pub get。 3. 要生成 Flutter.podspec 和框架,命令行切换到 Flutter module 根目录,然后运行以下命令:

cd iOS_flutter_mix/c/my_flutter
flutter build ios-framework --cocoapods --output=../MyApp/flutter_lib/

此时 flutter_lib 目录如下:

.
├── Debug
│   ├── App.xcframework
│   ├── Flutter.podspec
│   ├── FlutterPluginRegistrant.xcframework(只有当你有带有iOS平台代码的插件时)
│   └── shared_preferences.xcframework(每个插件都是独立的框架)
├── Profile
│   ├── App.xcframework
│   ├── Flutter.podspec
│   ├── FlutterPluginRegistrant.xcframework
│   └── shared_preferences.xcframework
└── Release
    ├── App.xcframework
    ├── Flutter.podspec
    ├── FlutterPluginRegistrant.xcframework
    └── shared_preferences.xcframework

shared_preferences.xcframework 是 flutter 对应其他平台的数据存储插件,例如 iOS 中的 NSUserDefaults FlutterPluginRegistrant.xcframework 是当 flutter 中有与 iOS 平台相关的插件时生成的,用于在 flutter 与 iOS 平台交互时使用。

  1. 创建 pod 并添加依赖,使用 CocoaPods 的宿主应用程序可以将 Flutter 添加到 Podfile 中,
cd MyApp
pod init
pod 'Flutter', :podspec => './flutter_lib/[build mode]/Flutter.podspec'

你必须选择相应的构建模式进行硬编码,将构建模式的值写在上面指令中的 [build mode] 位置。例如,你需要 flutter attach 的时候,应该使用 Debug,在你准备发布版本的时候,应该使用 Release。 然后

pod install

此时 Flutter.xcframework 已被 pod 管理,其他的框架像方式 B 那样链接并嵌入到你现有的应用程序中。

  1. 如上述方式在 iOS 项目中添加代码测试。

方式 D,优化改动方式 C,把所有框架都用 pod 来管理。

方式 C 中只对 Flutter.xcframework 进行 pod 管理,其他还是需要手动添加,不太方便。所以我们准备自建 pod 库,把所有框架都放进一个 pod 库中,然后添加到 iOS 项目中。

  1. 在 d 文件夹下创建 Flutter module、iOS 项目和 flutter_lib_pod 文件夹。
  2. 在 d 文件夹下执行 pod lib create flutter_lib_pod 命令,然后回答几个问题,根据你需求来就行。
cd d
pod lib create flutter_lib_pod

Cloning `https://github.com/CocoaPods/pod-template.git` into `flutter_lib_pod`.
Configuring flutter_lib_pod template.
security: SecKeychainSearchCopyNext: The specified item could not be found in the keychain.
------------------------------
To get you started we need to ask a few questions, this should only take a minute.
If this is your first time we recommend running through with the guide: 
 - https://guides.cocoapods.org/making/using-pod-lib-create.html
 ( hold cmd and double click links to open in a browser. )
What platform do you want to use?? [ iOS / macOS ]
 > 
ios
What language do you want to use?? [ Swift / ObjC ]
 > 
swift
Would you like to include a demo application with your library? [ Yes / No ]
 > no
Which testing frameworks will you use? [ Quick / None ]
 > none
Would you like to do view based testing? [ Yes / No ]
 > no

此时, flutter_lib_pod 文件夹如下

截屏2023-09-25 11.13.23.png

  1. 在 d/flutter_lib_pod 中新建文件夹 ios_frameworks 备用,然后来到 d/my_flutter 里创建一个 build_and_move_file.sh 脚本文件,用来编译 flutter 并把编译出的框架移动到 d/flutter_lib_pod/ios_frameworks 中添加到 pod 库里。 脚本文件参考网友写的并做了修改
if [ -z $out ]; then
    out='ios_frameworks'
fi

echo "准备输出所有文件到目录: $out"

echo "清除所有已编译文件"
find . -d -name build | xargs rm -rf
flutter clean
rm -rf $out
rm -rf build

flutter packages get

addFlag(){
    cat .ios/Podfile > tmp1.txt
    echo "use_frameworks!" >> tmp2.txt
    cat tmp1.txt >> tmp2.txt
    cat tmp2.txt > .ios/Podfile
    rm tmp1.txt tmp2.txt
}
echo "检查 .ios/Podfile文件状态"
a=$(cat .ios/Podfile)
if [[ $a == use* ]]; then
    echo '已经添加use_frameworks, 不再添加'
else
    echo '未添加use_frameworks,准备添加'
    addFlag
    echo "添加use_frameworks 完成"
fi

echo "编译flutter"

# 得到的是融合了模拟器和真机环境的 xcframework,默认包含 Debug、Profile、Release
flutter build ios-framework

# 下方这几种编译模式得到的是 framework,模拟器和真机分开的
#flutter build ios --debug --no-codesign
#flutter build ios --simulator --no-codesign
#flutter build ios --release --no-codesign

echo "编译flutter完成"


#创建输出路径
mkdir $out
#mkdir $out/Debug
#mkdir $out/Profile
#mkdir $out/Release

cp -r build/ios/framework $out
#也可以单独拷贝
#cp -r build/ios/framework/Debug/*.xcframework $out/Debug
#cp -r build/ios/framework/Profile/*.xcframework $out/Profile
#cp -r build/ios/framework/Release/*.xcframework $out/Release

#debug
#cp -r build/ios/Debug-iphoneos/*.framework $out
#cp -r build/ios/Debug-iphoneos/*/*.framework $out
#release
#cp -r build/ios/Release-iphoneos/*/*.framework $out
#cp -r build/ios/Release-iphoneos/*.framework $out


#网友的写法是这样的,但是现在路径变动了,已经不在这儿了
#cp -r .ios/Flutter/App.framework $out
#cp -r .ios/Flutter/engine/Flutter.framework $out



echo "复制framework库到临时文件夹: $out"

libpath='../flutter_lib_pod/'

rm -rf "$libpath/ios_frameworks"
mkdir $libpath
cp -r $out $libpath

echo "复制库文件到: $libpath"
  1. 执行build_and_move_file.sh 脚本文件
cd /d/my_flutter
sh build_and_move_file.sh

d/my_flutter/build/ios/framework 中的三种模式下的 xcframework 都导入到了 d/flutter_lib_pod/ios_frameworks/framework

  1. 编辑 d/flutter_lib_pod/flutter_lib_pod.podspec 文件,在 end 前一行添加代码,添加后整体如下(避免看着杂乱,已删除部分注释)
Pod::Spec.new do |s|
  s.name             = 'flutter_lib_pod'
  s.version          = '0.1.0'
  s.summary          = 'A short description of flutter_lib_pod.'


  s.description      = <<-DESC
TODO: Add long description of the pod here.
                       DESC

  s.homepage         = 'https://xxx/flutter_lib_pod'
  s.license          = { :type => 'MIT', :file => 'LICENSE' }
  s.author           = { 'xxx' => 'xxx' }
  s.source           = { :git => 'xxx/flutter_lib_pod.git', :tag => s.version.to_s }

  s.ios.deployment_target = '12.0'


  # 添加编译出来的依赖库
  s.static_framework = true
  p = Dir::open("ios_frameworks/framework")
  arr = Array.new
  arr.push('ios_frameworks/framework/Release/*.xcframework')
  s.ios.vendored_frameworks = arr

end

注意到这里是把 Release 模式下的框架给添加进来了,为了方便区分管理,我们可以把 flutter_lib_pod.podspec 文件拷贝一份命名为 flutter_lib_pod_debug.podspec,并修改内容对应为 Debug

Pod::Spec.new do |s|
  # name需要修改为flutter_lib_pod_debug
  s.name             = 'flutter_lib_pod_debug'
  s.version          = '0.1.0'
  s.summary          = 'A short description of flutter_lib_pod.'


  s.description      = <<-DESC
TODO: Add long description of the pod here.
                       DESC

  s.homepage         = 'https://xxx/flutter_lib_pod'
  s.license          = { :type => 'MIT', :file => 'LICENSE' }
  s.author           = { 'xxx' => 'xxx' }
  s.source           = { :git => 'https://xxx/flutter_lib_pod.git', :tag => s.version.to_s }

  s.ios.deployment_target = '12.0'


  # 添加编译出来的依赖库
  s.static_framework = true
  p = Dir::open("ios_frameworks/framework")
  arr = Array.new
  # 路径需要修改为 ios_frameworks/framework/Debug/*.xcframework
  arr.push('ios_frameworks/framework/Debug/*.xcframework')
  s.ios.vendored_frameworks = arr

end
  1. 在MyApp项目中创建 Podfile 文件并添加 pod 引用
cd d/MyApp
pod init
platform :ios, '12.0'

target 'MyApp' do
  use_frameworks!


  pod 'flutter_lib_pod_debug', :path => '../flutter_lib_pod', :configurations => ['Debug']
  # pod 'flutter_lib_pod', :path => '../flutter_lib_pod', :configurations => ['Release']

end

执行

pod install
  1. 打开 MyApp.xcworkspace,可以观察到 框架已经导入到 iOS 项目中

截屏2023-09-25 16.00.50.png

  1. 编写测试代码,查看效果

test.gif

方式 E,终极解决方案。

把这个方式单独列出来,是觉得此方案可能是最适合的。既然方式 D 已经实现了通过本地 pod 库进行管理这些 xcframework,那么把这个 pod 库关联到远端,就实现了平常使用 pod 导入三方库那样便捷了,在多人开发中也不需要所有人都安装 flutter 环境。

  1. 在 github、gitlib 或其他平台上创建一个空的仓库备用。
  2. 新建文件夹 e、把方式 D 中的 my_flutter 和 flutter_lib_pod 拷贝过来,修改 flutter_lib_pod 下的 flutter_lib_pod_debug.podspec

image.png 这里项目的信息要和远端仓库保持一致。

  1. 把 flutter_lib_pod 与远程库关联起来
cd flutter_lib_pod
git remote add origin https://github.com/NothingLuo/flutter_lib_pod.git

然后使用命令行或者 Sourcetree 把本地文件推送到远端,新建一个 tag 0.1.0 用于测试。

image.png

  1. 新建或者拷贝上边的 iOS 项目,更改 Podfile 文件。
platform :ios, '12.0'

target 'MyApp' do
  use_frameworks!


  pod 'flutter_lib_pod_debug', :git => 'https://github.com/NothingLuo/flutter_lib_pod.git', :configurations => ['Debug']
  # pod 'flutter_lib_pod_debug', :path => '../flutter_lib_pod', :configurations => ['Debug']
  # pod 'flutter_lib_pod', :path => '../flutter_lib_pod', :configurations => ['Release']

end

如果使用拷贝过来的 iOS 项目,先清理一下 Podfile,再执行 pod install 把之前导入的库清除掉。

把 pod 库的 git 地址指向远端。 5. 执行 pod install 拉取远端库。

cd MyApp
pod install

耐心等待一下,那些 xcframework 文有点大,完成后打开 MyApp.xcworkspace

image.png 6. 同上述的几种方式,添加代码测试,效果相同。

对比总结

几种方式各有优劣,选择适合自己的使用即可。

A,使用最简单省事,不需要复杂配置,方便原生与 flutter 联调,也是官方推荐的方式;缺点就是团队开发的话需要每个 iOS 开发都需要在本地安装有 flutter 环境,flutter 和 iOS 原生代码还耦合在一起。

B,不需要每个开发者本地都安装 flutter 环境,由开发 flutter 的负责编译后把 framework 分发给 iOS 开发者手动集成进入工程,也不需要依赖 Cocoapods;但是每当你在 Flutter module 中改变了代码,都必须运行 flutter build ios-framework,然后重新给 iOS 开发 framework进行替换,也要注意发版时 debug 和 release 包的替换。

C,不推荐,只是使用 Cocoapods 管理了一个 Flutter.xcframework,其他的还是需要和方式 B 一样处理。

D,此方式也不需要每个 iOS 开发本地都有 flutter 环境,两端代码之间也不耦合,就是前期配置稍微复杂一点,配置完之后就会简单很多,需要 flutter 开发向 iOS 工程中通过命令导入框架。

E,其实就是方式 D 的远程版,把本地 pod 库关联到远程库,这个可以让 flutter 开发完运行脚本打包再把框架上传到远端 pod 库,iOS 开发自己去配置 Podfile 拉取。

但是除了方式 A,其他方式两端混合联调起来都比较麻烦,毕竟这几种都是导入的是静态或动态库,只能分开调试,不过模块之间没有太多依赖也就无所谓了。

tip:由于本人技术有限,且是第一次做将 Flutter module 集成到 iOS 项目,也做了大量测试,但整理过程中难免有所疏漏或错误,请谅解,谢谢。

后续开发

本篇只讲了 Flutter 与 iOS 项目之间的各种集成方式,后续的页面开发跳转可参考官方文档:在 iOS 应用中添加 Flutter 页面 - Flutter 中文文档 - Flutter 中文开发者网站 - Flutter 或其他文章。

致谢

本文参考以下文章,🙏

将 Flutter module 集成到 iOS 项目 - Flutter 中文文档 - Flutter 中文开发者网站 - Flutter

混合开发:flutter集成进iOS工程 - 掘金 (juejin.cn)

Flutter、iOS混合开发实践 - 掘金 (juejin.cn)