Flutter技术:最新发展动态大解析

发表时间: 2022-09-13 07:23

张龑(网易有道技术团队)

Flutter的性能分析、工程架构、以及一些细节处理

1.为何Flutter

跨端技术众多,为何选择(Flutter),它能带来哪些优势,有哪些缺点。

Flutter VS 原生

无论如何,原生的运行效率毋庸置疑是最高的,但是从工程工作量的角度来对比的话,特别是快速试错和业务扩展阶段,flutter是目前为止比较推荐的利器。

Flutter VS Web

任何跨端的技术都是基于一码多端的思维,解决工程效率的问题,之前很多的跨端技术,例如React Native等都是基于web的跨端性解决方案,但是大家都知道,web在移动端上的运行效率和PC上有巨大差距的,这就导致RN不能很有效地在移动端完成各种复杂的交互式运算(例如复杂的动画运算,交互的执行性能等),即便是引入了Airbnb的Lottie引擎依然会在低端的手机上面显得很卡顿(当然也可以使用一些自研的引擎技术来针对各端来解决,不过这样就失去了跨端的意义)。

Flutter性能

lutter的编译方式和产物是决定其高效运行效率的前提,不同于web的跨端编译一样(web的跨端编译大多是选择了使用 “桥” 的概念来调用编译产物,通常是使用了原生端的入口 + web端的桥来实现),Flutter几乎是把dart的源码通过不同平台的编译原理生成各平台的产物,这种“去桥”的产物正式我们所希望得到的、贴近原生运行性能的编译产物(当然,在dart最初设计的时候,是参考了很多前端的结构来完成的,特别从语法上面能够很明显地感受到前端的痕迹,而且最初的dart2js的原理也是同样“”的概念)。

例如 9月23号 google发布的新flutter版本中,在支持的windows编译产物上,就是通过类似visual studio的编译工具(如果要将你的flutter工程编译成windows产物,需要提前安装一些VS相关的编译插件),生成了windows下的工程解决方案.sln,最终生成dll的调用方式,运行起来很流畅,可以下载附件中的Release.zip来尝试运行:

(PS:这里所有编译工程都是通过同一套代码完成,包括上文中的web地址、移动端案例还有这里的windows案例)

与RN的性能对比:

以上是同样功能模块下,Flutter和RN的一些数据上的对比,是从众多的数据中抽取出来比较有代表性的一组

跨端平台的多样性

引擎

Flare-Flutter是一款十分优秀的flutter动画引擎,编译出的动画已经在windows、移动端、web上亲测验证过。

语法糖

综合测评

互动应用

flutter生成的互动可以嵌入到任何端中使用精简的指令集进行互动,为互动场景(教学场景等带来巨大的希望),以下是直播同步互动的demo场景


2.Flutter业务架构

flutter中目前是没有现成的mvvm框架的,但是我们可以利用Element树特性来实现mvvm

ViewModel

