Flutter 混合开发基础

引言

Flutter 做为 Google 开源的新一代跨平台、高性能 UI 框架,旨在帮助开发者高效地构建出跨平台的、UI 与交互体验一致的精美应用,推出后一直倍受开发者的青睐。android

当须要开发一个全新的应用时,咱们能够很方便地从零开始,彻底使用 Flutter 进行开发。但若是是针对一个现有的应用,须要引入 Flutter 技术,显然使用 Flutter 所有重写一遍是不现实的。幸运的是,Flutter 很好地支持了以独立页面、甚至是 UI 片断的方式集成到现有的应用中,即所谓的混合开发模式。本文主要从一个 Android 开发的视角,谈谈 Android 平台下, Flutter 的混合开发与构建。json

Hello Flutter

相信如今应该不多会有移动端开发者不知道 Flutter,这里再也不作过多介绍。对于这门技术,使用过的应该绝大多数都会说好;没用过的推荐尝试一下,跑个 Demo 体验体验,有可能它就是你须要学习和掌握的最后一门新技术了。回过头来,Flutter 究竟有什么独特的魅力让它能从一众技术中脱颖而出呢?总结一下,主要有如下几点:安全

  • 跨平台:能够作到一套代码完美适配 Android、iOS 平台,将来还会覆盖更多平台,大大节省了开发人力与维护成本,同时拥有出色的跨端 UI 表现一致性。
  • 高效开发:SDK 提供了丰富的 UI 组件,开箱即用;声明式的 UI 构建方式,大大减小出错率;Debug 模式提供热重载能力,可实时预览代码变动,不须要从新编译安装。
  • 高性能:采用自建渲染引擎,独立于系统并可单独优化;区别于 RN、WEEX,没有中间层转换的额外开销;Release 模式下代码编译为 AOT 指令,运行高效。

受益于以上的核心优点,Flutter 推出后圈了不少移动开发者的粉,各互联网大厂也纷纷将其做为一项基础技术进行研究。在 Flutter 初期,其应用场景主要是从 0 构建一个全新 App,对混合开发的支持很不友好。但做为一门跨平台的技术框架,到底仍是须要依赖原平生台提供的诸多系统能力,此外还有众多现存原生 App 跃跃欲试,所以在这个需求背景下,混合开发的支持与完善至今已发展得愈来愈好,下面咱们就用一个简单的示例开始 Android 端的 Flutter 混合开发与构建之旅。微信

引入 Flutter 模块

要在一个已有的 Android Project 中使用 Flutter,须要引入一个 Flutter Module。在 Android Studio(须要确保 Flutter 插件已经成功安装并启用)中打开现有 Android 工程,经过使用 File > New > New Module… 菜单,咱们能够新建立一个 Flutter 模块或是导入一个外部的 Flutter 模块。架构

引入外部 Flutter 模块

这里以最简单的 Android App 项目为例,导入 Flutter 模块。在 Flutter 模块导入成功以后,原工程文件、结构都会发生一些变化,主要有:app

  • settings.gradle 文件新增了如下内容。其实就是执行对应 Flutter 模块下 .android/include_flutter.groovy 脚本文件,该步骤会引入一个名为 Flutter 的 Android Library Module,同时还会引入 Flutter 模块所依赖的全部插件。
setBinding(new Binding([gradle: this]))
evaluate(new File(
    settingsDir.parentFile,
    'flutter_module/.android/include_flutter.groovy'
))
include ':flutter_module'
project(':flutter_module').projectDir = new File('../flutter_module')
  • 项目结构变化,以下图所示:

项目结构变化

在引入 Flutter 模块以前,项目中仅有 app 一个 Module;而在引入以后,能够看到除了原有的 app Module 外,Flutter Gradle 插件自动引入了额外几个子 Module:框架

  • flutter_module:指代要引入的目标 Flutter Module,不会 apply Android 相关的任何插件,主要是包含 Flutter 相关源码、资源、依赖等。
  • flutter:为 Flutter Gradle 插件引入的 Android Library Module;主要负责编译 flutter_module 及其依赖的第三方 Package、Plugin 的 Dart 代码,以及打包 Flutter 资源等。
  • device_info:为 Flutter Gradle 插件自动引入的 Flutter Android Plugin Library Module,这是由于一开始我在 flutter_module 的 pubspec.yaml 文件中添加了对 device_info 这个插件的依赖。Flutter Gradle 工具会将 flutter_module 依赖到的全部插件其 Android 平台侧的代码、资源做为一个 Library Module 引入到项目中一块儿参与构建。若是要查看 flutter_module 引入了哪些 Plugin,能够查看其对应目录下的 .flutter-plugins 与 .flutter-plugins-dependencies 文件,这两个文件是执行 flutter pub get 时生成的,记录了插件的本地文件目录、依赖信息等。

