深圳幻海软件技术有限公司 欢迎您!

Web前端工程师实现Native APP需求,用Flutter做可攻可守的混合开发

2023-02-28

导读:近些年来前端技术不断推陈出新,使得web前端工程师开发NativeApp的成本逐渐降低,跨端技术也使得应用开发成本大幅度降低。这种背景下,越来越多的web前端工程师团队正在逐渐尝试独立开发App,使用各种综合性方案做着性能与效率上的平衡。开发一款NativeAPP往往都是一件复杂的事情,现在市

导读:近些年来前端技术不断推陈出新,使得web前端工程师开发Native App的成本逐渐降低,跨端技术也使得应用开发成本大幅度降低。这种背景下,越来越多的web前端工程师团队正在逐渐尝试独立开发App,使用各种综合性方案做着性能与效率上的平衡。

开发一款Native APP往往都是一件复杂的事情,现在市面上大多数的APP都不是单纯使用某种技术研发而成,而是随着项目需求的复杂程度、迭代效率、团队具体情况等诸多原因的影响逐步发展成为各种技术混合开发的APP架构。

技术无分好坏,适合自己团队的才是最合适的技术选型。啥意思?我来说说吧,比如你的团队几乎都使用vue开发,你非要选择使用React Native全员开发APP,成本就很大也很难推动;又比如你的团队客户端研发工程师很多,那就没必要非得用React Native,特别不人道。这就好像这么多年来,人们一直倡导通过优化工具提升效率,而不是直接通过技术干掉那些人。

那么,什么是合适的技术选型?我觉得对于一个团队,一方面能够满足需求优化体验,另一方面就是降本提效简单灵活。

为了帮助Web前端工程师实现APP需求理清楚知识脉络,本文将简单介绍开发Native APP常用的技术方法,再介绍如何用flutter做可攻可守的混合开发。

01 纯原生开发

移动端APP能够带来很好的体验,能够利用很多系统能力加持使之密闭生态发展迅速。但是使用原生方式(Native)来开发 App,不仅要求分别针对 iOS 和 Android 平台,使用不同的语言实现同样的产品功能,还要对不同的终端设备和不同的操作系统进行功能适配,并承担由此带来的测试维护升级工作。

图:原生开发App语言

02 纯跨端方案

为了减少开发维护成本,近年来很多“一套代码,多端运行”的跨平台方案犹如雨后春笋般的涌现出来,跨端方案针对不同业务目标,有Native APP的跨端,有小程序的跨端,也有横向的PC端移动端和web的跨端,这些技术方案都有各自的优缺点,其中 React Native 和 Flutter 都是非常具有代表性的技术方案。

图:跨端视图效果

1、React Native

RN 希望开发者能够在性能、展示、交互能力和迭代交付效率之间做到平衡。它在 Web 容器方案的基础上,优化了加载、解析和渲染这三大过程,以相对简单的方式支持了构建移动端页面必要的 Web 标准,保证了便捷的前端开发体验。并且在保留基本渲染能力的基础上,用原生自带的 UI 组件实现代替了核心的渲染引擎,从而保证了良好的渲染性能。

图:RN 原理图

但是,由于 React Native 的技术方案所限,使用原生控件承载界面渲染,在牺牲了部分 Web 标准灵活性的同时,固然解决了不少性能问题,但也引入了新的问题。除开通过 JavaScript 虚拟机进行原生接口的调用,JavaScript只能解释执行,而带来的通信低效不谈,由于框架本身不负责渲染,而是由原生代理,因此我们还需要面对大量平台相关的逻辑。要用好 React Native,除了掌握这个框架外,开发者还必须同时熟悉 iOS 和 Android 系统。

2、Flutter

在 Google 的强力带动下,Flutter 开辟了全新的思路,提供了一整套从底层渲染逻辑到上层开发语言的完整解决方案。视图渲染完全闭环在其框架内部,一切皆widget,不依赖于底层操作系统提供的任何组件,依靠底层图像渲染引擎 Skia从根本上保证了视图渲染在 Android 和 iOS 上的高度一致性。

