Flutter(三十九)-原生嵌入Flutter进行混合开发

2,663 阅读7分钟

「这是我参与2022首次更文挑战的第7天,活动详情查看:2022首次更文挑战

在上一节课我们演示了如何在Flutter中调用原生的界面,在我们日常开发过程中还有可能会在原生项目中,嵌入Flutter页面,虽然Flutter不推荐这样使用;接下来我们来实现以下,在原生项目中,嵌入Flutter页面;

创建Flutter Module

需要注意的是,如果我们的原生需要嵌入Flutter,那么Flutter就不能是一个单独的App,也就是我们创建的Flutter不能是一个Flutter App;如果你需要嵌入的Flutter已经作为App开发了,那么就需要移植代码;原生嵌入Flutter,我们的Flutter需要是一个Flutter Module

image.png

我们来看一下创建好之后的Flutter Module的工程目录:

image.png

在工程中,仍然会存在androidios的目录,这两个目录是为了让我们调试Flutter Module而创建的,并且他们是隐藏文件夹 (官方不建议在这两个文件夹中添加相关平台的代码,因为这里边的原生代码不会被打包,只推荐用来作为测试使用)Flutter Module的入口依然是main.dart;如果我们需要将原来Flutter App中的页面嵌入到原生中,那么就需要将Flutter App工程中的代码一直到lib文件夹下;

创建原生工程

我们在flutter_module同级目录下创建一个原生工程:

image.png

Flutter不能使用驼峰命名工程名字,原生可以使用驼峰命名

原生工程默认目录如下:

image.png

关联Flutter Module和原生

现在Flutter Module和原生工程都有了,我们想要将两个工程关联起来,就需要使用到CocoaPods;我们在原生工程NativeDemo文件夹下创建Podfile文件:

image.png

修改Podfile文件内容如下:

flutter_application_path = '../flutter_module' # flutter_module的相对路径
load File.join(flutter_application_path,'.iOS','Flutter','podhelper.rb') # 为iOS工程加载flutter_module这个Flutter工程

platform :ios, '9.0'

target 'NativeDemo' do
  install_all_flutter_pods(flutter_application_path) # 加载flutter依赖的相关库
  # Comment the next line if you don't want to use dynamic frameworks
  use_frameworks!

  # Pods for NativeDemo

end

接下来执行pod install命令:

image.png

接下来,我们在原生项目中,引用Flutter,确保项目能编译成功:

image.png

我们在ViewController类中添加两个按钮点击方法,实现如下:

image.png

运行结果如下:

iShot2021-12-18 15.19.07.gif

我们修改Flutter代码,然后先使用Android Studio运行一下Flutter Module工程,然后在此运行原生项目查看结果:

iShot2021-12-18 15.25.15.gif

原生项目中的Flutter页面发生改变;

Flutter引擎内存问题

我们在调用Flutter代码的时候,会发现内存暴涨的情况:

iShot2021-12-18 15.39.35.gif

在加载了Flutter页面之后,我们的内存一下子涨了将近80M,这是因为我们在加载Flutter页面的时候,需要初始化Flutter引擎;

image.png

在我们编译成功的app中,Frameworks目录下的Flutter.framework就是Flutter引擎,在运行时,其需要加载到内存中;而且我们发现,当前情况下,即使我们的Flutter页面消失了,内存依然没有相应的减少;更严重的是,当我们再次加载Flutter页面时,内存依然会再次暴涨:

image.png

我们没加载一次Flutter页面,内存都会响应的增长(内存泄漏了);

造成这种结果的原因是,我们每次加载Flutter页面的时候,都是初始化了一个新的FlutterViewController页面,也就是每一次都初始化了一次引擎(调用FlutterViewController实质上就是初始化渲染引擎); 这个问题我们稍后解决,我们继续按照开发流程向下继续;

显示不同的Flutter页面

我们在正常的开发流程中,很有可能两个按钮需要显示不同的Flutter页面,那么应该如何实现呢?这里有两种方式:

第一种:设置路由(不推荐)

使用FlutterViewControllersetInitialRoute方法初始化路由,代码如下:

image.png

这样,我们就在加载Flutter的时候,给Flutter路由传递了参数onetwo;接下来,Flutter页面最响应的修改:

image.png

Flutter中通过MyApp中的window.defaultRouteName获取原生传递的onetwo信息赋值给pageIndex,然后根据pageIndex显示不同的页面;效果如下:

iShot2021-12-18 16.26.49.gif

那么,在Flutter中我们如何返回原生页面呢?我们在Flutter中通过MethodChannel给原生传递消息:

image.png 接下来,针对原生代码优化;

我们在调用FlutterViewControllersetInitialroute方法传递路由的时候,我们从该方法的注释中可以意识到该方法是在初始化引擎之后调用的:

 * This method must be called immediately after `initWithProject` and has no
 * effect when using `initWithEngine` if the `FlutterEngine` has already been
 * run.

也就是说,我们初始化FlutterViewController的过程就是在初始化Flutter引擎,所以第一次调用Flutter页面的时候,会相对卡顿了一下;我们将原生代码修改如下:

image.png

  • 初始化FlutterEngine时一定要调用run方法来启动引擎;
  • 初始化FlutterViewController的时候,将引擎传递进去;
  • viewDidload中先调用一下引擎,提前初始化;防止第一次调用时卡顿的发生;

此时,我们来看一下运行效果:

iShot2021-12-18 17.05.14.gif

iShot2021-12-18 17.08.26.gif

原生项目启动时,内存就达到了80M,说明在原生项目启动时,我们已经初始化了FlutterEngine,而且第一次调用之后,内存也没有再次大幅增长;

但是,此时我们发现,我们使用setInitialRoute方法传递的参数没有被Flutter页面接收到,setInitialRoute已然失效;我们换一种通信方式

第二种:MethodChannel

根据我们之前使用MethodChannel进行通信的流程,我们将原生代码修改如下:

image.png

在原生代码中使用FlutterMethodChannelFlutter通信;

然后修改Flutter端代码如下:

image.png

Flutter端使用MethodChannel原生进行通信;其中PageOne页面代码如下:

image.png

其他页面与此页面代码基本一样;最终运行效果如下:

iShot2021-12-18 18.04.09.gif

注意事项

运行报错App.framework

报错有两种情况:

  • 模拟器运行,调用Flutter时显示白屏,并且报错如下:
Failed to find assets path for "Frameworks/App.framework/flutter_assets"
  • 真机运行,直接崩溃,报错如下:
dyld[4112]: Library not loaded: @rpath/App.framework/App
  Referenced from: /private/var/containers/Bundle/Application/5106B07B-3EF3-4961-B20F-8DB08991BD63/NativeDemo.app/NativeDemo
  Reason: tried: '/usr/lib/swift/App.framework/App' (no such file), '/private/var/containers/Bundle/Application/5106B07B-3EF3-4961-B20F-8DB08991BD63/NativeDemo.app/Frameworks/App.framework/App' (no such file), '/private/var/containers/Bundle/Application/5106B07B-3EF3-4961-B20F-8DB08991BD63/NativeDemo.app/Frameworks/App.framework/App' (no such file), '/private/var/containers/Bundle/Application/5106B07B-3EF3-4961-B20F-8DB08991BD63/NativeDemo.app/Frameworks/App.framework/App' (no such file), '/usr/lib/swift/App.framework/App' (no such file), '/private/var/containers/Bundle/Application/5106B07B-3EF3-4961-B20F-8DB08991BD63/NativeDemo.app/Frameworks/App.framework/App' (no such file), '/private/var/containers/Bundle/Application/5106B07B-3EF3-4961-B20F-8DB08991BD63/NativeDemo.app/Frameworks/App.framework/App' (no such file), '/private/var/containers/Bundle/Application/5106B07B-3EF3-4961-B20F-8DB08991BD63/NativeDemo.app/Frameworks/App.framework/App' (no such file), '/System/Library/Frameworks/App.framework/App' (no such file)

解决方案,使用Android Studio运行一次Flutter Module工程,然后在目录flutter_module->build->ios会生成真机或者模拟器的编译文件夹:

image.png

将对应的真机或者模拟器文件夹下的App.framework文件拖入XcodeGeneral->Framewords,Libraries,and Embedded Content下:

iShot2021-12-18 15.10.48.gif

再次运行,即可运行成功;

更新Flutter Module代码问题

我们在原生项目中,通过这种方式引入Flutter Module工程来使用,如果Flutter Module工程中代码后续进行了修改,那么直接用Xcode运行是不会显示最新的代码效果的;

有以下两种方式解决:

  • 先使用Android Studio来编译运行,然后使用Xcode运行;
  • Xcode清空缓存再次运行(有时候不好用);