abstract class BaseViewModel {  bool _isFirst = true;  BuildContext context;  bool get isFirst => _isFirst;  @mustCallSuper  void init(BuildContext context) {    this.context = context;    if (_isFirst) {      _isFirst = false;      doInit(context);    }  }  // the default load data method  @protected  Future refreshData(BuildContext context);  @protected  void doInit(BuildContext context);  void dispose();}123456789101112131415161718192021222324
class ViewModelProvider<T extends BaseViewModel> extends StatefulWidget {  final T viewModel;  final Widget child;  ViewModelProvider({    @required this.viewModel,    @required this.child,  });  static T of<T extends BaseViewModel>(BuildContext context) {    final type = _typeOf<_ViewModelProviderInherited<T>>();    _ViewModelProviderInherited<T> provider =        // 查询Element树中缓存的InheritedElement        context.ancestorInheritedElementForWidgetOfExactType(type)?.widget;    return provider?.viewModel;  }  static Type _typeOf<T>() => T;  @override  _ViewModelProviderState<T> createState() => _ViewModelProviderState<T>();}class _ViewModelProviderState<T extends BaseViewModel>    extends State<ViewModelProvider<T>> {  @override  Widget build(BuildContext context) {    return _ViewModelProviderInherited<T>(      child: widget.child,      viewModel: widget.viewModel,    );  }  @override  void dispose() {    widget.viewModel.dispose();    super.dispose();  }}// InheritedWidget可以被Element树缓存class _ViewModelProviderInherited<T extends BaseViewModel>    extends InheritedWidget {  final T viewModel;  _ViewModelProviderInherited({    Key key,    @required this.viewModel,    @required Widget child,  }) : super(key: key, child: child);  @override  bool updateShouldNotify(InheritedWidget oldWidget) => false;}123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354

DataModel

import 'dart:convert';import 'package:pupilmath/datamodel/base_network_response.dart';import 'package:pupilmath/datamodel/challenge/challenge_ranking_list_item_data.dart';import 'package:pupilmath/utils/text_utils.dart';///历史榜单class ChallengeHistoryRankingListResponse    extends BaseNetworkResponse<ChallengeHistoryRankingData> {  ChallengeHistoryRankingListResponse.fromJson(Map<String, dynamic> json)      : super.fromJson(json);  @override  ChallengeHistoryRankingData decodeData(jsonData) {    if (jsonData is Map) {      return ChallengeHistoryRankingData.fromJson(jsonData);    }    return null;  }}class ChallengeHistoryRankingData {  String props;  int bestRank; //最佳排名  int onlistTimes; //上榜次数  int total; //总共挑战数  List<ChallengeHistoryRankingItemData> ranks; //先给10天  //二维码  String get qrcode =>      TextUtils.isEmpty(props) ? '' : json.decode(props)['qrcode'] ?? '';  ChallengeHistoryRankingData.fromJson(Map<String, dynamic> json) {    props = json['props'];    bestRank = json['bestRank'];    onlistTimes = json['onlistTimes'];    total = json['total'];    if (json['ranks'] is List) {      ranks = [];      (json['ranks'] as List).forEach(          (v) => ranks.add(ChallengeHistoryRankingItemData.fromJson(v)));    }  }}///历史战绩的itemclass ChallengeHistoryRankingItemData {  ChallengeRankingListItemData champion; //当天最好成绩  ChallengeRankingListItemData user;  ChallengeHistoryRankingItemData.fromJson(Map<String, dynamic> json) {    if (json['champion'] is Map)      champion = ChallengeRankingListItemData.fromJson(json['champion']);    if (json['user'] is Map)      user = ChallengeRankingListItemData.fromJson(json['user']);  }}123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657

View

import 'dart:convert';import 'package:dio/dio.dart';import 'package:flutter/material.dart';import 'package:pupilmath/datamodel/challenge/challenge_history_ranking_list_data.dart';import 'package:pupilmath/entity_factory.dart';import 'package:pupilmath/network/constant.dart';import 'package:pupilmath/network/network.dart';import 'package:pupilmath/utils/print_helper.dart';import 'package:pupilmath/viewmodel/base/abstract_base_viewmodel.dart';import 'package:rxdart/rxdart.dart';//每日挑战历史战绩class ChallengeHistoryListViewModel extends BaseViewModel {  BehaviorSubject<ChallengeHistoryRankingData> _challengeObservable =      BehaviorSubject();  Stream<ChallengeHistoryRankingData> get challengeRankingListStream =>      _challengeObservable.stream;  @override  void dispose() {    _challengeObservable.close();  }  @override  void doInit(BuildContext context) {    refreshData(context);  }  @override  Future refreshData(BuildContext context) {    return _loadHistoryListData();  }  _loadHistoryListData() async {    Map<String, dynamic> parametersMap = {};    parametersMap["pageNum"] = 1;    parametersMap["pageSize"] = 10; //拿10天数据    handleDioRequest(      () => NetWorkHelper.instance          .getDio()          .get(challengeHistoryListUrl, queryParameters: parametersMap),      onResponse: (Response response) {        ChallengeHistoryRankingListResponse rankingListResponse =            EntityFactory.generateOBJ(json.decode(response.toString()));        if (rankingListResponse.isSuccessful) {          _challengeObservable.add(rankingListResponse.data);        } else {          _challengeObservable.addError(null);        }      },      onError: (error) => _challengeObservable.addError(error),    );  }  Future<ChallengeHistoryRankingData> syncLoadHistoryListData(    int pageNum,    int pageSize,  ) async {    Map<String, dynamic> parametersMap = {};    parametersMap["pageNum"] = pageNum;    parametersMap["pageSize"] = pageSize;    try {      Response response = await NetWorkHelper.instance          .getDio()          .get(challengeHistoryListUrl, queryParameters: parametersMap);      ChallengeHistoryRankingListResponse rankingListResponse =          EntityFactory.generateOBJ(json.decode(response.toString()));      if (rankingListResponse.isSuccessful) {        return rankingListResponse.data;      } else {        return null;      }    } catch (e) {      printHelper(e);    }    return null;  }}1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283

一些基础架构

view和viewmodel如何实现初始化和相互作用:

Flutter业务架构抽离

如果是统一系列的产品业务形态,还可以抽离出一套核心的架构,复用在同样的生产产品线上,例如当前产品线以教育为主,利用flutter的一码多端性质,则可以把题版生产工厂、渲染题版引擎、 适配框架、 以及跨端接口的框架都抽离出来,迅速地形成可以推广复用的模板,可以事半功倍地解决掉业务上的试错成本问题,当然,其他产品性质的业务线均可如此。

3.Flutter适配

任何框架中的UI适配都是特别繁重的工作,跨端上的适配更是如此,因此在同一套布局里面,各个平台的换算过程显得尤为重要,起初的时候,flutter中并没有提供某种诸如 dp 或者 sp 的适配方式,而且考虑到直接更改底层matrix换算比例的话可能会让原本高清分辨率的手机显示不是那么清楚,而flutter的宽高单位都是num,最后编译的时候才会去对应到各个平台的单位尺寸。为了减轻设计师的设计负担,这里通常使用一套ios的设计稿即可,以375 x 667的通用设计稿为例,转换过来到android上是360 x 640 (对应1080 x 1920),这里flutter的单位也是和对应手机的像素密度有关的。

构造一个转换工具类:

//目前适配iPhone和iPad机型尺寸import 'dart:io';import 'dart:ui';import 'dart:math';import 'package:pupilmath/utils/print_helper.dart';bool initScale = false;//针对ios平台的scale系数double iosScaleRatio = 0;//针对android平台的scale系数// (因为所有设计稿均使用ios的设计稿进行,所以需要转换为android设计稿上的尺寸,// 否则无法进行小屏幕上的适配)double androidScaleRatio = 0;//文字缩放比double textScaleRatio = 0;const double baseIosWidth = 375;const double baseIosHeight = 667;const double baseIosHeightX = 812;const double baseAndroidWidth = 360;const double baseAndroidHeight = 640;void _calResizeRatio() {  if (Platform.isIOS) {    final width = window.physicalSize.width;    final height = window.physicalSize.height;    final ratio = window.devicePixelRatio;    final widthScale = (width / ratio) / baseIosWidth;    final heightScale = (height / ratio) / baseIosHeight;    iosScaleRatio = min(widthScale, heightScale);  } else if (Platform.isAndroid) {    double widthScale = (baseAndroidWidth / baseIosWidth);    double heightScale = (baseAndroidHeight / baseIosHeight);    double scaleRatio = min(widthScale, heightScale);    //取两位小数    androidScaleRatio = double.parse(scaleRatio.toString().substring(0, 4));  }}bool isFullScreen() {  return false;}//缩放double resizeUtil(double value) {  if (!initScale) {    _calResizeRatio();    initScale = true;  }  if (Platform.isIOS) {    return value * iosScaleRatio;  } else if (Platform.isAndroid) {    return value * androidScaleRatio;  } else {    return value;  }}//缩放还原//每个屏幕的缩放比不一样,如果在ios设备上出题,则题目坐标值需要换算成原始坐标,加载的时候再通过不同平台换算回来double unResizeUtil(double value) {  if (iosScaleRatio == 0) {    _calResizeRatio();  }  if (Platform.isIOS) {    return value / iosScaleRatio;  } else {    return value / androidScaleRatio;  }}//文字缩放大小_calResizeTextRatio() {  final width = window.physicalSize.width;  final height = window.physicalSize.height;  final ratio = window.devicePixelRatio;  double heightRatio = (height / ratio) / baseIosHeight / window.textScaleFactor;  double widthRatio = (width / ratio) / baseIosWidth / window.textScaleFactor;  textScaleRatio = min(heightRatio, widthRatio);}double resizeTextSize(double value) {  if (textScaleRatio == 0) {    _calResizeTextRatio();  }  return value * textScaleRatio;}double resizePadTextSize(double value) {  if (Platform.isIOS) {    final width = window.physicalSize.width;    final ratio = window.devicePixelRatio;    final realWidth = width / ratio;    if (realWidth > 450) {      return value * 1.5;    } else {      return value;    }  } else {    return value;  }}double autoSize(double percent, bool isHeight) {  final width = window.physicalSize.width;  final height = window.physicalSize.height;  final ratio = window.devicePixelRatio;  if (isHeight) {    return height / ratio * percent;  } else {    return width / ratio * percent;  }}123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117

具体使用:

这样每次如果有分辨率变动或者适配方案变动的时候,直接修改resizeUtil即可,但是这样带来的问题就是,在编写过程中单位变得很冗长,而且不熟悉团队工程的人会容易忘写,导致查错时间变长,代码侵入性较高,于是利用dart语言的扩展函数特性,为resizeUtil做一些改进。

低侵入式的resizeUtil

通过扩展dart的num来构造想要的单位,这里用 dp 和 sp 来举例,在resizeUtil中加入扩展:

extension dimensionsNum on num {  ///转为dp  double get dp => resizeUtil(this.toDouble());  ///转为文本大小sp  double get sp => resizeTextSize(this.toDouble());  ///转为pad文字适配  double get padSp => resizePadTextSize(this.toDouble());}12345678910

然后在布局中直接书写单位即可:

4.Flutter中的一些坑

泛型上的坑

刚开始在移动端上使用泛型来做数据的自动解析时,使用了T.toString来判断类型,但是当编译成web的release版本时,在移动端正常运行的程序在web上无法正常工作:

刚开始的时候把目标一直定位在编译的方式上,因为存在dev profile release三种编译模式,只有在release上无法运行,误以为是release下编译有bug,随着和flutter团队的深入讨论后,发现其实是泛型在release模式下的坑,即在web版本的release模式下,一切都会进行压缩(包含类型的定义),所以在release下,T.toString()返回的是null,因此无法识别出泛型特征,具体的讨论链接:https://github.com/flutter/flutter/issues/47967

In release mode everything is minified, the (T.toString() == “Construction2DEntity”) comparison fails and you get entity null returned.

If you change the code to (T ==Construction2DEntity) it will fix your app.

最后建议无论在何种模式下,都直接写成T==的形式最为安全

class EntityFactory {  static T generateOBJ<T>(json) {    if (1 == 0) {      return null;    } else if (T == "ChallengeRankingListDataEntity") {      /// 每日挑战排行榜      return ChallengeHomeRankingListResponse.fromJson(json) as T;    } else if (T == "KnowledgeEntity") {      return KnowledgeEntity.fromJson(json) as T;    }  }}123456789101112

在编译成web产物后如何使用iframe来加载其他网页

对于移动端来说,webview_flutter可以解决掉加载web的问题,不过编译成web产物后,已经无法直接使用webview插件来进行加载,此时需要用到dart最初设计来编写网页的一些方式,即HtmlElmentView:

import 'package:flutter/material.dart';import 'dart:ui' as ui;import 'dart:html' as html;void main() {  runApp(MyApp());}class MyApp extends StatelessWidget {  @override  Widget build(BuildContext context) {    return MaterialApp(      title: 'Flutter Demo',      home: MyHomePage(),    );  }}class MyHomePage extends StatelessWidget {  @override  Widget build(BuildContext context) {    return Scaffold(      body: Center(           child: Iframe()      ),      floatingActionButton: FloatingActionButton(        onPressed: (){},        tooltip: 'Increment',        child: Icon(Icons.add),      ),     );  }}class Iframe extends StatelessWidget {  Iframe(){    ui.platformViewRegistry.registerViewFactory('iframe', (int viewId) {      var iframe = html.IFrameElement();      iframe.src='https://flutter.dev';      return iframe;  });  }  @override  Widget build(BuildContext context) {    return Container(      width:400,      height:300,      child:HtmlElementView(viewType: 'iframe')    );  }}1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950

不过这种方式会带来新的底层刷新渲染问题(当鼠标移动到某个元素时,会不停地闪动刷新),目前在新的版本上已修复,有兴趣的同学可以看看:https://github.com/flutter/flutter/issues/53253

Flutter如何加载本地的html并且进行通信

内置html是很多工程的需求,很多网上的资料都是通过把本地的html做成数据流的方式然后加载进来,这种做法的兼容性很不好,而且编写过程中容易出现很多文件流过大无法读取的问题,其实这些做法都不是很舒适,我们应该通过IFrameElement来进行加载并通信,做法和前端很类似:

在ios13.4上webview的手势无法正常使用

官方的webview_flutter在上一个版本当ios升级到13.4之后会出现手势被拦截且无法正常使用的情况,换成flutter_webview_plugin后暂时解决掉该问题(目前webview已经做了针对性的修复,但是还未验证),但是flutter_webview_plugin在ios上又无法写入user-agent,目前可以通过修改本地的插件代码进行解决:

文件位置为

flutter/.pub-cache/hosted/pub.flutter-io.cn/flutter_webview_plugin-0.3.11/ios/Classes/FlutterWebviewPlugin.m
修改内容为在146行(initWebview方法中初始化WKWebViewConfiguration后)添加如下代码
if (@available(iOS 9.0, *)) {
if (userAgent != (id)[NSNull null]) {
self.webview.customUserAgent = userAgent;
}
}

关于webview_flutter的手势问题还在不断的讨论中:https://github.com/flutter/flutter/issues/53490

5.关于布局和运算

容器widget和渲染widget

GlobalKey

通过GlobalKey获取RenderBox来获取渲染出的控件的size和position等参数:

浮点运算

在dart的浮点运算中,由于都是高精度的double运算,当运算长度过长的时候,dart会自动随机最后的一位小数,这样会导致每一次有些浮点运算每一次都是不确定的,这时需要手动进行精度转换,例如在计算两条线段是否共线时:

Matrix的平移和旋转

在矩阵的换算过程中,如果使用普通的matrix.translate,会导致rotate之后,再进行translate会在旋转的基数上面做系数叠加平移运算,这样计算后得到的不是自己想要的结果,因此如果运算当中有rotate操作时,应当使用leftTranslate来保证每次运算的独立性:

6.项目优化

避免build() 方法耗时:

重绘区域优化:

尽量避免使用Opacity

Flutter的单线程模型

优先全部执行完Microtask Queue中的Event,直到Microtask Queue为空,才会执行Event Queue中的Event

耗时方法放在isolate

7.杂谈总结

经历了对flutter长期的探索和项目验证,目前对flutter有自己的一些杂谈总结:

(1).flutter在移动端的表现还是很不错的,在运行流畅度方面也是非常棒,经过优化过后的带大量图像运算的App运行在2013年的旧android手机上面依然十分流畅,ios的流畅程度也堪比原生;

(2).对于web的应用来说,flutter还在不断地改进,其中还有很多的坑没有解决,这里包括了移动端的webview以及编程成的web应用,还不适合大面积的投入到web的生产环境中;

(3).关于和Native的混编,为了避免产生混合栈应用中的内存问题和渲染问题等,建议尽量将嵌入原生的flutter节点设计在叶子节点上,即业务栈跳转到flutter后尽量完成结束后再回到Native栈中;

(4).基于“去桥”的原生编译方式,flutter在未来各个平台上的运行应该会充满期待,目前验证的移动端应用打包成windows应用后,运行表现还是很不错的,当然一些更大型的应用需要时间去摸索和完善;

(5).语法方面,flutter中的dart正在变得越来越简单,也在借鉴一些优秀的前端框架上的语法,例如react等,kotlin中也有很多相似的地方,感觉flutter团队正在努力地促进大前端时代的发展。

总之,flutter确实带来了很多以前的跨端方案没法满足的惊喜的地方,相信不久的将来一码多端会变得越来越重要,特别是在新业务的探索成本上表现得十分抢眼。

以上是一些对flutter的一些粗浅的总结,欢迎有兴趣的小伙伴一起探讨。

网易技术热爱者队伍持续招募队友中!网易有道,与你同道,因为热爱所以选择, 期待志同道合的你加入我们,简历可发送至邮箱:bjfanyudan@corp.netease.com

附件:
链接:https://pan.baidu.com/s/1_JjnD1q5JXgctX04e1h8tA
提取码:7r4i