图:Flutter 架构图

Flutter 的开发语言 Dart,是 Google 专门为大前端开发量身打造的专属语言,借助于先进的工具链和编译器,成为了少数同时支持JIT和AOT的语言之一,开发期调试效率高,发布期运行速度快、执行性能好,在代码执行效率上可以媲美原生 App。Dart 避免了抢占式调度和共享内存,可以在没有锁的情况下进行对象分配和垃圾回收,在性能方面表现相当不错。虽然 Dart 开发语言有一定的学习成本,但是学习成本并不高,很容易上手。

2022年3月 Flutter 2 发布,声称使用相同的代码库为 iOS、Android、Windows、macOS 和 Linux 五种操作系统构建原生应用,甚至可以嵌入到汽车、电视和智能家电,为环境计算提供最普适、可移植的体验。

图:跨端方案的对比图

从 Web 容器时代到以 React Native 为代表的泛 Web 容器时代,最后再到以 Flutter 为代表的自绘引擎时代,这些优秀的跨平台开发框架们慢慢抹平了各个平台的差异,使得操作系统的边界变得越来越模糊。

03 混合开发

混合开发也称为Hybird开发方式,是一种非常高效的APP开发方式,主要通过多种技术共同研发同一个APP应用,让专业的人做专业的部分,在技术、效率、能力、目标之间做平衡,达到核心部分体验优秀、部分功能迭代迅速成本低的开发方案。目前大多数APP也都是采用的多技术共用的开发方式,比如淘宝、美团、百度等APP都有混合开发的影子。

下面介绍几种常见的混合开发方案。

1、原生 + webview 方案

这个方案主要是通过原生开发出Native APP的外壳,内部的页面和功能主要通过常规的web技术实现,系统端能力通过原生壳子暴露出API给web实现能力调用。

这种技术对于不同的业务侧重点会有些不同,如果客户端主导的业务native会重一些,如果是纯web前端的团队web实现会非常多。这个主要是根据团队现状和业务需要做实现上的调整,如果一个部分非常可能会成为未来核心业务则大概率会使用Native及时实现或者重构。

图:原生 + Webview 方案

2、原生 + Flutter方案

原生APP的开发成本是非常高的,就像前文所说的同一个APP需要分别针对 iOS 和 Android 平台进行开发,也就是说有几种平台就需要几个技术方向的人力,这样的产品实现是非常消耗人力的。所以人们一直在寻找合适的方案一次开发就能够运行在不同的平台上,Flutter 就是 Google 开发的优秀的跨端开发Native APP技术方案,通过使用统一的编程语言 Dart ,根据一切皆 widget 的结构组织,通过触发自研渲染引擎Skia在系统应用中绘制页面,能够达到无差异的渲染效果。

无差异的绘制UI和交互表现,这也是Flutter最强大的地方,对于UI视图的渲染和交互体验基本上与原生开发的表现差距不大,但是能够节省大约1 / 2的人力,这样的降本提效方法十分受到欢迎。

图:原生 + Flutter 通信交互图

图:Flutter原生混编

当然,不同项目的面临的情况也不同,我们主要介绍的是以flutter为主的开发结构,但是很多业务原来使用原生开发的,后来为了实现一些低成本的需求开发,所以在原生开发的结构上接入 Flutter 技术进行混编开发。

3、原生 + Flutter + Webview 方案

如果团队中有很多易变的需求,或者说团队中纯web前端成员占据大多数,那么引入 webview 是一个不错的方案,虽然 webview 也存在很多问题,但是相对来说比起 RN 还是容易定位和解决的。

图:原生 + Flutter + Webview