注意:一个工程不能包含多个 Flutter Module,最多只能引入一个,这是由 Flutter 的 Gradle 插件决定的。异步

使用 Flutter

完成 Flutter 模块的引入后,咱们再来看看如何使用 Flutter。async

添加依赖

首先须要在 App 模块的build.gradle脚本文件中添加对Flutter工程的依赖,只有这样 Flutter 模块才会参与到整个应用的构建中来,咱们也才可以在 App 模块中调用到 Flutter 提供的 Java 层 API。以下所示:工具

dependencies {
  implementation project(':flutter')
}

运行 Flutter 页面

咱们能够选择使用 Activity、Fragment 或者 View 来承载 Flutter 的 UI,这里主要介绍前面两种方式,并假设flutter_module中已经经过runApp方法渲染了一个widget。

  • 运行 Flutter Activity。使用io.flutter.embedding.android.FlutterActivity类能够很方便的启动一个 Flutter Activity,固然咱们也能够继承它并扩展本身的逻辑。示例代码以下:
FlutterActivity 
  .withNewEngine() 
  .build(context) 
  .also { startActivity(it) }
  • 运行 Flutter Fragment。可使用FlutterFragmentActivity或者FlutterFragment来添加 Flutter UI 片断:a. 使用FlutterFragmentActivity能够自动建立并添加一个FlutterFragment;b. 手动建立FlutterFragment后添加到目标 Activity 中。示例代码以下:
val flutterFragment = FlutterFragment.withNewEngine() 
      .dartEntrypoint(getDartEntrypointFunctionName()) 
      .initialRoute(getInitialRoute()) 
      .appBundlePath(getAppBundlePath()) 
      .flutterShellArgs(FlutterShellArgs.fromIntent(intent)) 
      .handleDeeplinking(shouldHandleDeeplinking()) 
      .renderMode(renderMode) 
      .transparencyMode(transparencyMode) 
      .shouldAttachEngineToActivity(shouldAttachEngineToActivity()) 
      .build<FlutterFragment>() 
fragmentManager 
      .beginTransaction() 
      .add( 
           FRAGMENT_CONTAINER_ID, 
           flutterFragment, 
           TAG_FLUTTER_FRAGMENT 
          ) 
       .commit()
  • 平台层和 Flutter 层通讯。不管是开发 Plugin 仍是业务逻辑,平台层与 Flutter 层通讯是必不可少的,为此就须要使用到MethodChannel。平台层经过MethodChannel请求调用 Flutter 层 API 时,数据在通过打包编码后,经过 JNI、DartVM 传到 Flutter 层解码后使用;待结果计算完成后,又会从新打包编码,通过 DartVM、JNI 传回到 Native 层;同理,在 Flutter 层请求调用平台层的 API 时,数据处理是一致的,只是流转方向相反。经过这种方式,平台层与 Flutter 层就创建了一个双向的、异步的通讯通道。在下面的示例代码中,Native 层使用dev.flutter.example/counter建立一个MethodChannel,并设置 Handler 接收 Dart 的远程方法调用 incrementCounter,并调用 reportCounter 将结果回传。
channel = MethodChannel(flutterEngine.dartExecutor, "dev.flutter.example/counter") 
channel.setMethodCallHandler { call, _ -> 
     when (call.method) { 
         "incrementCounter" -> { 
              count++ 
              channel.invokeMethod("reportCounter", count) 
         } 
     } 
}

Dart 层使用相同的名称建立 MethodChannel,并设置 Handler 处理回调结果,随后调用 incrementCounter 方法请求 counter。示例代码以下:

final _channel = MethodChannel('dev.flutter.example/counter'); 
_channel.setMethodCallHandler(_handleMessage); 
_channel.invokeMethod('incrementCounter'); 
 
Future<dynamic> _handleMessage(MethodCall call) async { 
    if (call.method == 'reportCounter') { 
      _count = call.arguments as int; 
      notifyListeners(); 
    } 
  }

这里咱们是经过手动建立 MethodChannel 进行通讯的,这在进行简单通讯的场景是没问题的,但在通讯接口 API 比较复杂的状况就不是很适用了。

一是繁琐,由于咱们须要手写大量的打包、拆包代码;二是容易出错。这个时候就轮到 Pigeon 大显身手了。Pigeon 是一个官方推出的代码生成工具,能够生成类型安全的双向通讯 API 接口,具体能够参考官方的 Example,这里再也不赘述。

