跨平台开发框架 Flutter 初探
一、背景
蓝海系统,作为运维技术中心的鼎力之作。公司内部越来越多同事离不开该系统,对系统的依赖度越来越大。与此同时,蓝海也迎来了新的挑战。蓝海,目前只能通过浏览器来进行访问。同事在没有携带电脑的情况下,很难登录蓝海系统(通过手机端打开浏览器进行访问,效果也不理想)。因此,越来越多同事反馈蓝海能否出一个移动端的APP呢?我们秉承着运维服务化的理念,积极响应以及服务同事。对蓝海移动端APP化进行立项以及技术性研究。
二、技术选型
在研究移动端技术选型时,发现移动端目前有三种主流的做法
-
Native App(原生开发APP开发模式) -
Web APP(HTML5 APP 框架开发模型) -
Hybrid APP(混合模式移动应用)
原生APP 开发的优点
-
可轻易调用手机所有硬件实现功能 -
速度更快、性能高、整体用户体验最好 -
质量安全性很高
原生APP开发的缺点
-
开发周期长以及维护成本高(比如 ios 和 android 需 2 端开发 2 个app) -
学习成本高(ios 必须会 swift, android 必须会 Java)
WebAPP 开发的优点
-
项目独立,维护更容易,兼容度多平台 -
开发周期短,学习成本低 -
更新发版更容易
WebAPP 开发的缺点
-
体验不好,对设备和网络要求高,经常卡顿,性能差 -
无法调用系统硬件
混合APP开发的优点
-
维护容易,能调用部分系统硬件,兼容多平台 -
开发周期短,学习成本低
混合APP开发的缺点
-
可能需要会原生开发以及支持更多的硬件调用 -
性能跟原生还是有差距,对设备一定要求
技术 | 原生开发 | H5开发 | 混合开发 |
---|---|---|---|
维护 | 难 | 低 | 比较简单 |
性能 | 高 | 低 | 较高 |
学习成本 | 大 | 低 | 比较容易 |
根据不同维度的技术角度来进行对比,发现混合开发模式比较适合。无论从维护成本,性能以及学习成本来说,都是OK的。
混合开发中存在两个主流的框架
-
Flutter -
React Native
Flutter | React Native | |
---|---|---|
Github Start | 137K | 102K |
Fork | 21K | 21.8K |
诞生时间 | 2017年5月 | 2015年4月 |
渲染效率 | 高 | 低 |
开发语言 | Dart | JavaScript |
根据上表数据进行对比,Flutter 比 React Native 在 性能和流行度上都比较高。
基于项目技术性和个人发展来说,选择Flutter 无疑是最优的方案
三、基础入门
3.1 Dart 语言
学习 Flutter,得先了解 Dart 语言[1]。通过这几天的学习,Dart语法对于前端同学来说,上手还是很容易的,风格非常相似。
官网 HelloWorld 的例子是
Every app has a
main()
function. To display text on the console, you can use the top-levelprint()
function:void main() {
print('Hello, World!');
}
3.2 环境配置
程序成功运行起来,必须拥有环境。由于 Flutter 是 Google 公司出品的,因此网络方面要求比较苛刻。
具体的安装流程,参考官网 https://docs.flutter.dev/get-started/install
Flutter 支持多种编辑器如:Android Studio、Xcode、Vscode。但是既然作为双端支持跨双端的开发,笔者推荐使用 VSCode。
使用 VSCode 开发 flutter 记得安装两个插件
-
Flutter
-
Dart
3.3 创建项目
-
通过命令来创建项目
flutter create projectname
-
通过 VSCode 来创建项目
点击 vscode -> 查看 -> 命令面板
-
输入 flutter
3.4 项目结构
将刚刚创建好的 flutter 项目使用 VSCode 进行打开,目录结构中存在 IOS 和 Android 的环境了。这些我们暂时不用关注。入口文件是 lib/main.dart。
打开后,看不懂也没有关系,毕竟 flutter 自带运行案例对新人不太友好。
3.5 启动模拟器
点击 VSCode 右上角的 debug 按照,就可以完成启动(记得先选中 main.dart)
一个完整的 flutter 程序就已经启动起来了
3.6 Hello World
运行起来发现并不是标准的"Hello World",因此我需要对 main.dart 进行改造
import 'package:flutter/material.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
// This widget is the root of your application.
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: Scaffold(
appBar: AppBar(
title: Text("demo1"),
),
body: Text("Hello World!"),
));
}
}
代码中出现的 StatelessWidget 是什么?后面进阶时会提及到
目前代码的层级关系:
MaterialApp -> Scaffold -> body -> Text
Scaffold 是 flutter 的页面脚手架,里面提供了 appBar、body、floatingActionButton、drawer等组件。具体可以查看 Scaffold 源码或者查看 flutter 官网关于 Scaffold 的源码。
"Hello World!"终于看到了。这个是很简单的静态页面,一般业务场景是不会出现这个页面的,一般都是有数据进行交互以及变化的。接下来,对这个页面进行改造一下,页面中出现一个按钮,点击之后会切换"Hello World!"文字。
import 'package:flutter/material.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
// This widget is the root of your application.
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: AppPage(),
);
}
}
class AppPage extends StatefulWidget {
@override
_AppPageState createState() => _AppPageState();
}
class _AppPageState extends State<AppPage> {
late String name;
@override
void initState() {
super.initState();
name = "Hello Word!";
}
void changeName() {
String a;
if (name == "Hello Word!") {
a = "GG";
} else {
a = "Hello Word!";
}
setState(() {
name = a;
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: Column(
children: [Text(name), ElevatedButton.icon(onPressed: changeName, icon: Icon(Icons.add), label: Text("变化"))],
),
),
appBar: AppBar(
title: Text("demo1"),
),
);
}
}
这时接触过 React 的童鞋就不难发现,setState 还是很眼熟的 ^_^。
四、进阶
通过刚刚 HelloWorld 的案例,可能不少的童鞋会发现,类中继承 StatelessWidget 和 StatefulWidget。这两个类分别代表什么意思呢?
还记背景分析的时候,说过 flutter 框架是使用 dart 语言来进行开发的,为什么会选择 dart 语言来进行开发呢?
性能,性能,性能 (重要的事情说三遍!!)
StatelessWidget:无状态变更,UI 静态固化的 Widget,页面渲染性能更高
StatefulWidget:因状态变更可以导致 UI 变更的 Widget,涉及到数据渲染场景,都是用 StatefulWidget(例如上面的例子)
4.1 生命周期
4.1.1 StatelessWidget
StatelessWidget 生命周期只有一个(这就不难解释,为什么 StatelessWidget 页面渲染性能更高了)
-
build
build 是用来创建 Widget 的,但因为 build 在每次界面刷新的时候都会调用。
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
// This widget is the root of your application.
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: AppPage(),
);
}
}
4.1.2 StatefulWidget
StatefulWidget 的生命周期依次是:
-
createState -
initState -
didChangeDependencies -
build -
didUpdateWidget -
deactivate -
dispose
生命周期一:createState
运行 StatefulWidget 组件时,首先会执行构造函数,然后执行 createState 函数。(构造函数并不是生命周期的一部分,具体了解需要去源码查看~)
当 StatefulWidget 组件插入到组件树中时 createState 函数由 Framework 调用,此函数在树中给定的位置为此组件创建 state, 如果在组件树的不同位置都插入此组件,即创建了多个组件。
系统会为每个组件创建单独一个 State ,当组件从组件树中移除,然后重新插入到组件树中时,createState 函数将会被调用创建一个新的 State。
createState 函数执行完毕后表示当前组件已经在组件树中,此时有一个非常重要的属性 mounted 被 Framework 设置为 true。
生命周期二:initState
initState 函数在组件被插入树中时被 FrameWork 调用(在createState 之后),此函数只会被调用一次,子类通常会重写此方法,在其中进行初始化操作,比如刚刚例子中的 name, 加载网络请求中的数据,或者订阅某些通知等业务逻辑。重写该方法时一定要调用 super.initState()
@override
void initState(){
super.initState();
// 初始化业务逻辑...
}
生命周期三:didChangeDependencies
当 StatefulWidget 第一次创建的时候, didChangeDependencies 方法会在 initState 方法之后立即调用,之后当 StatefulWidget 刷新的时候,就不会调用了,除非你的StatefulWidget 依赖的InheritedWidget 发生变化之后, didChangeDependencies 才会调用,所以 didChangeDependencies 有可能调用多次。
生命周期四:build
build 的调用方式有两种:1. StatefulWidget 第一次创建的时候,build 方法会在 didChangeDependencies 方法之后立即调用。
2.每当 UI 需要重新渲染的时候,build 都会被调用
所以千万不要在 build 里面做除了创建 Widget 之外的操作,因为这个会影响 UI 的渲染效率。
生命周期五:didUpdateWidget
当组件的 configuration 发生变化时调用此函数,当父组件使用相同的 runtimeType 和 Widget.key 重新构建一个新的组件时,Framework 将更新此 State 对象的组件属性以引用新的组件,然后使用先前的组件作为参数调用此方法。
@override
void didUpdateWidget(covariant StatefulLifecycle oldWidget){
super.didUpdateWidget(oldWidget);
}
didUpdateWidget 通常用于当前组件与前组件进行对比。
生命周期六:deactivate
当框架从树中移除 State 对象时会调用这个方法。在某些情况下,挪动 State 位置也会触发这个方法
生命周期七:dispose
当框架从树中永久移除此 State 对象时将会调用此方法,与 deactivate 的区别是,deactivate 还可以重新插入到树中,而 dispose 表示 State 对象永远不会在 build。
这里插入一点关于源码
After the framework calls [dispose], the [State] object is considered unmounted and the [mounted] property is false. It is an error to call [setState] at this point. This stage of the lifecycle is terminal: there is no way to remount a [State] object that has been disposed.
通过对源码的解读,可以发现 mounted 的值决定了 组件是否渲染到 Framework 中。
4.2 路由
还记得之前说的[代码层级](#3.6 Hello World "代码层级")吗?MaterialApp 是作为最外层,因此路由最有可能在这里进行实现。
点击 MaterialApp 源码中查看
源码中还提供了如何创建路由的例子
根据源码的提示,对之前的 main.dart 进行改造。
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
// This widget is the root of your application.
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: AppPage(),
routes: {"/home": (context) => HomePage()},
);
}
}
新增一个逻辑函数 jumpPage(Navigator 路由跳转类,里面有很多方法。详情要到官网或者源码查看)
void jumpPage(context) {
Navigator.pushNamed(context, "/home");
}
将之前按钮点击事件替换成 jumpPage
class _AppPageState extends State<AppPage> {
late String name;
@override
void initState() {
super.initState();
name = "Hello Word!";
}
void changeName() {
String a;
if (name == "Hello Word!") {
a = "GG";
} else {
a = "Hello Word!";
}
setState(() {
name = a;
});
}
void jumpPage(context) {
Navigator.pushNamed(context, "/home");
}
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: () {
FocusScope.of(context).requestFocus(FocusNode());
},
child: Scaffold(
body: Center(
child: Column(
children: [
Text(name),
TextField(
decoration: InputDecoration(hintText: "用户名", labelText: "用户名", prefix: Icon(Icons.person)),
),
ElevatedButton.icon(
onPressed: () => jumpPage(context), icon: const Icon(Icons.add), label: const Text("变化")),
],
),
),
appBar: AppBar(
title: Text("demo1"),
),
),
);
}
}
在lib文件夹下创建 home.dart, 创建 homePage 类
import 'package:flutter/material.dart';
class HomePage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
body: Container(
child: Center(
child: Text("Home"),
)),
);
}
}
这就完成了简单的路由跳转功能啦~
4.3 组件
Flutter 提供了很多组件,而且每个组件都是继承 Widget。在我的理解里面:Flutter 万物都是 Widget。
我在演示 demo 时默认导入 material 的包,因此接下来的涉及到的组件都是以 material 为主,少量的原生组件为辅
4.3.1 Text 文本信息
Text(
String this.data, {
Key? key,
this.style,
this.strutStyle,
this.textAlign,
this.textDirection,
this.locale,
this.softWrap,
this.overflow,
this.textScaleFactor,
this.maxLines,
this.semanticsLabel,
this.textWidthBasis,
this.textHeightBehavior,
}
)
/// 通过源码可知,data 就是我们需要输入的内容。style,可以对文件的样式进行修改
/// 我这边就简单的弄个蓝色的HelloWorld, 字体稍微大一点的
class HomePage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
body: Container(
child: Center(
child: Text(
"Hello World",
style: TextStyle(color: Colors.blue, fontSize: 40.0),
),
)),
);
}
}
效果就长这样了~ 更多的细节需要查看源码或者到官网查看哦
4.3.2 Button
material 里面提供了不少关于 Button 类型的 DropdownButton、ElevatedButton 、FloatingActionButton、IconButton、OutlinedButton、PopupMenuButton、TextButton
我这里就以 TextButton 为例子,其他想起请看 https://docs.flutter.dev/development/ui/widgets/material
import 'package:flutter/material.dart';
class HomePage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
body: Container(
child: Center(
child: ClipRRect(
borderRadius: BorderRadius.circular(4),
child: Stack(
children: [
Positioned.fill(
child: Container(
decoration: BoxDecoration(
gradient: LinearGradient(
colors: [
Color(0xFF0D47A1),
Color(0xFF1976D2),
Color(0xFF42A5F5),
],
),
),
),
),
TextButton(
onPressed: () {},
child: const Text("测试"),
style: TextButton.styleFrom(
padding: const EdgeInsets.all(16.0),
primary: Colors.white,
textStyle: TextStyle(fontSize: 20.0),
),
)
],
),
))),
);
}
}
一个炫酷的 TextButton 按钮就完成啦~
4.3.3 TextField 输入框
const TextField(
obscureText: true, /// 是否开启密码模式
decoration: InputDecoration(
border: OutlineInputBorder(), // 边框
labelText: 'Password',
)
)
一个简单又漂亮的密码输入框就完成啦~
五、项目演示
上面讲述了那么多关于 flutter 的基础知识,相信大家应该对flutter 有一定的了解以及认知了。
现在就跟随我的步伐开始运行大佬的 demo,看看一款开源的 app 做出来的效果是怎么样的?
这个 demo 看起来是不是非常 nice~ 这就是用 flutter 来实现的哦,有兴趣的童鞋记得要进去看看大佬写的代码,以及实现的思路。
六、吐槽flutter
6.1 flutter Dart 语言升级太快
dart 每次升级都会给人带来一定的学习成本,例如:上个版本不需要 const 这个版本突然间就需要强制添加上 const。有些需要 new 有些又不需要 new ,给人带来一定的困扰。
6.2 不能热更新
flutter 在远古版本的时候还是可以支持热更新的,Google 在官网上已经说明无法热更新了。咸鱼团队研发了自己一套热更新方案,但并没有开源出来。对于我们来说,热更新暂时是没有了,只能等待官网提供或者其他大佬开源出来了~。毕竟使用热更新,性能有所下降。有得必有失,Google 的做法基于性能角度来衡量,确实不应该支持热更新。
6.3 反人类嵌套
以前编写 div 的时候,觉得多层嵌套已经很烦了。没想到来了 flutter 嵌套更加恐怖,例如 Padding 都单独拎出来作为一个 Widget。。你就可以想象一下,这个层级是多么恐怖。。
七、结束语
Flutter 现在在大前端上异常火热,越来越多人喜欢使用 Flutter 来开发APP。加上 Flutter 官方已经在安全版本上支持 Web 以及 Windows 开发,这让一份代码变成多端再也不是梦想了。
笔者目前对 Flutter 的认识也是处于比较浅的地步,研究不深入,如有疏漏,还请指出~
END
全中国只有不到1% 的人关注了运维军团
你是个有眼光的人!
(由于交流群人数已超100人,需要进群的小伙伴可以添加运维小编的微信:)