前言:学习 Flutter 有一段时间了,本篇文章主要是记录下 Flutter 学习历程的一些心得和开发体验,罗列出一些重要的点,需要结合官方文档食用,希望能够帮助到大家,共同学习进步。
目录:
0.学习资料汇总
1.对 Flutter 的初步认识
2.环境搭建&项目结构
3.Dart语言
4.直接撸一个登录页面?or 先学一些基础的控件?
5.页面绘制 - 熟能生巧
6.项目中功能点实现(网络请求、数据共享、数据存储、路由跳转)
7.总结
0. 学习资料汇总
网站 | 介绍 |
---|---|
Flutter 中文网 | 资料很全,对于不同平台的开发者了解入门 Flutter 很有帮助 |
Flutter 实战 | 系统化的 Flutter 开发教程,由浅入深 |
[Flutter SDK源码] | 项目中 command + 单击就可以查看,注释很清楚,组件都带有使用示例 |
Dart 语言学习 | Dart官方教程 |
咸鱼技术 | 咸鱼团队掘金地址,Flutter的进阶使用以及原理探索 |
[Gallery源码] | Flutter 官方示例 APP |
flutter-go | Flutter开发者最强辅助 App(阿里开源),包含常用Widget 的介绍和使用示例(目前不再维护,但仍可作为参考 ) |
1. 对 Flutter 的初步认识
提起Flutter,不得不说一下跨平台开发,先来了解一下跨平台开发的一些知识。
1.1 跨平台技术
根据其原理,大致分为三类:
- H5 + 原生混合开发(Cordova、 lonic、微信小程序)
- 需要动态变更的内容通过
H5
实现,WebView 加载
,系统能力则需要原生支持; - web 技术栈,社区及资源丰富;
- 复杂界面或动画有
性能问题
;
- JavaScript 开发 + 原生渲染(React Native、Weex)
- 使用JavaScriptCore将
虚拟DOM布局信息传递给原生
,原生根据布局信息通过对应的原生控件去渲染 - web 开发技术栈,上手快,开发成本低,性能比 H5 提高很多,支持动态化;
- JS 为脚本语言,执行时需要
JIT(Just In Time,运行时编译)
,执行效率和AOT(Ahead Of Time,运行前编译)
代码有差距,依赖原生控件,不同平台控件需要单独维护;
- 自绘 UI + 原生(Flutter、QT for mobile)
- 在不同平台实现统一接口的渲染引擎绘制 UI,不依赖系统原生控件;
- 性能高,灵活、容易维护,同一套代码,使用同一个渲染引擎;
-
动态性不足
,采用 AOT 模式编译发包
补充知识:AOT & JIT
程序主要有两种运行方式:静态编译
与动态解释
:
- AOT(Ahead of time),即提前编译,在程序执行前就编译好了,比如原生开发打包都是先将代码提前编译好;
- JIT(Just-in-time),即即时编译,代码一边翻译一边运行,代表语言 JavaScript、python 等脚本语言;
上面👆内容详细可见:Flutter实现:跨平台技术简介
1.2 Flutter 框架介绍
官方提供的Flutter框架的分层结构图如下:
1.2.1 Flutter Framework
- 纯
Dart
实现的 SDK,实现了一套基础库 - 最上层提供了 Material (安卓样式) 和 Cupertino (苹果样式) 两种风格的组件库;
-
Widgets
是Flutter
提供的一套基础组件库; -
Rendering
抽象的布局层,Flutter UI框架的核心,依赖于最下面两层,会构建一个 UI 树,当UI树有变化时,会计算出有变化的部分,然后更新UI树,最终绘制到屏幕,类似于React中的虚拟DOM
; - 最下两层(Foundation和Animation、Painting、Gestures)对应 dart:ui package,提供动画、手势、绘制能力;
一般来说,我们开发接触最多的也就是最上面两层了。
1.2.2 Flutter Engine
- 纯 C++实现的 SDK,其中包括了 Skia引擎、Dart运行时、文字排版引擎等。在代码调用 dart:ui库时,调用最终会走到Engine层,然后实现真正的绘制逻辑;
- Skia作为渲染/GPU后端,在Android和Fuchsia上使用FreeType渲染,在iOS上使用CoreGraphics来渲染字体;
Flutter 的底层图像渲染引擎是 Skia,Skia 是 Android 官方的图像渲染引擎,支持跨平台,使用同一个渲染引擎保证了在不同平台上的渲染效果, 但是Flutter iOS SDK则需要嵌入 Skia,导致 Flutter iOS SDK 打包的包体积要比 Android 大一些。
1.2.3 Embedder
- 嵌入层,把Flutter嵌入到各个平台上去,主要工作包括渲染Surface设置,线程设置,以及插件等;
从这里可以看出,Flutter的平台相关层很低,平台(如iOS)只是提供一个画布,剩余的所有渲染相关的逻辑都在Flutter内部,这就使得它具有了很好的跨端一致性。
1.3 为什么选择 Flutter?
因为 Boss 直聘上看到很多岗位要求中,Flutter 都作为了加分项
,另外,作为一名称职码农,多一门手艺也能多混一碗饭吃。😺
但是实际上我是被 Flutter 的优点所吸引的,主要是以下两点:
跨平台
保持两个端(Web 端的开发还没有体验过,暂且不提)的视觉效果一致,另一方面方便资本主义更好的剥削我们,之前需要 Android&iOS两个端的开发成本,现在减半了;开发效率高
Debug 模式基于 JIT,热重载(Hot Reload)多🐂🍺,不用重新编译就能看到改动后的效果;
Release 模式基于AOT,发布时通过 AOT 生成高效的本地代码,保证应用性能;响应式框架
相比于 Vue 这个框架来说,略有不及,但比原生好的太多了。
这些概念在Flutter官网以及一些初识Flutter 博客上都有介绍(毕竟要吹嘘一番),对这些概念有了解之后,才能更好的向身边其他人推广Flutter。
2. 环境搭建&项目结构
2.1 环境搭建
Flutter 开发最好使用 Mac book,因为需要支持 iOS 开发。
环境搭建基本流程:
- 下载 Flutter 官方 SDK,要使用
稳定版本
的哦; - 配置环境变量;
- 下载 Xcode (主要是支持 iOS 开发),安装 CocoaPods,以及开发使用的 IDE:Android Studio 或者 VSCode;
- IDE 中下载相应的 Flutter 插件;
具体操作可见Flutter实战:安装 Flutter。
2.2 项目结构认识
以 VS Code 为例,如下图:
-
Image
文件夹是后来创建的,存放图片资源 -
iOS
为 iOS 部分代码,使用CocoaPods
管理依赖; -
android
为 Android 部分代码,使用Gradle
管理依赖; -
lib
为 dart 代码,我们之后写的代码都存放在lib 下 -
build
文件夹存放的是开发中编译的产物 -
pubspec.yaml
存放项目中的依赖,包括第三方的依赖和图片、字体等资源依赖
2.2.1 项目依赖库管理
可以在 pub.dev 上面搜索到支持使用的依赖包,然后在 pubspec.yaml 文件中进行配置,如下所示:
pubspec.yaml
文件如下:
当有添加或者删除文件配置时,需要执行(flutter pub get)
,在 VS Code中 Command +S
保存时,默认去执行了,如果使用 Android Studio 的话,右上角会有pub get
的按钮,点击即可。
2.2.2 图片资源配置
在根目录创建一个文件夹,将所需要的图片资源放到该文件夹下,然后将图片路径写到 pubspec.yaml
文件中,在 Image 控件中填写路径即可。具体可以查看:Flutter 中文网:在 Flutter 中添加资源和图片
这里有一个注意的点,可以不用把每个图片的详细路径写到pubspec.yaml
文件中,直接写文件夹就可以,如下图:
2.2.3 程序的启动入口:lib/main.dart
Flutter中 main()
函数也是应用程序的起点,在"lib/main.dart"文件中,实现很简单:
void main() {
runApp(MyApp());
}
MyApp()可以理解为一个控件或者说一个页面视图,runApp 方法就是将这个控件显示到屏幕上,这个控件就是视图树的根节点。
应用的入口介绍详细可以看:Flutter实战:计数器应用示例
3. Dart 语言
大概了解了项目接口之后,我们先别急着去写一些布局,在此之前我们需要先学习 Dart 语言,这非常重要。
Dart 语言和Java & JavaScript
在某些方面很相似,集百家之长吧,变量的声明&类型&使用,函数的使用,类的使用都很简单,需要重点学习的有:
- 异步操作(await、async & Future),对应 js 中的(await、async & Promise)
- mixin 混入,在类的声明处使用 with 关键字,和 js 中的一致
- 函数的可选参数(在控件的构造方法中经常使用)
- Dart中的空安全(Dart 2.12 之后引入)主要是标明变量可能为 null,相当于 Swift 的可选值,同样有? 声明, ! 强制解包操作
- 类的命名构造函数
Dart 语言的学习,推荐Dart中文网中学习,很全,推荐先过一遍,太过复杂的可以先略过,用到的时候再去学习。
4. 直接撸一个登录页面?or 先学一些基础的控件?
相信你肯定忍不住去先学习了一些基础的控件,这是对的,希望你能够继续认真了解一些基础控件,这样能够帮助你顺利的撸一个登录页面出来。
在 Flutter 开发中,万物皆 Widget
,比如加个视图(页面)、点击事件、加个背景色、改个位置等,都得需要相应的Widget,实例如下:
// 可点击的文本
Widget testWidget() {
return GestureDetector( // 给非 button 组件增加点击事件
onTap: () {
print("点击了按钮");
},
child: Padding( // 设置 padding
padding: EdgeInsets.all(10),
child: Text("文本显示"),
),
);
}
如果没有前端开发经验的小伙伴们最好先在网上了解一下Margin(组件之间的距离)和Padding(组件内部内容的边距)的作用,网上搜了这篇文章:CSS 彻底理解margin与padding,理解Margin 和 Padding的意思即可。有意思的是 Flutter 提供了 Padding 组件,并没有提供 Margin 组件,我都是使用 Container(相当于多个组件的结合体)。
控件(Widget) 是一个抽象类,我们可以简单把它理解为 视图(View)
来使用,在实际开发中我们自定义控件并不直接继承 Widget ,一般继承StatelessWidget(无状态组件)
或者StatefulWidget(有状态组件)
,这里的状态可以理解为组件内的数据,它们也是抽象类,继承 Widget。
StatelessWidget(无状态组件) 和 StatefulWidget(有状态组件)最大的区别在于:
- StatelessWidget只是根据数据提供一个 View,内部不维护动态可变的数据
- StatefulWidget 是有状态(数据)的,多了一个相关的 state 类用于维护状态,内部的状态(数据)改变时,可以调用 setState 方法,重新 调用build方法
这里的build
方法是上面两个组件的共同点,我们自定义控件的时候,也是重写build
方法来构建控件,示例代码:
# StatelessWidget:
class Echo extends StatelessWidget {
const Echo({
Key key,
@required this.text,
this.backgroundColor:Colors.grey,
}):super(key:key);
final String text;
final Color backgroundColor;
@override
// 重写 build 方法构建控件
Widget build(BuildContext context) {
return Center(
child: Container(
color: backgroundColor,
child: Text(text),
),
);
}
}
# StatefulWidget
class CounterWidget extends StatefulWidget {
const CounterWidget({
Key key,
this.initValue: 0
});
final int initValue;
@override
// createState 方法中,返回了对应的 State 类的实例
_CounterWidgetState createState() => new _CounterWidgetState();
}
// 和 StatefulWidget 对应的 State 类
class _CounterWidgetState extends State<CounterWidget> {
int _counter;
@override
void initState() {
super.initState();
//初始化状态
_counter=widget.initValue;
print("initState");
}
@override
// 重写 build 方法,StatefulWidget的 build 方法在 State 类中
Widget build(BuildContext context) {
print("build");
return Scaffold(
body: Center(
child: FlatButton(
child: Text('$_counter'),
//点击后计数器自增,相当于状态改变了,这里调用 setState方法,之后会调用 build 方法,根据当前的_counter去显示
onPressed:()=>setState(()=> ++_counter,
),
),
),
);
}
}
也就是说,在开发中,如果我们想单纯显示数据的话就继承 StatelessWidget
去自定义控件,如果控件中的数据可能会变化,进而引起视图的变化,就需要继承StatefulWidget
了。
StatefulWidget
相比于 StatelessWidget
更复杂,因为它多了状态管理这个功能,我们需要着重了解 State 的生命周期了,如下图:
理解
StatefulWidget 的生命周期
很重要,开发中经常需要用到,上面的知识来自于Flutter实战:Widget简介。
下面列出一些常用的基础组件
和布局组件
,大概先了解一下就行,明白什么样的布局使用什么样的控件就行,开发时可以直接点进去查看文档以及附带的使用示例:
基础组件名字 | 介绍 |
---|---|
Image | 图片显示,支持本地图片资源显示和网络图片显示 |
Text | 文字显示,相当于 Label,其中 style 可以设置各种样式 |
Text.rich + TestSpan | 富文本显示,比如登录页面一般都有是否同意<xxx 用户协议> |
RaisedButton 、FlatButton、OutlineButton等等 | 按钮,提供点击事件 |
Icon | 图标显示,内置了一些图标,估计实际开发中也不太能用得上 |
TextField | 文本输入,设置多行输入就成了 TextView 了哦 |
Wrap | 流水式布局 |
ListView | 滚动视图 |
布局组件名字 | 介绍 |
---|---|
Flex(Row、Column) | 相当于前端的 Flex 布局,Row&Column分别是在横向和纵向对于 Flex 布局的封装 |
Expanded | 在 Flex 的 children 中使用,内部flex属性,决定对于剩余空间的占有比例 |
Stack + Positioned | 可以实现绝对布局 |
Align (Center) | 控制自身在父控件中的位置 |
Padding | 控制自身内边距 |
Container | 使用的比较多,可以设置边框、背景色等 |
Scaffold | 页面布局脚手架,AppBar(导航栏) + BottomNavigationBar(底部选项卡) + Drawer(抽屉) + FloatButton(右下角浮动按钮) |
功能类组件 | 介绍 |
---|---|
Inkwell | 给控件增加点击事件,有涟漪效果 |
GestureDetector | 给控件增加点击事件,没有涟漪效果,手势很多 |
SafeArea | 页面安全区域适配 |
上面👆的组件使用在Flutter 实战
中都可以找到详细的介绍,当然也可以在 B站上可以搜索一下 Flutter教学视频,上面有很多哦。
5. 页面绘制 - 熟能生巧
5.1 登录页面的绘制
终于到了这一步,开始上手实操了,先截个登录页面的效果图吧:
UI 布局大概如下:
- 页面整体是一个
Column
组件 - 顶部使用了
Stack + Positioned
控制 x 号和 暂不登录的位置(也可以用Row
来做) - 接着是按顺序平铺
Image、Text、TextField
和一个登录按钮(GestureDetector + Container
实现) - 最下方使用了
Expanded + Align
控制TextSpan
实现的富文本显示在最下层
布局方式并不是唯一的,只要能够实现就可以。
另外,实现一个完整的登录页面需要注意以下几点:
- 注意安全区域(顶部不规则状态栏和底部圆下巴)的设置,需要整体包裹在
SafeArea
控件中 - 键盘弹起会将底部用户协议那块顶起,需要设置
Scaffold
的resizeToAvoidBottomInset
,或者页面使用ListView
实现 - 点击空白处需要隐藏键盘,可以给
Scaffold
的body
包裹GestureDetector
控件,增加点击手势 - 监听两个
TextField
的文本输入,都不为空时登录按钮显示为可点击状态 - 获取验证码,需要使用
Future
进行倒计时,可以尝试一下哦
5.2 首页底部选项卡功能
先看一下整体的页面效果,先忽略首页的页面布局:
上面提到过的 Scaffold
就可以实现这种布局,实现方式如下:
- 自定义一个
Widget
继承StatefulWidget
类 -
body
使用PageView
来管理需要切换的页面 - 底部使用
BottomNavigationBar
创建底部选项卡 -
State
类中增加selectIndex
,通过点击底部选项卡更改selectIndex
,控制PageView
显示指定的子页面
实现代码如下:
class TabbarPage extends StatefulWidget {
@override
_TabbarPageState createState() => _TabbarPageState();
}
class _TabbarPageState extends State<TabbarPage> {
// 四个子页面
var _pageList = [HomePage(), FoundPage(), StudyPage(), MyPage()];
// 记录当前选择的Index
var _selectInex = 0;
// 控制pageView滑动
PageController _pageController = PageController();
@override
Widget build(BuildContext context) {
return Scaffold(
// 这里不需要 AppBar,子页面中带有就行
// PageView 控制页面切换
body: PageView(
children: _pageList,
controller: _pageController,
physics: NeverScrollableScrollPhysics(), // 禁止滑动
),
// 使用 BottomNavigationBar 类即可,这里我是自定义的,为了去除点击涟漪效果
bottomNavigationBar: BottomAppBar(
child: Container(
height: 50,
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
tabbarItem(0, "首页", "Image/tabbar/icon_home_normal.png",
"Image/tabbar/icon_home_selected.png"),
tabbarItem(1, "发现", "Image/tabbar/icon_found_normal.png",
"Image/tabbar/icon_found_selected.png"),
tabbarItem(2, "学习", "Image/tabbar/icon_study_normal.png",
"Image/tabbar/icon_study_selected.png"),
tabbarItem(3, "我的", "Image/tabbar/icon_my_normal.png",
"Image/tabbar/icon_my_selected.png")
],
),
)),
);
}
// 底部item
Widget tabbarItem(
int index, String title, String normalImgName, String selectImgName) {
var imgName = index == _selectInex ? selectImgName : normalImgName;
var titleColor = index == _selectInex
? ColorUtil.hexColor(0x007AFF)
: ColorUtil.hexColor(0x666666);
return GestureDetector(
onTap: () {
// 底部 item 点击时的事件
if (_selectInex == index) {
return;
}
// 调用 setState 方法,重新 build
setState(() {
// 控制 PageView 显示第一个子页面
_pageController.jumpToPage(index);
_selectInex = index;
});
},
child: Container(
padding: EdgeInsets.symmetric(vertical: 5, horizontal: 20),
child: Column(
children: [
Image.asset(
imgName,
width: 20,
height: 20,
),
Text(
title,
style: TextStyle(color: titleColor),
)
],
),
),
);
}
}
这种布局需要注意的是子页面状态的保存,可以在每个子页面的 initState 方法中输出一下,每一次切换都会重新调用 initState 方法,说明页面重新创建了,这时需要子页面的 State 类混入(mixin)AutomaticKeepAliveClientMixin
,有三步:
- State 使用 with 关键字混入
AutomaticKeepAliveClientMixin
- 重写
get wantKeepAlive
方法 - build方法中调用
super.build(context)
代码如下:
5.3 首页布局挑战 & 用户信息页面实现
可以找一个稍微复杂点的App 首页进行模仿,先不要急着去封装控件,直接写下来就行,这一步主要练习的是基础控件的使用和基础的布局方式。
基本上写完这一个首页之后,对于基础控件&布局就可以熟练使用了。
然后,实现一个我的页面,显示登录用户的信息,示例如下:
要求:
- 顶部红线框,根据 bool 值(之后就是用户的登录状态)可以判断显示用户登录&非登录样式
- 底部红线框根据 bool 值判断“退出登录”按钮是否显示
登录页面 + 主页选项卡 + 我的信息页面为下面的功能实现提供了基础,下面会讲到网络请求、数据共享、数据存储、路由跳转等功能实现。
6. 功能实现
6.1 网络请求管理
Dart IO
库中提供了用于发起Http请求的一些类,可以直接使用HttpClient
来发起请求。使用HttpClient
发起请求的示例:
try {
//创建一个HttpClient
HttpClient httpClient = new HttpClient();
//打开Http连接
HttpClientRequest request = await httpClient.getUrl(
Uri.parse("https://www.baidu.com"));
//使用iPhone的UA
request.headers.add("user-agent", "Mozilla/5.0 (iPhone; CPU iPhone OS 10_3_1 like Mac OS X) AppleWebKit/603.1.30 (KHTML, like Gecko) Version/10.0 Mobile/14E304 Safari/602.1");
//等待连接服务器(会将请求信息发送给服务器)
HttpClientResponse response = await request.close();
//读取响应内容
_text = await response.transform(utf8.decoder).join();
//输出响应头
print(response.headers);
//关闭client后,通过该client发起的所有请求都会中止。
httpClient.close();
} catch (e) {
_text = "请求失败:$e";
}
使用HttpClient
发起网络请求比较麻烦,目前使用最多第三方网络请求库就是dio, 支持Restful API、FormData、拦截器、请求取消、Cookie管理、文件上传/下载、超时、自定义适配器等,使用很简单,文档非常详细,简单的get 请求示例:
Response response;
var dio = Dio();
response = await dio.get('/test?id=12&name=wendu');
print(response.data.toString());
// Optionally the request above could also be done as
response = await dio.get('/test', queryParameters: {'id': 12, 'name': 'wendu'});
print(response.data.toString());
使用时只需要按需对其进行简单封装即可,可以参考这篇文章:强大的 dio 封装,可以满足你的一切需要。
6.2 数据转Model
这个真的是比较坑的一个地方,Flutter中并没有像Java开发中的Gson/Jackson一样的Json序列化类库。因为这样的库需要使用运行时反射
,这在Flutter中是禁用的。因为运行时反射会干扰Dart的_tree shaking_
(核心思想:一个程序所有可能的执行流程都可以用函数调用的树来表示,这样就可以消除那些从未被调用的函数。),使用_tree shaking_
,可以在release
版中“去除”未使用的代码,优化应用程序的大小。由于反射会默认应用到所有代码,因此_tree shaking_
会很难工作,在启用反射时很难知道哪些代码未被使用,因此冗余代码很难剥离,所以Flutter中禁用了Dart的反射功能
,而正因如此也就无法实现动态转化Model
的功能。
所以一般定义 model 的时候,最终都是使用命名构造函数,参数为 Map<String, dynamic>
类型:
class Student {
late String name;
late int age;
late String sex;
Student(this.name, this.age, this.sex);
// 使用命名构造函数
Student.fromJson(Map<String, dynamic> map) {
this.name = map["name"];
this.age = map["age"];
this.sex = map["sex"];
}
}
针对于有大量成员变量的类,下面几种方式可以稍微提高下效率:
- 使用官方推荐的json_serializable;
- 在json_to_dart 该网站可以复制 json 数据,直接生成指定类名的代码,稍作修改即可;
- Android Studio 可以使用FlutterJsonBeanFactory插件,VS Code 可以使用Json to DartModel 插件(还没用过,可以自己试试)
6.3 数据存储
数据存储方式一般由:写入本地文件、数据库、使用三方库shared_preferences。
使用最多的也就是shared_preferences
了,比如保存用户登录信息、用户配置信息等。它保存数据的形式为 Key-Value,支持 Android 和 iOS。在 Android 中使用 SharedPreferences
,在 iOS中使用 NSUserDefaults
。
使用方式很简单,如下:
// 保存数据
_saveData() async {
var prefs = await SharedPreferences.getInstance();
prefs.setInt('Key_Int', 12);
}
// 读取数据
Future<int> _readData() async {
var prefs = await SharedPreferences.getInstance();
var result = prefs.getInt('Key_Int');
return result ?? 0;
}
但是需要注意的是获取 SharedPreferences单例对象
是一个异步操作
,一般在 app 启动时会先获取到 SharedPreferences 的单例对象
,保存起来,再继续往下操作。
6.4 路由管理
6.4.1 普通路由使用
系统提供了 Navigator 类来进行路由管理,简单的使用如下:
// 跳转一个新页面
Navigator.push( context,
MaterialPageRoute(builder: (context) {
return NewRoute();
}));
6.4.2 路由传值操作
Navigator 的push 操作会返回一个Future 对象
,用于接收二级页面的返回值:
# 一级页面中
// 打开`TipRoute`,并等待返回结果
var result = await Navigator.push(
context,
MaterialPageRoute(
builder: (context) {
return TipRoute(
// 路由参数
text: "我是提示xxxx",
);
},
),
);
//输出`TipRoute`路由返回结果
print("路由返回值: $result");
# 二级页面 pop 时回传值
Navigator.pop(context, "我是返回值"),
6.4.3 命名路由
开发中一般使用命名路由,就是建立一个路由表
,路由跳转时根据对应的名字进行跳转:
MaterialApp(
title: 'Flutter Demo',
initialRoute:"/", //名为"/"的路由作为应用的home(首页)
theme: ThemeData(
primarySwatch: Colors.blue,
),
//注册路由表
routes:{
"new_page":(context) => NewRoute(),
"/":(context) => MyHomePage(title: 'Flutter Demo Home Page'), //注册首页路由
},
# home: MyHomePage(title: 'Flutter Demo Home Page'), // initialRoute 和 home 不能同时存在,因为都是指定首页
);
6.4.4 路由钩子
MaterialApp有一个onGenerateRoute
属性,当调用Navigator.pushNamed(...)
打开命名路由时,如果指定的路由名在路由表中已注册,则会调用路由表中的builder函数来生成路由组件;如果路由表中没有注册,才会调用onGenerateRoute来生成路由,可以不使用routes属性,直接使用onGenerateRoute属性,这样实现控制页面权限的功能就非常容易:
// 自定义路由
MaterialPageRoute routeWithSetting(RouteSettings setting) {
print("********************************");
print("routeName:${setting.name}");
print("routeArguments:${setting.arguments}");
print("********************************");
Object? arguments = setting.arguments;
// 判断如果需要登录,返回登录页面路由
if (arguments != null &&
(arguments as Map)["needLogin"] == true &&
!UserUtil().isLogin()) {
return loginRoute();
}
// _routeMap 同样为维护的一个路由表
WidgetBuilder? builder = _routeMap[setting.name];
if (builder != null) {
return MaterialPageRoute(builder: builder, settings: setting);
}
// 如果未找到,则返回指定的 404 页面
return unknowRouteWithSetting(setting);
}
# main.dart 中
MaterialApp(
...
debugShowCheckedModeBanner: false,
initialRoute: "/",
onGenerateRoute: (settings) {
// 自定义去处理
return RouteManager().routeWithSetting(settings);
},
...
}));
6.5 全局状态管理
Flutter 也是一个响应式的框架,就避免不了状态管理这个概念了,同一个页面多个子组件的状态可以由这个页面(父组件)去管理,但是跨页面的状态管理,就需要使用一个全局状态管理器了。
Provider
是官方推出的一个状态管理框架,也有一些其他开源的状态管理框架:Redux、BloC 等等。
Provider 是基于InheritedWidget
的特性实现的,所以需要先了解一下 InheritedWidget:
InheritedWidget
提供了一种数据在widget树中从上到下传递
、共享的方式,比如我们在应用的根widget中通过InheritedWidget
共享了一个数据,那么我们便可以在任意子widget中来获取该共享的数据。如果子 Widget使用了这个共享数据,则代表子widget依赖有依赖
InheritedWidget,当共享数据发生变化时,子 widget 的
didChangeDependencies 将被调用。这种机制可以使子组件在所依赖的InheritedWidget
变化时来更新自身!比如主题更改功能的实现。
这里推荐:inherited_widget介绍、provider实现两篇文章,能深入理解 provider 的实现。
简单的使用示例,以用户信息为例:
- 先创建一个类混入ChangeNotifier
class UserInfoState with ChangeNotifier {
Userinfo? userinfo;
bool isLogin = false;
UserInfoState() {
_configUserInfo();
}
// 更新用户信息
updateUserInfo() {
_configUserInfo();
}
// 获取用户数据,并通知观察者
_configUserInfo() {
userinfo = UserUtil().userinfo;
isLogin = userinfo != null;
notifyListeners();
}
}
- 在合适的位置(这里放到了最顶层)创建
void main() {
// 设置默认的请求环境,如果有多个,使用MultiProvider
runApp(MultiProvider(providers: [
ChangeNotifierProvider(
create: (context) => UserInfoState(),
),
// ChangeNotifierProvider(create: (context) => CommonState())
], child: MyApp()));
}
- 在需要监听的位置使用Consumer(推荐),这样可以控制build的范围,提高性能
Consumer<UserInfoState>(builder: (context, data, child) {
return Scaffold(
appBar: AppBar(
title: Text(
"我的学习",
style: TextStyle(fontSize: 18),
),
elevation: 0,
bottom: data.isLogin ? _topTabbar() : null,
),
body: data.isLogin ? _bodyWidget() : _placeHolderWidget());
});
在需要改变状态的位置使用
Provider.of<UserInfoState>(context,listen: false).updateUserInfo()
这种形式更改状态,listen代表不会依赖(监听)这个状态,只是获取。
7. 总结
到了这一步之后,可以自己尝试撸一个完整的项目,同时去看一些Flutter SDK 的源码和进阶知识点了。
去年学习了Uni 开发框架
( Vue.js 开发跨平台应用的前端框架),今年又重新学了一下 Flutter 框架
,两者间布局方式
、异步处理方式(Future&Promise、await、async)
都非常相似。
另外最重要的一点是:学习路线很像
。了解框架 -> 学习框架使用的语言 -> 简单布局学习 -> 项目开发最常用的功能使用(网络请求、路由、状态)-> 进阶学习。
同时不得不感叹,前端 or 移动端的框架层出不穷,学好基础知识很有必要,这样可以让你在学习一个新的框架的时候能够快速上手。
目前来看,Flutter还是比较火的,因为其相对于原生开发而言:节省效率、开发成本低,相对于H5、React Native来说,性能更高。
但是,Flutter也是有局限性的:
- 目前不支持动态更新
- 只是UI框架,一些需求依赖原生去实现
针对于不同的业务需求,肯定还是要采取合适的技术方案去实现。
- 只是UI框架,一些需求依赖原生去实现
感觉不支持动态更新是个致命的问题,开发成本、开发效率可以日夜加班来补足,性能最高 <= 原生,所以之后可能还是几种跨平台方案共存,Flutter大一统还是不太可能。