常见webview插件依赖于Flatter嵌入Android和 iOS本机视图的机制,底层使用AndroidView和UiKitView。iOS12版本已经废弃UIWebView强推WKWebView,WKWebView 在独立进程中加载网页。其中 webview_flutter 是官方维护的 WebView插件,特性是基于原生和 Flutter SDK封装,继承 StatefulWidget,因此支持内嵌于 flutter Widget 树中,这是比较灵活的。

图:Flutter Webview插件对比

04 用Flutter做混合开发的经验

我们的项目包括APP必备的开屏,然后是登陆页面,登录成功后是加载页面,然后打开首页,在首页可以打开直播浮窗,跳转到大多数页面直播浮窗都悬浮在那里,包括首页和其他页面都是迭代频率非常高的。

图:项目需求

假设我们有一个 APP 项目,你的团队大部分人都是 vue 的深度依赖开发者,项目初期很多需求可能都会不断底推倒重来,需求迭代频率非常高,但是也需要一定程度的保证用户体验,并且支持多个平台。这种背景下,使用 flutter + webview + 原生 的开发方案确实能够顺利的完成任务。

那么,根据项目需求我们可以简单划分各个部分设计。例如开屏、登录和加载页面的变更频率不高而且用户体验要求平稳顺滑,这里可以采用 flutter 技术来实现;对于1对1视频通话部分可以考虑使用 RTC 和 webRTC 技术,而原生方式接入 sdk 和实现需求的稳定性和延时是比较可靠的,所以使用原生方式实现;其他的页面如果面临业务初级阶段、变化频率高等情况,我们可以考虑使用 web 技术实现(H5),如果存在对于客户端能力或者系统能力的调用,可以使用 Jsbridge 中间层实现。

图:设计简图

1、结构实现

因为是从 0 到 1 的项目,我们选择创建一个flutter项目,按照设计的结构进行研发。项目入口在 main.dart 中实现;通过 routers.dart 的业务逻辑实现页面间的切换;具体的页面实现在 pages 目录中相应的同名文件;UI 组件位于 widgets 中,可以通过 import 进行使用;熟悉客户端研发的同学们会发现,在flutter项目里有 Android 和 iOS 目录,原生开发在相应的原生目录中实现就可以;flutter扩展插件可以通过引用放置到plugins目录中,具体的配置在 pubspec.yaml 文件中实现。

app-project
    |----android // Android原生目录
    |    |--gradle
    |    |--build.gradle // Android配置文件
    |    |--key.jks
    |    |--key.properties
    |    |--app
    |        |--libs
    |        |--src
    |        |--profile
    |        |   |--AndroidManifest.xml 
    |        |--main
    |            |--kotlin
    |            |--res
    |            |--AndroidManifest.xml 
    |----ios
    |    |--Gemfile
    |    |--Podfile // ios 的pod 依赖文件
    |    |--Flutter
    |    |--scripts
    |    |   |--AppIcon.sh
    |    |   |--flutterbuild.sh
    |    |   |--setup.sh
    |    |--Runner
    |        |--Info.plist
    |        |--main.m
    |        |--Assets.xcassets
    |        |--Base.lproj
    |        |--AppDelegate.h
    |        |--AppDelegate.m
    |----lib // Flutter 工作区
    |    |--assets // 本地的资源工作区
    |    |--config
    |    |--jsBridge // 提供webview jsbrige
    |    |   |--live
    |    |   |--login
    |    |   |--cache
    |    |--pages  // flutter 页面
    |    |   |--live
    |    |   |--loading
    |    |   |--webViewPage
    |    |   |--login
    |    |--routers
    |    |   |--routers.dart
    |    |--utils
    |    |   |--cache.dart
    |    |   |--color.dart
    |    |   |--network.dart
    |    |   |--events.dart
    |    |--widgets
    |    |   |--routers.dart
    |    |--main.dart // 入口文件
    |----plugins
    |    |--flutter-inappwebview  //webview库
    |    |--flutter-login
    |----test
    |----web
    |----pubspec.yaml // flutter 配置文件
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.
  • 27.
  • 28.
  • 29.
  • 30.
  • 31.
  • 32.
  • 33.
  • 34.
  • 35.
  • 36.
  • 37.
  • 38.
  • 39.
  • 40.
  • 41.
  • 42.
  • 43.
  • 44.
  • 45.
  • 46.
  • 47.
  • 48.
  • 49.
  • 50.
  • 51.
  • 52.
  • 53.
  • 54.
  • 55.
  • 56.
  • 57.
  • 58.

