Flutter技术杂谈

在这里插入图片描述

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

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

1.为什么Flutter

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

先看看具体的工程效果

web端的连接android

flutter工程效果ios

Flutter VS 原生

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

在这里插入图片描述

Flutter VS Web

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

Flutter性能

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

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

在这里插入图片描述

在这里插入图片描述

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

与RN的性能对比:

在这里插入图片描述

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

跨端平台的多样性

![]](img-blog.csdnimg.cn/20201022165…)

引擎

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();
}
复制代码
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;
}
复制代码

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)));
    }
  }
}

///历史战绩的item
class 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']);
  }
}
复制代码

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;
  }
}
复制代码

一些基础架构

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;
  }
}
复制代码

具体使用:

这样每次若是有分辨率变更或者适配方案变更的时候,直接修改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());
}
复制代码

而后在布局中直接书写单位便可:

4.Flutter中的一些坑

泛型上的坑

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

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

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;
    }
  }
}
复制代码

在编译成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')
    );
  }
}
复制代码

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

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的手势问题还在不断的讨论中:github.com/flutter/flu…

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

附件: 连接:pan.baidu.com/s/1_JjnD1q5… 提取码:7r4i

相关文章
相关标签/搜索