0 前言
从原生客户端开发,到DCloud,再到React Native,再到Flutter。「前端一直在追求低成本高效率的满足用户体验」。
为了更好的服务航空人,同时配合我们「理财、健康、生活」三位一体的服务升级计划,我们使用Flutter打造了我们全新的APP3.0。
1 为什么选择 Flutter
Flutter是「Google」开源的UI工具包,帮助开发者通过一套代码高效构建多平台精美应用,支持移动、网页、桌面和嵌入式平台。
移动端跨平台技术对比
正如开头提到的,移动端跨平台技术在不断的发展,那么为什么会出现这些技术?它们又是为了解决什么问题呢?解答了这些问题,我们就可以知道为什么会诞生Flutter,以及它是如何做到「一套代码多端复用的同时还有高性能的交互体验」。
H5 + 原生 APP
H5方式,实际只是套了个APP的壳。APP和H5可以独立开发,互不影响,项目开发速度较快。热更新方便,并不依赖APP发版。其中,JavaScript利用Bridge与原先进行数据通信、调用原生API,前端界面使用Webview完成渲染。
但是,使用此方式开发的APP,初次打开时,会出现白屏。且在低性能的手机上体验较差,因为Webview就会消耗几十M的内存。
虽然,前端工程师可以采用单页面应用、渐进式网页或者服务端渲染等技术方案进行性能优化,让体验接近原生。但是并不能弥补技术本身的缺陷。
React Native
为了解决H5方式的弊端,前端工程师们又推出了React Native。抛弃Webview,使用原生去解析RN的显示配置,生成OEM Widgets,从而渲染出Native组件,让渲染性能和交互性能都大大提升。但是由于Android和iOS端存在差异,需要进行特殊处理,实际是写了两套代码。
而且,由于依赖Bridge和原生进行通信,最终Bridge也就成为了性能的瓶颈,尤其是高频率的通信场景(复杂的动画)体验不佳。
Flutter
最终,Flutter走出了另一条路,多了一个编译过程,使用Dart做AOT编译,将代码直接编译成Native代码,去掉Bridge,没有繁琐的数据通信和交互,性能也就更前进了一步。
不再使用平台的OEM Widgets,而是通过Skia-2D图形化引擎,绘制界面。Skia本身也是Android和Chrome的底层渲染引擎,所以性能方面完全不用担心。真正做到了「一套代码适配多端,且没有中间商赚差价(中间通信层),而且性能接近原生」。
2 如何快速上手 Flutter
上文通过比较各种移动端跨平台技术方案,让我们知道为什么会诞生Flutter,如果你是个技术极客,想必已经想要自己动手尝试了。接下来,由于本人是一名前端工程师,我将从我的角度讲解「如何快速上手 Flutter」。
将从以下几方面进行讲解:语言特性、项目结构、布局原理、生命周期。
语言特性
Flutter的编程语言是Dart,它是键全类型,使用静态类型检查来确保变量的值始终与变量的静态类型相匹配。尽管是强类型,但是支持类型推断,类型注释是可选的。
同时,它是一门面向对象的语言,在Dart中,变量都是引用类型,也就是说所有的变量都是对象。
具体的使用方法,让我们对比下JavaScript:
JavaScript VS Dart
void main() {
// 变量声明
var name;
print(name == null);
name = 'Jingqb';
print(name);
int age = 6;
// 强类型 不可更改变量类型
age = '6';
// 字符串
String slogan = 'Finance Health Life';
// 模板字符串
String describe = "Jingqb's slogan is $slogan";
List sloganList = ['Finance', 'Health'];
// 扩展运算符
sloganList = [...sloganList, 'Life'];
Function getJinqbAge() {}
// 箭头函数
String getJinqbSlogan() => slogan;
// 异步函数 async 和 await
Future getName(String name) async {
String name = await Future.delayed(Duration(seconds: 1), () => name);
print(name); // Jingqb
}
}复制
与JavaScript一样,我们可以使用var
定义变量,但是初始值为null
。同时也支持模板字符串、扩展运算符、箭头函数、async
和await
;
不同的是,如果我们明确知道声明变量的类型,可以在声明时就指定它的类型,这样上文中的age = '16';
在编写代码过程中IDE就会报错,避免代码实际运行时产生错误。个人感觉强类型的语言特性会是一种趋势,正如TypeScript的出现,虽然JavaScript写起来灵活,但是因为是解释型语言,有些错误要在运行时才会发现。而强类型则避免了一些低级错误。
项目结构
在日常的开发过程中,我们会使用脚手架来创建项目,比如create-react-app
,Flutter也提供了脚手架,只要我们在命令行输入flutter create projectname
,即可创建项目。(创建之前,请先按照官网教程安装好Flutter)。
$ flutter create jingqb
Creating project jingqb...
...
...
...
Running "flutter pub get" in jingqb... 1,359ms
Wrote 78 files.
All done!
In order to run your application, type:
$ cd jingqb
$ flutter run
Your application code is in jingqb\lib\main.dart.复制
下图中的Android文件夹就是编译好的原生代码,iOS代码需在Mac中编译。
在开发过程中,我们都会使用开源软件包,在前端开发项目中,使用package.json
中管理这些依赖和脚本。在Flutter项目中,使用pubspec.yaml
文件来管理这些依赖,也用来管理静态资源。
version: 1.0.0+1
environment:
sdk: ">=2.12.0 <3.0.0"
dependencies:
flutter:
sdk: flutter
# The following adds the Cupertino Icons font to your application.
# Use with the CupertinoIcons class for iOS style icons.
cupertino_icons: ^1.0.2
dev_dependencies:
flutter_test:
sdk: flutter
fonts:
- family: Schyler
fonts:
- asset: fonts/Schyler-Regular.ttf
- asset: fonts/Schyler-Italic.ttf
style: italic
- family: Trajan Pro
fonts:
- asset: fonts/TrajanPro.ttf
- asset: fonts/TrajanPro_Bold.ttf
weight: 700复制
布局原理
Widget
Flutter将组件命名为Widget,官方提供了300多个widget,在Flutter中一切UI皆Widget,Flutter有两大不同风格的Widget库,一个是基于Material Design(材料设计)风格的组件库;一个是基于cupertino的iOS设计风格的组件库。
中心思想是用Widget构建你的UI。当Widget的状态发生变化时,Widget会重新构建UI,Flutter会对比前后变化的不同,确定底层渲染树从一个状态转换到下一个状态所需的最小更改(类似于React/Vue中虚拟DOM的diff算法)。
在Flutter中,Widget的功能是「描述一个 UI 元素的配置数据」,也就是说,Widget并不是最终绘制在设备屏幕上的显示元素,而它只是描述显示元素的一个配置数据。
实际上,Flutter中真正代表屏幕上显示元素的类是Element,也就是说,「Widget 只是描述 Element 的配置数据」。一个Widget可以对应多个Element。这是因为同一个Widget对象可以被添加到UI树的不同部分,而真正渲染时,UI树的每一个Element节点都会对应一个Widget对象。
理解上面的概念,我们就能接受Flutter的一个缺陷:嵌套地狱。我们只是在写配置信息,页面最终的展示样子由Element决定,这也叫声明式写法。和React的JSX
原理类似。
在React中,组件一开始分为函数组件和类组件,是为了解决组件渲染的性能问题,需要根据数据变化的可以创建为类组件,后期更是进化出Hook:解决性能问题的同时更好的复用组件逻辑。
在Flutter中,组件可以分为StatelessWidget和StatefulWidget,和React类似,StatefulWidget拥有复杂的生命周期和状态可变,也是使用setState
方法改变状态。
布局
Flutter支持线性布局(Row
和Column
)、弹性布局(Flex
和Expanded
)、流式布局(ListView
和GridView
)和层叠布局(Stack
和Position
)。
网页开发过程中,我们使用三剑客来完成页面:HTML定义结构,CSS定义样式,JavaScript定义交互。而在Flutter中,正如上文所述,我们只通过Widget来完成UI:「结构、样式和交互三者合一」。所以布局需选择合适的Widget。要不然会报错,在调试过程中,IDE会提示,如果未解决,在生产的表现是渲染不完整。
Flutter布局离不开约束,其原理是:「父元素向子元素传递约束,子元素向父元素传递大小」。这跟网页不同,网页里盒子模型的大小是由子元素决定。
一开始布局时,会出现一些问题,比如为什么字不会换行、为什么元素超出页面高度不能自动滚动等。与网页端不同,浏览器做了很多幕后工作。而在Flutter中,我们需要选择合适的Widget才能完成布局。同时要注意自定义组件的划分,避免嵌套地狱。
由于Widget的数据决定了UI和交互,和网页端不同,对于一个元素的样式我们可以通过覆写CSS来自定义样式。而在Flutter,就没有这么容易了。不过我们可以利用好Theme
来改变样式。
生命周期
和React一样,组件具有生命周期,利用生命周期,我们可以控制组件的渲染与变化。
无状态组件,生命周期只有build
,只会渲染一次。而有状态组件,会根据数据的变化进行多次渲染。它的生命周期包含以下几个阶段:
createState,创建
State
的方法,当StatefulWidget被调用时会立即执行createState
。initState ,
State
各变量的初始赋值,或者获取服务端数据后调用setState
来设置State
。(这个方法需调用super
重写父类的方法)didChangeDependencies,该函数是在该组件依赖的
State
(全局)发生变化时调用,例如语言或者主题等。build,主要是返回需要渲染的Widget,由于数据变化时,
build
就会被调用,因此在该函数中只返回Widget相关逻辑,避免因组件多次渲染影响性能。reassemble,主要是提供开发阶段使用,在debug模式下,每次热重载都会调用该函数。
didUpdateWidget,该函数主要是在组件重新构建时调用,比如说热重载或父组件发生
build
。deactivate,在组件被移除节点后会被调用,如果该组件被移除节点,然后未被插入到其他节点时,则会继续调用
dispose
永久移除。dispose,永久移除组件,并释放组件资源。
总结
本文简单介绍了Flutter的相关知识,但路漫漫其修远兮,要想用好Flutter,还涉及到很多方面:数据管理、路由管理、接口请求等。
虽然Flutter的优点不少,但是也有一些缺陷:
因为发展时间短,生态圈不够繁荣,优秀的开源库较少 组件过度封装,需挑选合适的Widget才能完成页面 本质上只是UI层,脱离不了原生,需要开发人员具备原生基础开发能力,或者和原生开发工程师配合 由于代码是编译生成,打包后,安装包会增大
综上所述,本人觉得Flutter未来可期,应该是目前移动端跨平台技术中比较优秀的。
技术总是在不断发展,应该保持好奇心,去了解去学习,而不是排斥新技术的出现。并且技术本身是相通的,应善于归纳总结自己所掌握的技术,通过对比或者类比的方式,找出技术之间的相同点或差异点,快速入门新技术。