1.1 页面实现

如果你有过原生系统(Android、iOS)或原生 JavaScript 开发经验的话,应该知道视图开发是命令式的,需要精确地告诉操作系统或浏览器用何种方式去做事情。与此不同的是,Flutter 的视图开发是声明式的,其核心设计思想就是将视图和数据分离,这与 React 的设计思路完全一致。例如代码实现如下:

// 页面实现事例
import 'package:flutter/widgets.dart';
class MyAPP extends StatelessWidget {
@override
  Widget build(BuildContext context) {
    return const Center(child: Text('Hello Qun'));
  }
}
void main() => runApp(new MyAPP());
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.

当你所要构建的用户界面不随任何状态信息的变化而变化时,需要选择使用 StatelessWidget,反之则选用 StatefulWidget。 渲染也非常有意思,Widget 是 Flutter 世界里对视图的一种结构化描述,里面存储的是有关视图渲染的配置信息; Element 则是 Widget 的一个实例化对象,将 Widget 树的变化做了抽象,能够做到只将真正需要修改的部分同步到真实的 Render Object 树中,最大程度地优化了从结构化的配置信息到完成最终渲染的过程; 而 RenderObject,则负责实现视图的最终呈现,通过布局、绘制完成界面的展示。

图:界面生成的“三棵树”

Dart 是单线程的,这意味着代码是有序的,按照在 main 函数出现的次序一个接一个地执行,不会被其他代码中断。关于组件层面的原始指针事件的监听,Flutter 提供了 Listener Widget,可以监听其子 Widget 的原始指针事件。

// 事件响应
Listener(
  child: Container(
    color: Colors.yellow,// 背景色
    width: 700,
    height: 700,
  ),
  // 手势按下回调
  onPointerDown: (event) => print("down $event"),
  // 手势移动回调
  onPointerMove:  (event) => print("move $event"),
  // 手势抬起回调
  onPointerUp:  (event) => print("up $event")
);

  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.

1.2 路由设置

如果说 UI 框架的视图元素的基本单位是组件,那应用程序的基本单位就是页面了。对于拥有多个页面的应用程序而言,需要有一个统一的机制来管理页面之间的跳转,通常被称为路由管理或导航管理。

在 Flutter 中,页面之间的跳转是通过 Route 和 Navigator 来管理的。根据是否需要提前注册页面标识符,Flutter 中的路由管理可以分为两种方式:

  • 基本路由:无需提前注册,在页面切换时需要自己构造页面实例。
// 基本路由
class FirstPage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return RaisedButton(
      // 打开页面
      onPressed: ()=> Navigator.push(context, MaterialPageRoute(
          builder: (context) => SecondPage()
      ));
    );
  }
}
class SecondPage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return RaisedButton(
      // 回退页面
      onPressed: ()=> Navigator.pop(context)
    );
  }
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 命名路由:需要提前注册页面标识符,在页面切换时通过标识符直接打开新的路由。
// 命名路由
MaterialApp(
    ...
    // 注册路由
    routes:{
      "uri_page":(context)=>UriPage(),
    },
);
// 使用名字打开页面
Navigator.pushNamed(context,"uri_page");
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.

1.3 插件引用

依赖库的依赖是通过pubspec.xml中配置完成。

// 版本配置,iOS最终是以identify中的版本配置为准
version: 1.0.30+1
// flutter sdk环境配置
environment:
  sdk: ">=2.12.0-0 <3.0.0"
  flutter: ">=1.22.2"
