出品/ 有道智云
Part 01
为何 Flutter
跨端技术众多,为何选择 Flutter?它能带来哪些优势,有哪些缺点?
先看看具体的工程效果:
01
Flutter VS 原生
无论如何,原生的运行效率毋庸置疑是最高的,但是从工程工作量的角度来对比的话,特别是快速试错和业务扩展阶段,Flutter 是目前为止比较推荐的利器。

02
Flutter VS Web

03
Flutter 性能


04
与 RN 的性能对比
以上是同样功能模块下,Flutter 和 RN 的一些数据上的对比,是从众多的数据中抽取出来比较有代表性的一组。
05
跨端平台的多样性
06
引擎
Flare-Flutter 是一款十分优秀的 Flutter 动画引擎,编译出的动画已经在 Windows、移动端、web 上亲测验证过。
07
语法糖
A?.B
如果 A 等于 null,那么 A?.B 为 null
如果 A 不等于 null,那么 A?.B 等价于 A.B
Animal animal = new Animal('cat');
Animal empty = null;
//animal 非空,返回 animal.name 的值 cat
print(animal?.name);
//empty 为空,返回 null
print(empty?.name);
08
综合测评
09
互动应用
Flutter 生成的互动可以嵌入到任何端中使用精简的指令集进行互动,为互动场景(教学场景等带来巨大的希望),以下是直播同步互动的 demo 场景。
Part 02
Flutter 业务架构
Flutter 中目前是没有现成的 mvvm 框架的,但是我们可以利用 Element 树特性来实现 mvvm。
01
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({
this.viewModel,
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;
_ViewModelProviderState<T> createState() => _ViewModelProviderState<T>();
}
class _ViewModelProviderState<T extends BaseViewModel>
extends State<ViewModelProvider<T>> {
Widget build(BuildContext context) {
return _ViewModelProviderInherited<T>(
child: widget.child,
viewModel: widget.viewModel,
);
}
void dispose() {
widget.viewModel.dispose();
super.dispose();
}
}
// InheritedWidget可以被Element树缓存
class _ViewModelProviderInherited<T extends BaseViewModel>
extends InheritedWidget {
final T viewModel;
_ViewModelProviderInherited({
Key key,
this.viewModel,
Widget child,
}) : super(key: key, child: child);
bool updateShouldNotify(InheritedWidget oldWidget) => false;
02
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']);
}
03
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;
}
04
一些基础架构
05
View 和 ViewModel 如何实现初始化和相互作用:
06
Flutter 业务架构抽离
如果是统一系列的产品业务形态,还可以抽离出一套核心的架构,复用在同样的生产产品线上,例如当前产品线以教育为主,利用 Flutter 的一码多端性质,则可以把题版生产工厂、渲染题版引擎、 适配框架、 以及跨端接口的框架都抽离出来,迅速地形成可以推广复用的模板,可以事半功倍地解决掉业务上的试错成本问题,当然,其他产品性质的业务线均可如此。
Part 03
Flutter 适配
01
构造一个转换工具类:
//目前适配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;
}
02
具体使用:
这样每次如果有分辨率变动或者适配方案变动的时候,直接修改 resizeUtil 即可,但是这样带来的问题就是,在编写过程中单位变得很冗长,而且不熟悉团队工程的人会容易忘写,导致查错时间变长,代码侵入性较高,于是利用 dart 语言的扩展函数特性,为 resizeUtil 做一些改进。
03
低侵入式的 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());
然后在布局中直接书写单位即可:
Part 04
Flutter 中的一些坑
01
泛型上的坑
刚开始在移动端上使用泛型来做数据的自动解析时,使用了 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;
}
}
02
在编译成 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 {
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
home: MyHomePage(),
);
}
}
class MyHomePage extends StatelessWidget {
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;
});
}
Widget build(BuildContext context) {
return Container(
width:400,
height:300,
child:HtmlElementView(viewType: 'iframe')
);
}
不过这种方式会带来新的底层刷新渲染问题(当鼠标移动到某个元素时,会不停地闪动刷新),目前在新的版本上已修复,有兴趣的同学可以看看:
https://github.com/flutter/flutter/issues/53253
03
Flutter如何加载本地的html并且进行通信
内置 html 是很多工程的需求,很多网上的资料都是通过把本地的 html 做成数据流的方式然后加载进来,这种做法的兼容性很不好,而且编写过程中容易出现很多文件流过大无法读取的问题,其实这些做法都不是很舒适,我们应该通过 IFrameElement 来进行加载并通信,做法和前端很类似:
04
在 iOS 13.4 上 WebView 的手势无法正常使用
Part 05
关于布局和运算
01
容器 Widget 和渲染 Widget
02
GlobalKey


03
浮点运算
04
Matrix 的平移和旋转
Part 06
项目优化
01
避免 build() 方法耗时:
02
重绘区域优化:
03
尽量避免使用 Opacity
04
Flutter的单线程模型
优先全部执行完 Microtask Queue 中的 Event,直到 Microtask Queue 为空,才会执行 Event Queue 中的 Event。
05
耗时方法放在isolate
Isolate 是 Dart 里的线程,每个 Isolate 之间不共享内存,通过消息通信;
Dart 的代码运行在 Isolate 中,处于同一个 Isolate 的代码才能相互访问;
Part 07
杂谈总结
经历了对 Flutter 长期的探索和项目验证,目前对 Flutter 有自己的一些杂谈总结:
01
Flutter 在移动端的表现还是很不错的,在运行流畅度方面也是非常棒,经过优化过后的带大量图像运算的 app 运行在2013年的旧 Android 手机上面依然十分流畅,iOS 的流畅程度也堪比原生。
02
对于 web 的应用来说,Flutter 还在不断地改进,其中还有很多的坑没有解决,这里包括了移动端的 WebView 以及编程成的 web 应用,还不适合大面积的投入到 web 的生产环境中。
03
关于和 Native 的混编,为了避免产生混合栈应用中的内存问题和渲染问题等,建议尽量将嵌入原生的 Flutter 节点设计在叶子节点上,即业务栈跳转到 Flutter 后尽量完成结束后再回到Native栈中。
04
基于“去桥”的原生编译方式,Flutter 在未来各个平台上的运行应该会充满期待,目前验证的移动端应用打包成 Windows 应用后,运行表现还是很不错的,当然一些更大型的应用需要时间去摸索和完善。
05
语法方面,Flutter 中的 dart 正在变得越来越简单,也在借鉴一些优秀的前端框架上的语法,例如 react 等,kotlin 中也有很多相似的地方,感觉 Flutter 团队正在努力地促进大前端时代的发展。
总之,Flutter 确实带来了很多以前的跨端方案没法满足的惊喜的地方,相信不久的将来,一码多端会变得越来越重要,特别是在新业务的探索成本上表现得十分抢眼。
以上是一些对 Flutter 的一些粗浅的总结,欢迎有兴趣的小伙伴一起探讨。
web 端效果测试请点击文末【阅读原文】体验。
网易有道,与你同道,因为热爱所以选择, 期待志同道合的你加入我们。
- END -
本文分享自微信公众号 - 有道技术团队(youdaotech)。
如有侵权,请联系 support@oschina.cn 删除。
本文参与“OSC源创计划”,欢迎正在阅读的你也加入,一起分享。