Pigeon :https://flutter.dev/docs/development/platform-integration/platform-channels#pigeon

Flutter APK 解析

到这里,咱们已经了解了如何在现有 Android 项目中引入并使用 Flutter,接下来咱们再来探究一下 Flutter APK 的结构,看看 Flutter Tools 在这个 APK 包内到底打包了哪些东西。下面两图分别为 Debub 模式和 Release 模式下构建出来的 Flutter APK 包结构,忽略了非 Flutter 相关的项。

Flutter APK 包结构

能够看到两个模式下的 APK 结构大体相同,说明以下:

  • lib/{arch}/libflutter.so:为对应架构的 Flutter Engine 共享库,负责 Flutter 渲染、JNI 通讯、DartVM。若是不须要对应架构的版本,经过 abiFilters 能够 Exclude 掉。
  • lib/{arch}/libapp.so:只存在于 Release 模式下,共享库中包含 Dart AOT 生成的二进制指令和数据。在运行时,Flutter Engine 经过 Dynamic Load 的方式,从共享库中读取对应的可执行机器指令以及数据。
  • assets/flutter_assets:Flutter 引用到的相关资源
    • fonts:包含字体库。
  • FontManifest.json:引用到的字体库清单文件,json 格式,全部使用到的字体、以及字体文件在 flutter_assets 下的路径。
  • AssetManifest.json:其余资源清单文件,json 格式,为全部资源名称到资源路径的映射,Flutter 在加载某一项资源时,会经过这个配置清单找到对应路径的资源进行读取后加载。
  • kernel_blob.bin、isolate_snapshot_data、vm_snapshot_data:只存在于 Debug 模式下,分别为 DartVM 字节码与数据,其做用相似于 libapp.so,只是存在形式、打包方式不一样。在 Debug 模式下,Flutter Tools 将指令和数据分别打包,主要是为了热重载(HotReload)服务的,而在 Release 模式下是统一打包成共享库。

踩过的坑

这里,也总结了几个咱们在应用的时候遇到的问题,供你们参考避坑。

  • 路由管理复杂:这里面包括 Flutter 层内部的页面路由管理以及 Flutter 与原生的混合栈管理。前者在 Navigator 2.0 API 中已经获得了很好的完善与支持,但后者仍面临着诸多限制与不足,须要改进。目前项目中还未涉及到后者这种很复杂的业务场景,所以对这一块的研究比较少,感兴趣的同窗能够了解一下诸如 flutter_boost 此类的开源解决方案。
  • 生命周期不对应:Android 的组件通常都会有本身的生命周期,Flutter 的 Widget State 也有一套本身的生命周期,但这二者其实并非一一对应的。好比原生的 Activity 页面虽然已经被 Finish 并 Destroy 掉了,但 Flutter 层的页面并不必定会随之而被 Dispose,尤为是在使用 Cache Flutter Engine 的时候。Flutter 页面是能够脱离原生页面而存在的,它们能够被动态地 Attach 和 Detach,Attach 时会触发从新渲染,Detach 时 UI 相关的全部操做都会 Pending 直到从新被 Attach。因此在混合开发中,业务逻辑不该该过分依赖 Widget State 的一些生命周期方法,由于它们可能会被延后执行从而致使一些奇怪的 Bug。

总结

Flutter 混合开发使得开发者能够渐进式地进行 Flutter 开发与迁移,是 Flutter 寄生于原平生台相当重要的一环。

本文主要从一个 Android 开发的视角,介绍了 Flutter 混合开发的入门知识。随着 Flutter 开源项目的不断迭代与演进,混合开发的体验正在变得愈来愈好、性能也愈来愈高。但美中不足的是仍然有一些应用场景与问题并未获得很好地完善与解决,好比 Flutter 多实例问题(咱们也将在本月的另外一篇文章中跟你们分享介绍咱们在实践 Flutter 多实例遇到的问题与解决方案,敬请关注)。

瑕不掩瑜,Flutter 这门技术总体而言仍是很是不错的,它现在仍处于快速发展的阶段,相信在 Flutter 团队与开源社区的共同努力下,将来的生态值得期待。

做者简介

李成达,网易云信资深移动端开发工程师,热衷于研究跨平台开发技术以及工程提效,目前主要负责视频会议组件化 SDK 的相关研发工做。

关于网易云信的技术实践,也欢迎关注网易云信官网

更多技术干货,欢迎关注【网易智企技术+】微信公众号。

相关文章
相关标签/搜索