// 依赖配置  
dependencies:
  flutter:
    sdk: flutter
  permission_handler: ^7.1.0
  flutter_inappwebview:
    path: ./plugins/flutter_inappwebview
  joymo_app_upgrade:
    path: ./plugins/joymo_app_upgrade
  tal_login:
    path: ./plugins/tal_login
  student_101_live:
    path: ./plugins/student_101_live
dev_dependencies:
  flutter_test:
    sdk: flutter

  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.

1.4 原生接入

开发过程不免需要原生底层能力的支持,上文已经介绍了使用platform channel方式完成对接,这里用代码示例下(原生的直播能力如何对接,以单独的插件StudentLive为例子)。

// 定义一个 flutter 侧的 名称为"student_101_live"的MethodChannel 
static const MethodChannel _channel =
    const MethodChannel('student_101_live');


// 展示原生端直播UI
static Future<void> showLive(Map<String,dynamic>? map) async {
  
  try{
    // 调用原生方法名为:"showLiveDialog"方法
    await _channel.invokeMethod('showLiveDialog',map);
  }on MissingPluginException catch(e){
    print('MissingPluginException, please check plugin has been registered or not,error message:${e.message}');
  }on PlatformException catch(e){
    print('invoke method log---showLiveDialog failed,error message:${e.message}');
  }
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.

iOS侧的代码,每个plugin自动编译会生成一个对应的Student101LivePlugin类:

// 这个plugin类是模版编译生成,继承自FlutterPlugin
@implementation Student101LivePlugin
+ (void)registerWithRegistrar:(NSObject<FlutterPluginRegistrar>*)registrar {
// 定义一个和flutter侧相同名称的"student_101_live" 的FlutterMethodChannel   
FlutterMethodChannel* channel = [FlutterMethodChannel
                                     methodChannelWithName:@"student_101_live"
                                     binaryMessenger:[registrar messenger]];
    
  
    Student101LivePlugin* instance = [[Student101LivePlugin alloc] init];
    // 设置一个对应dart的methodDelegate
    [registrar addMethodCallDelegate:instance channel:channel];
    
}


- (void)handleMethodCall:(FlutterMethodCall*)call result:(FlutterResult)result {
  // 判断是否和flutter侧的方法对应起来  
  if ([@"showLiveDialog" isEqualToString:call.method]) {
        // 这里method delegate的处理,完成原生方法的调用
        if (call.arguments != nil) {
            [self initLiveView:call.arguments];
        }
        result(@"iOS showLiveDialog");
    }else {
        result(FlutterMethodNotImplemented);
    }
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.
  • 27.
  • 28.

上面就已经完成了原生的接入,那这个studen101LivePlugin 如何完成注册的呢,系统已经帮忙做了,查看ios/Runner/GeneratedPluginRegistrant.m,可以看到依赖的插件都被注册,这样你只用关注业务代码的编写即可。

@implementation GeneratedPluginRegistrant
+ (void)registerWithRegistry:(NSObject<FlutterPluginRegistry>*)registry {
  [Student101LivePlugin registerWithRegistrar:[registry registrarForPlugin:@"Student101LivePlugin"]];
  [FLTURLLauncherPlugin registerWithRegistrar:[registry registrarForPlugin:@"FLTURLLauncherPlugin"]];
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.

总结一下: 在fluttter 侧和原生侧分别创建一个名称相同的channel,然后再根据channel下的各个方法完成对接的调用,是不是很简单。

1.5 web接入

APP 中使用H5进行页面编写的方案往往包括 web 资源离线方案和 server资源方案,这两种方案最大的区别就是前者的静态资源已经下发到APP中可以离线使用,后者需要通过向 web 服务端请求资源返回后通过webview进行渲染展现。

1.5.1 web 资源离线方案

通过为 webview 指定访问网站的 URL ,通过向服务端请求页面进行渲染。

1.5.2 service资源方案

离线方案往往与在线方案相结合,能够提供更好的用户体验和也不需要服务端承载大量的访问压力。

图:离线化web方案

前端应用(Web)保持原有的开发部署流程,仅通过webpack plugin修改打包流程,建立与配置平台的联系,并生成压缩文件,上传到CDN。这就让现有的H5应用方便的具备离线包的能力,后续新开发的离线应用也可以同时具备静态发布的能力。

离线包配置平台(Configuration Platform)用于管理各个离线包应用,包括对应用的增删改查,版本管理,配置查看,设置检查更新的url等功能,由webpack plugin生成的配置会通过接口入库。每次版本更新都会存储下来,通过上线操作生效。

APP后端(APP Server)主要提供APP侧离线包配置查询的接口,也包含ios和android离线包开启与否的开关,在配置平台设置生效的离线包应用会以列表的形式提供给APP使用。

客户端(APP)通过接口获取离线包清单,如果某个应用版本需要更新,先将本地包资源删除,再进行更新。通过整体考量,目前我们没有做增量更新和差量包的维护,为了 减少应用体积过大带来的更新问题,我们提供分包的配置,可以分步实现离线化。在开启离线包功能的WebView中,对所有资源请求进行反向代理,如果资源在本地有缓存,走本地缓存,没有则请求在线资源。

1.6 开屏配置

Flutter 开屏页面(也称为启动页)也是在各家平台上自己设置的,iOS 和 Android 都不相同。

iOS:由于 iOS 必须使用 Xcode storyboard 提供应用启动页面,在项目根项目下执行 open ios/Runner.xcworkspace 打开Flutter应用程序的Xcode项目,然后选择 Runner/Assets.xcassets,将需要的启动图拖到 LaunchImage 图像集中即可。

Android:因 Android 启动页面是个 AndroidView,同时Flutter第一帧也在绘制,这时候两者之间会有空隙。Flutter 2.5之后做了优化(将Android启动页保持到Flutter的第一帧渲染完成),也改变了启动页面的设置方式。简单介绍下2.5以后的设置方式,在Android AndroidManifest.xml配置

<activity
    android:name=".MyActivity"
    // 1、配置启动的style
    android:theme="@style/LaunchTheme"
    // ...
    >
    <meta-data
       // 2、配置普通的style,系统会从启动style直接过渡到这个style
        android:name="io.flutter.embedding.android.NormalTheme"
        android:resource="@style/NormalTheme"
        />
    <intent-filter>
        <action android:name="android.intent.action.MAIN"/>
        <category android:name="android.intent.category.LAUNCHER"/>
    </intent-filter>
</activity>
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.

2、编译发布

Flutter 虽然实现多端的 UI 统一,最终的发布还是交给了各家平台发布流程。

Android

通常的流程如下:全程通过Android studio完成。

Flutter clean 清理自动编译生成的缓存文件,避免因为缓存文件导致的编译错误。

Flutter clean 


=========执行输出=========
Cleaning Xcode workspace...                                         4.1s
Deleting build...                                                1,116ms
Deleting .dart_tool...                                               6ms
Deleting .packages...                                                0ms
Deleting Generated.xcconfig...                                       1ms
Deleting flutter_export_environment.sh...                            0ms
Deleting .flutter-plugins-dependencies...                            0ms
Deleting .flutter-plugins...                                         0ms

  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.

Flutter pub get 拉取工程依赖库。

Flutter pub get 


=========部分执行输出=========
Running "flutter pub get" in studentflutter...
executing: [/Users/xx/talworkproject/xx/studentflutter/]
/Users/xx/talworkproject/xx/fluttersdk/bin/cache/dart-sdk/bin/pub --verbose get --no-precompile
FINE: Pub 2.14.4
MSG : Resolving dependencies...
SLVR: fact: app_student_flutter is 1.0.30+1
SLVR: derived: app_student_flutter
SLVR: fact: app_student_flutter depends on flutter any from sdk
SLVR: fact: app_student_flutter depends on permission_handler ^7.1.0
SLVR: fact: app_student_flutter depends on limiting_direction_csx ^0.1.0
SLVR: fact: app_student_flutter depends on url_launcher ^6.0.0-nullsafety.4
SLVR: fact: app_student_flutter depends on package_info ^2.0.0
SLVR: fact: app_student_flutter depends on shared_preferences ^2.0.5
SLVR: fact: app_student_flutter depends on connectivity_plus ^1.0.2
SLVR: fact: app_student_flutter depends on dio ^4.0.0
SLVR: fact: app_student_flutter depends on app_settings ^4.1.0
SLVR: fact: app_student_flutter depends on fluttertoast ^8.0.7
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.

Flutter build apk 最终产生一个可以发布的APK文件,发布出去。

Flutter build apk --release/--debug
  • 1.

iOS

通常流程如下:需要Xcode+Android studio/VScode完成,前两步同Android 。

Flutter clean
Flutter pub get
  • 1.
  • 2.

Flutter build ipa 生成一个构建归档。

Flutter build ipa --release/--debug/--profile
  • 1.

后面的操作和纯原生的 iOS 发布流程相同,使用 xcode 完成归档校验,upload to App store等。

平台编译流程

flutter 虽屏蔽了平台的编译流程,但是依然脱离不了各平台编译流程,那它是如何做到呢?因篇幅有限以 Android Fluttter(纯Flutter项目) 编译几个重要编译步骤来说明:

图:Flutter在Android上的编译

App模块下的 build.gradle,这个是安卓的编译配置文件,承载编译的重点内容。

...省略
apply plugin: FlutterPlugin
class FlutterPlugin implements Plugin<Project> {
   // ......
   // 重点入口
    @Override
    void apply(Project project) {
        //......
        project.extensions.create("flutter", FlutterExtension)
        // 重点:添加flutter构建相关的各种task
        this.addFlutterTasks(project)
        //......
        // flutter shell 命令
        String flutterExecutableName = Os.isFamily(Os.FAMILY_WINDOWS) ? "flutter.bat" : "flutter"
        flutterExecutable = Paths.get(flutterRoot.absolutePath, "bin", flutterExecutableName).toFile();
        String flutterProguardRules = Paths.get(flutterRoot.absolutePath, "packages", "flutter_tools",
                "gradle", "flutter_proguard_rules.pro")
        // 给所有的buildtypes 添加依赖 
        project.android.buildTypes.all this.&addFlutterDependencies
    }
 
    /**
     * Adds the dependencies required by the Flutter project.
     * This includes:
     *    1. The embedding
     *    2. libflutter.so
     */
     // 重要,看上面注释就可以看出主要是The embedding和libflutter.so
    void addFlutterDependencies(buildType) {
        String flutterBuildMode = buildModeFor(buildType)
        //.....
        platforms.each { platform ->
            String arch = PLATFORM_ARCH_MAP[platform].replace("-", "_")
            // Add the `libflutter.so` dependency.
            addApiDependencies(project, buildType.name,
                    "io.flutter:${arch}_$flutterBuildMode:$engineVersion")
        }
    }
    //......
    // 重点:整个编译过程中的重点和难点,最终是产出flutter层的产物 app.so和libs.jar,篇幅有限
    private void addFlutterTasks(Project project) {
            //......
            String taskName = toCammelCase(["compile", FLUTTER_BUILD_PREFIX, variant.name])
            FlutterTask compileTask = project.tasks.create(name: taskName, type: FlutterTask) {
                //......
            }
            // 中间产物lib.jar
            File libJar = project.file("${project.buildDir}/${AndroidProject.FD_INTERMEDIATES}/flutter/${variant.name}/libs.jar")
            Task packFlutterAppAotTask = project.tasks.create(name: "packLibs${FLUTTER_BUILD_PREFIX}${variant.name.capitalize()}", type: Jar) {
                destinationDir libJar.parentFile
                archiveName libJar.name
                dependsOn compileTask
                targetPlatforms.each { targetPlatform ->
                    String abi = PLATFORM_ARCH_MAP[targetPlatform]
                    from("${compileTask.intermediateDir}/${abi}") {
                        include "*.so"
                        // Move `app.so` to `lib/<abi>/libapp.so`
                        rename { String filename ->
                            return "lib/${abi}/lib${filename}"
                        }
                    }
                }
            }
 
        if (isFlutterAppProject()) {
            project.android.applicationVariants.all { variant ->
                // assemble task任务,最终走到 Android assemble 任务中
                Task assembleTask = getAssembleTask(variant)
                Task copyFlutterAssetsTask = addFlutterDeps(variant)
                def variantOutput = variant.outputs.first()
                def processResources = variantOutput.hasProperty("processResourcesProvider") ?
                    variantOutput.processResourcesProvider.get() : variantOutput.processResources
                processResources.dependsOn(copyFlutterAssetsTask)
                // Copy the output APKs into a known location, so `flutter run` or `flutter build apk`
                // flutter build apk的归档
                variant.outputs.all { output ->
                    assembleTask.doLast {
                        // .......
                        project.copy {
                            from new File("$outputDirectoryStr/${output.outputFileName}")
                            into new File("${project.buildDir}/outputs/flutter-apk");
                            rename {
                                return "${filename}.apk"
                            }
                        }
                    }
                }
            }
         // ...
        }
        // ...
    }
}

  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.
  • 27.
  • 28.
  • 29.
  • 30.
  • 31.
  • 32.
  • 33.
  • 34.
  • 35.
  • 36.
  • 37.
  • 38.
  • 39.
  • 40.
  • 41.
  • 42.
  • 43.
  • 44.
  • 45.
  • 46.
  • 47.
  • 48.
  • 49.
  • 50.
  • 51.
  • 52.
  • 53.
  • 54.
  • 55.
  • 56.
  • 57.
  • 58.
  • 59.
  • 60.
  • 61.
  • 62.
  • 63.
  • 64.
  • 65.
  • 66.
  • 67.
  • 68.
  • 69.
  • 70.
  • 71.
  • 72.
  • 73.
  • 74.
  • 75.
  • 76.
  • 77.
  • 78.
  • 79.
  • 80.
  • 81.
  • 82.
  • 83.
  • 84.
  • 85.
  • 86.
  • 87.
  • 88.
  • 89.
  • 90.
  • 91.
  • 92.
  • 93.

flutter shell -> flutter dart command -> flutter 中的 gradle 脚本(最终和平台编译方式结合起来)-> flutter shell 将各种编译脚本链路化。

常见问题

  • 乱用动画问题: 不要乱用 Animation 动画,这个动画占用主线程资源,如果一直高频触发会抢占资源影响其他代码逻辑的执行。
  • 交互冲突问题:在屏幕假死的现象触发后,要检查原生、flutter和web事件之间是否冲突,这里面的逻辑关系需要搞清楚。
  • 联调问题:在联调阶段,web、flutter和原生之间需要考虑经常切换环境的开发场景。
  • Flutter sdk版本问题:统一Flutter sdk 环境避免在不同开发者中产生一些因为环境配置的问题。

05 总结

技术的发展使得作为一名Web前端工程师实现Native APP需求更加容易,可以考虑使用flutter做可攻可守的混合开发,随着业务的稳定逐渐将稳定业务原生化,借助 web 高性价比的开发成本又不会因为开发效率措施商机。整个混合开发的全链路还是比较长的,从各种技术的功能开发、联调、编译和发布都会积累非常多的实战经验,这份经历也是非常宝贵的。