作者:小波
蓝海系统,作为运维技术中心的鼎力之作。公司内部越来越多同事离不开该系统,对系统的依赖度越来越大。与此同时,蓝海也迎来了新的挑战。蓝海,目前只能通过浏览器来进行访问。同事在没有携带电脑的情况下,很难登录蓝海系统(通过手机端打开浏览器进行访问,效果也不理想)。因此,越来越多同事反馈蓝海能否出一个移动端的APP呢?我们秉承着运维服务化的理念,积极响应以及服务同事。对蓝海移动端APP化进行立项以及技术性研究。
在研究移动端技术选型时,发现移动端目前有三种主流的做法
原生APP 开发的优点
原生APP开发的缺点
WebAPP 开发的优点
WebAPP 开发的缺点
混合APP开发的优点
混合APP开发的缺点
技术 | 原生开发 | H5开发 | 混合开发 |
---|---|---|---|
维护 | 难 | 低 | 比较简单 |
性能 | 高 | 低 | 较高 |
学习成本 | 大 | 低 | 比较容易 |
根据不同维度的技术角度来进行对比,发现混合开发模式比较适合。无论从维护成本,性能以及学习成本来说,都是OK的。
混合开发中存在两个主流的框架
Flutter | React Native | |
---|---|---|
Github Start | 137K | 102K |
Fork | 21K | 21.8K |
诞生时间 | 2017年5月 | 2015年4月 |
渲染效率 | 高 | 低 |
开发语言 | Dart | JavaScript |
根据上表数据进行对比,Flutter 比 React Native 在 性能和流行度上都比较高。
基于项目技术性和个人发展来说,选择Flutter 无疑是最优的方案
学习 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!'); }
程序成功运行起来,必须拥有环境。由于 Flutter 是 Google 公司出品的,因此网络方面要求比较苛刻。
具体的安装流程,参考官网 https://docs.flutter.dev/get-started/install
Flutter 支持多种编辑器如:Android Studio、Xcode、Vscode。但是既然作为双端支持跨双端的开发,笔者推荐使用 VSCode。
使用 VSCode 开发 flutter 记得安装两个插件
flutter create projectname
点击 vscode -> 查看 -> 命令面板
3. 输入 flutter
将刚刚创建好的 flutter 项目使用 VSCode 进行打开,目录结构中存在 IOS 和 Android 的环境了。这些我们暂时不用关注。入口文件是 lib/main.dart。
打开后,看不懂也没有关系,毕竟 flutter 自带运行案例对新人不太友好。
点击 VSCode 右上角的 debug 按照,就可以完成启动(记得先选中 main.dart)
一个完整的 flutter 程序就已经启动起来了
运行起来发现并不是标准的”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(例如上面的例子)
StatelessWidget 生命周期只有一个(这就不难解释,为什么 StatelessWidget 页面渲染性能更高了)
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(), ); } }
StatefulWidget 的生命周期依次是:
生命周期一: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"), )), ); } }
这就完成了简单的路由跳转功能啦~
Flutter 提供了很多组件,而且每个组件都是继承 Widget。在我的理解里面:Flutter 万物都是 Widget。
我在演示 demo 时默认导入 material 的包,因此接下来的涉及到的组件都是以 material 为主,少量的原生组件为辅
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), ), )), ); } }
效果就长这样了~ 更多的细节需要查看源码或者到官网查看哦
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 按钮就完成啦~
const TextField( obscureText: true, /// 是否开启密码模式 decoration: InputDecoration( border: OutlineInputBorder(), // 边框 labelText: 'Password', ) )
一个简单又漂亮的密码输入框就完成啦~
更多组件需要到官网文档进行查阅,这里不再过多的演示~ 地址👉 https://docs.flutter.dev/development/ui/widgets
上面讲述了那么多关于 flutter 的基础知识,相信大家应该对flutter 有一定的了解以及认知了。
现在就跟随我的步伐开始运行大佬的 demo,看看一款开源的 app 做出来的效果是怎么样的?
贴上大佬的 github 地址: https://github.com/simplezhli/flutter_deer(🐶保命)
这个 demo 看起来是不是非常 nice~ 这就是用 flutter 来实现的哦,有兴趣的童鞋记得要进去看看大佬写的代码,以及实现的思路。
dart 每次升级都会给人带来一定的学习成本,例如:上个版本不需要 const 这个版本突然间就需要强制添加上 const。有些需要 new 有些又不需要 new ,给人带来一定的困扰。
flutter 在远古版本的时候还是可以支持热更新的,Google 在官网上已经说明无法热更新了。咸鱼团队研发了自己一套热更新方案,但并没有开源出来。对于我们来说,热更新暂时是没有了,只能等待官网提供或者其他大佬开源出来了~。毕竟使用热更新,性能有所下降。有得必有失,Google 的做法基于性能角度来衡量,确实不应该支持热更新。
以前编写 div 的时候,觉得多层嵌套已经很烦了。没想到来了 flutter 嵌套更加恐怖,例如 Padding 都单独拎出来作为一个 Widget。。你就可以想象一下,这个层级是多么恐怖。。
Flutter 现在在大前端上异常火热,越来越多人喜欢使用 Flutter 来开发APP。加上 Flutter 官方已经在安全版本上支持 Web 以及 Windows 开发,这让一份代码变成多端再也不是梦想了。
笔者目前对 Flutter 的认识也是处于比较浅的地步,研究不深入,如有疏漏,还请指出~