在Android Studio中创建好项目以后,项目的入口即是lib下的main.dart
import 'package:flutter/material.dart';
void main() {
runApp(const MyApp());
}
//无状态的组件(Stateless widget)
class MyApp extends StatelessWidget {}
//有状态的组件(Stateful widget)
class MyHomePage extends StatefulWidget {}
//Stateful widget 至少由两个类组成:
// 一个StatefulWidget类,一个State类
//StatefulWidget类本身是不变的,但是State类中的状态在 widget 生命周期中可能会发生变化。
//_MyHomePageState类是MyHomePage类对应的状态类,所以build方法在_MyHomePageState里面
class _MyHomePageState extends State<MyHomePage> {}
Stateful widget 至少由两个类组成: 一个StatefulWidget类,一个State类
StatefulWidget类本身是不变的,但是State类中的状态在 widget 生命周期中可能会发生变化。
_MyHomePageState类是MyHomePage类对应的状态类,所以build方法在_MyHomePageState里面
Flutter 中是通过 Widget 嵌套 Widget 的方式来构建UI和进行实践处理的,所以记住,Flutter 中万物皆为Widget
在 Flutter 中, widget 的功能是“描述一个UI元素的配置信息”,它就是说, Widget 其实并不是表示最终绘制在设备屏幕上的显示元素
Widget类本身是一个抽象类,其中最核心的就是定义了createElement()接口,在 Flutter 开发中,我们一般都不用直接继承Widget类来实现一个新组件,相反,我们通常会通过继承StatelessWidget或StatefulWidget来间接继承widget类来实现。StatelessWidget和StatefulWidget都是直接继承自Widget类,而这两个类也正是 Flutter 中非常重要的两个抽象类
Widget 只是描述一个UI元素的配置信息,那么真正的布局、绘制是由谁来完成的呢?Flutter 框架的的处理流程是这样的:
1.根据 Widget 树生成一个 Element 树,Element 树中的节点都继承自 Element 类。
2.根据 Element 树生成 Render 树(渲染树),渲染树中的节点都继承自RenderObject 类。
3.根据渲染树生成 Layer 树,然后上屏显示,Layer 树中的节点都继承自 Layer 类。
真正的布局和渲染逻辑在 Render 树中,Element 是 Widget 和 RenderObject 的粘合剂,可以理解为一个中间代理。
三棵树中,Widget 和 Element 是一一对应的,但并不和 RenderObject 一一对应
渲染树在上屏前会生成一棵 Layer 树
StatelessWidget相对比较简单,它继承自widget类,重写了createElement()方法返回StatelessElement
StatelessWidget用于不需要维护状态的场景,它通常在build方法中通过嵌套其他 widget 来构建UI,在构建过程中会递归的构建其嵌套的 widget
如下自定义回显字符串的Echo widget
class Echo extends StatelessWidget {
//在继承 widget 时,构造第一个参数通常应该是Key
const Echo({
//widget 的构造函数参数应使用命名参数
Key? key,
//命名参数中的必需要传的参数要添加required,这样有利于静态代码分析器进行检查
required this.text,
this.backgroundColor = Colors.grey, //默认为灰色
//如果 widget 需要接收子 widget ,那么child或children参数通常应被放在参数列表的最后
}):super(key:key);
// widget 的属性尽可能的被声明为final,防止被意外改变
final String text;
final Color backgroundColor;
@override
//每一个 widget 都会对应一个 context 对象
Widget build(BuildContext context) {
//context参数提供了
//从当前 widget 开始向上遍历 widget 树
//以及按照 widget 类型查找父级 widget 的方法
return Center(
child: Container(
color: backgroundColor,
child: Text(text),
),
);
}
}
调用
Widget build(BuildContext context) {
return Echo(text: "hello world");
}
每一个 widget 都会对应一个 context 对象,context参数提供了
从当前 widget 开始向上遍历 widget 树
以及按照 widget 类型查找父级 widget 的方法
下面是在子树中获取父级 widget 的一个示例:
class ContextRoute extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("Context测试"),
),
body: Container(
child: Builder(builder: (context) {
// 在 widget 树中向上查找最近的父级`Scaffold` widget
Scaffold scaffold = context.findAncestorWidgetOfExactType<Scaffold>();
// 直接返回 AppBar的title, 此处实际上是Text("Context测试")
return (scaffold.appBar as AppBar).title;
}),
),
);
}
}
StatefulWidget也是继承自widget类,添加了一个新的接口createState(),重写createElement()方法返回StatefulElement,StatefulElement中可能会多次调用createState()来创建状态(State)对象。
一个 StatefulWidget 类会对应一个 State 类,State表示与其对应的 StatefulWidget 要维护的状态
当State被改变时,可以手动调用其setState()方法通知Flutter 框架状态发生改变,Flutter 框架在收到消息后,会重新调用其build方法重新构建 widget 树,从而达到更新UI的目的。
理解State的生命周期对flutter开发非常重要,StatefulWidget 生命周期如图
当 widget 第一次插入到 widget 树时会被调用,对于每一个State对象,Flutter 框架只会调用一次该回调,所以,通常在该回调中做一些一次性的操作,如状态初始化、订阅子树的事件通知等
当State对象的依赖发生变化时会被调用
在之前build() 中包含了一个InheritedWidget,然后在之后的build() 中Inherited widget发生了变化,那么此时InheritedWidget的子 widget 的didChangeDependencies()回调都会被调用,比如当系统语言 Locale 或应用主题改变时,Flutter 框架会通知 widget 调用此回调。需要注意,组件第一次被创建后挂载的时候(包括重创建)对应的didChangeDependencies也会被调用
它主要是用于构建 widget 子树的,会在如下场景被调用:
在调用initState()之后。
在调用didUpdateWidget()之后。
在调用setState()之后。
在调用didChangeDependencies()之后。
在State对象从树中一个位置移除后(会调用deactivate)又重新插入到树的其他位置之后
此回调是专门为了开发调试而提供的,在热重载(hot reload)时会被调用,此回调在Release模式下永远不会被调用。
在 widget 重新构建时,Flutter 框架会调用widget.canUpdate来检测 widget 树中同一位置的新旧节点,然后决定是否需要更新,如果widget.canUpdate返回true则会调用此回调,
widget.canUpdate会在新旧 widget 的 key 和 runtimeType 同时相等时会返回true,也就是说在在新旧 widget 的key和runtimeType同时相等时didUpdateWidget()就会被调用
当 State 对象从树中被移除时,会调用此回调
在一些场景下,Flutter 框架会将 State 对象重新插到树中,如包含此 State 对象的子树在树的一个位置移动到另一个位置时
如果移除后没有重新插入到树中则紧接着会调用dispose()方法。
当 State 对象从树中被永久移除时调用;通常在此回调中释放资源。
我们有两种方法在子 widget 树中获取父级 StatefulWidget 的State 对象。
context对象有一个findAncestorStateOfType()方法,该方法可以从当前节点沿着 widget 树向上查找指定类型的 StatefulWidget 对应的 State 对象
// 查找父级最近的Scaffold对应的ScaffoldState对象
ScaffoldState _state = context.findAncestorStateOfType<ScaffoldState>()!;
// 打开抽屉菜单
_state.openDrawer();
但是通过 context.findAncestorStateOfType 获取 StatefulWidget 的状态的方法是通用的
在 Flutter 开发中有一个默认的约定:如果 StatefulWidget 的状态是希望暴露出的,应当在 StatefulWidget 中提供一个of 静态方法来获取其 State 对象,开发者便可直接通过该方法来获取;如果 State不希望暴露,则不提供of方法。这个约定在 Flutter SDK 里随处可见
所以,上面示例中的Scaffold也提供了一个of方法,我们其实是可以直接调用它的:
// 直接通过of静态方法来获取ScaffoldState
ScaffoldState _state=Scaffold.of(context);
// 打开抽屉菜单
_state.openDrawer();
比如我们想显示 snack bar 的话可以通过下面代码调用:
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text("我是SnackBar")),
);
通过GlobalKey来获取! 步骤分两步:
1.给目标StatefulWidget添加GlobalKey。
//定义一个globalKey, 由于GlobalKey要保持全局唯一性,我们使用静态变量存储
static GlobalKey<ScaffoldState> _globalKey= GlobalKey();
...
Scaffold(
key: _globalKey , //设置key
...
)
2.通过GlobalKey来获取State对象
_globalKey.currentState.openDrawer()
GlobalKey 是 Flutter 提供的一种在整个 App 中引用 element 的机制。如果一个 widget 设置了GlobalKey,那么我们便可以通过
globalKey.currentWidget获得该 widget 对象、
globalKey.currentElement来获得 widget 对应的element对象,
如果当前 widget 是StatefulWidget,则可以通过globalKey.currentState来获得该 widget 对应的state对象。
注意:使用 GlobalKey 开销较大,如果有其他可选方案,应尽量避免使用它。另外,同一个 GlobalKey 在整个 widget 树中必须是唯一的,不能重复。
Flutter 最原始的定义组件的方式就是通过定义RenderObject 来实现,而StatelessWidget 和 StatefulWidget 只是提供的两个帮助类。下面我们简单演示一下通过RenderObject定义组件的方式:
class CustomWidget extends LeafRenderObjectWidget{
@override
RenderObject createRenderObject(BuildContext context) {
// 创建 RenderObject
return RenderCustomObject();
}
@override
void updateRenderObject(BuildContext context, RenderCustomObject renderObject) {
// 更新 RenderObject
super.updateRenderObject(context, renderObject);
}
}
class RenderCustomObject extends RenderBox{
@override
void performLayout() {
// 实现布局逻辑
}
@override
void paint(PaintingContext context, Offset offset) {
// 实现绘制
}
}
LeafRenderObjectWidget 继承 RenderObjectWidget 继承Widget
如果组件不包含子组件,直接继承 LeafRenderObjectWidget
如果自定义的 widget 包含子组件,则可以根据子组件的数量来选择继承SingleChildRenderObjectWidget 或 MultiChildRenderObjectWidget
Flutter在基础组件库之上 Flutter 又提供了
一套 Material 风格( Android 默认的视觉风格)
和一套 Cupertino 风格(iOS视觉风格)的组件库。
要使用基础组件库,需要先导入:
import 'package:flutter/widgets.dart';
Text (opens new window):文本组件。
Row (opens new window)、 Column (opens new window):
弹性布局类 widget 可让您在水平(Row)和垂直(Column)方向上创建灵活的布局。其设计是基于 Web 开发中的 Flexbox 布局模型。
Stack (opens new window): 取代线性布局 (和 Android 中的FrameLayout相似),允许子 widget 堆叠, 你可以使用 Positioned (opens new window)来定位他们相对于Stack的上下左右四条边的位置。Stacks是基于Web开发中的绝对定位布局模型设计的。
Container (opens new window): 可让您创建矩形视觉元素,Container 可以装饰一个BoxDecoration (opens new window), 如 background、一个边框、或者一个阴影。
Container (opens new window)也可以具有边距(margins)、填充(padding)和应用于其大小的约束(constraints)。
另外, Container (opens new window)可以使用矩阵在三维空间中对其进行变换
Material组件可以帮助我们构建遵循 Material Design 设计规范的应用程序。Material 应用程序以MaterialApp (opens new window) 组件开始
比如Theme组件,它用于配置应用的主题,我们已经使用过多个 Material 组件了,如:Scaffold、AppBar、TextButton等
import 'package:flutter/material.dart';
Flutter 也提供了一套丰富的 Cupertino 风格的组件,尽管目前还没有 Material 组件那么丰富
//导入cupertino widget 库
import 'package:flutter/cupertino.dart';
class CupertinoTestRoute extends StatelessWidget {
@override
widget build(BuildContext context) {
return CupertinoPageScaffold(
navigationBar: CupertinoNavigationBar(
middle: Text("Cupertino Demo"),
),
child: Center(
child: CupertinoButton(
color: CupertinoColors.activeBlue,
child: Text("Press"),
onPressed: () {}
),
),
);
}
}
由于 Material 和 Cupertino 都是在基础组件库之上的,所以如果我们的应用中引入了这两者之一,则不需要再引入flutter/ widgets.dart了,因为它们内部已经引入过了。
如果不确定到底该怎么管理状态,那么推荐的首选是在父 Widget 中管理
全局状态管理 如 Provider、Redux
路由入栈(push)操作对应打开一个新页面,路由出栈(pop)操作对应页面关闭操作,而路由管理主要是指如何来管理路由栈
//导航到新路由
Navigator.push(
context,
MaterialPageRoute(builder: (context) {
return NewRoute();
})
MaterialPageRoute继承自PageRoute类,PageRoute类是一个抽象类
MaterialPageRoute 构造函数的各个参数的意义
MaterialPageRoute({
WidgetBuilder builder,
RouteSettings settings,
bool maintainState = true,
bool fullscreenDialog = false,
})
builder 是一个WidgetBuilder类型的回调函数,它的作用是构建路由页面的具体内容,返回值是一个widget。我们通常要实现此回调,返回新路由的实例。
settings 包含路由的配置信息,如路由名称、是否初始路由(首页)。
maintainState:默认情况下,当入栈一个新路由时,原来的路由仍然会被保存在内存中,如果想在路由没用的时候释放其所占用的所有资源,可以设置maintainState为 false。
fullscreenDialog表示新的路由页面是否是一个全屏的模态对话框,在 iOS 中,如果fullscreenDialog为true,新页面将会从屏幕底部滑入(而不是水平方向)。
如果想自定义路由切换动画,可以自己继承 PageRoute 来实现,我们将在后面介绍动画时,实现一个自定义的路由组件
Navigator是一个路由管理的组件,它提供了打开和退出路由页方法。
最常用的两个方法
//返回值是一个Future对象,用以接收新路由出栈(即关闭)时的返回数据
Future push(BuildContext context, Route route)
//将栈顶路由出栈,result 为页面关闭时返回给上一个页面的数据
bool pop(BuildContext context, [ result ])
Navigator类中第一个参数为context的静态方法都对应一个Navigator的实例方法, 比如Navigator.push(BuildContext context, Route route)等价于Navigator.of(context).push(Route route)
先定义新页面TipRoute
class TipRoute extends StatelessWidget {
TipRoute({
Key key,
required this.text, // 接收一个text参数
}) : super(key: key);
final String text;
// 打开`TipRoute`,并等待返回结果
var result = Navigator.push(
context,
MaterialPageRoute(
builder: (context) {
return TipRoute(
// 路由参数
text: "我是提示xxxx",
);
},
),
);
//输出`TipRoute`路由返回结果
print("路由返回值: $result");
Navigator.pop(context, "我是返回值")
给路由起一个名字,通过路由名字直接打开新的路由
Map<String, WidgetBuilder> routes;
MaterialApp(
title: 'Flutter Demo',
initialRoute:"/", //名为"/"的路由作为应用的home(首页)
theme: ThemeData(
primarySwatch: Colors.blue,
),
//注册路由表
routes:{
"new_page":(context) => NewRoute(),
"/":(context) => MyHomePage(title: 'Flutter Demo Home Page'), //注册首页路由
}
);
//有参数注册路由表
MaterialApp(
... //省略无关代码
routes: {
"tip2": (context){
return TipRoute(text: ModalRoute.of(context)!.settings.arguments);
},
},
);
通过路由名打开新路由页
Future pushNamed(BuildContext context, String routeName,{Object arguments})
//无参打开
Navigator.pushNamed(context, "new_page");
//带参数打开新页面
Navigator.of(context).pushNamed("new_page", arguments: "hi");
//通过RouteSetting获取路由参数
var args=ModalRoute.of(context).settings.arguments;
需求:登录前可以看商品,登录后才能看订单
MaterialApp有一个onGenerateRoute属性。
Navigator.pushNamed(…)打开命名路由时,如果路由表中没有注册,会调用onGenerateRoute来生成路由
MaterialApp(
... //放弃使用路由表,使用此方法动态跳转
onGenerateRoute:(RouteSettings settings){
return MaterialPageRoute(builder: (context){
String routeName = settings.name;
// 如果访问的路由页需要登录,但当前未登录,则直接返回登录页路由,
// 其他情况则正常打开路由。
}
);
}
);
onGenerateRoute 只会对命名路由生效
Flutter 使用pubspec.yaml来管理第三方依赖包。
name: flutter_in_action
description: First Flutter Application.
version: 1.0.0+1
dependencies:
flutter:
sdk: flutter
cupertino_icons: ^0.1.2
dev_dependencies:
flutter_test:
sdk: flutter
flutter:
uses-material-design: true
各个字段的意义:
name:应用或包名称。
description: 应用或包的描述、简介。
version:应用或包的版本号。
dependencies:应用或包依赖的其他包或插件。
dev_dependencies:开发环境依赖的工具包(而不是flutter应用本身依赖的包)。
flutter:flutter相关的配置选项。
如何添加、下载并使用第三方包?
仓库:https://pub.dev/
比如添加english_words:https://pub.dev/packages/english_words/install
dependencies:
english_words: ^4.0.0
之后执行flutter packages get 命令来下载依赖包
或者
在pubspec.yaml单击右上角的 Pub get
上面依赖方式是依赖Pub仓库的,我们还可以依赖本地包和git仓库。
//依赖本地包
dependencies:
pkg1:
path: ../../code/pkg1
//依赖git包(假设包在根目录)
dependencies:
pkg1:
git:
url: git://github.com/xxx/pkg1.git
//依赖git包(假设包不在根目录可以指定目录)
dependencies:
package1:
git:
url: git://github.com/flutter/packages.git
path: packages/package1
更多依赖方式:https://dart.dev/tools/pub/dependencies
Flutter 也使用pubspec.yaml 来管理资源
//asset的实际目录可以是任意文件夹
flutter:
assets:
- assets/my_icon.png
- assets/background.png
assets构建过程中,会在相邻子目录中查找具有相同名称的任何文件
建议使用 DefaultAssetBundle (opens new window)来获取当前 BuildContext 的AssetBundle
//加载图片,返回ImageProvider
AssetImage('graphics/background.png')
//或者,返回widget
Image.asset('graphics/background.png')
//假设您的应用程序依赖于一个名为“my_icons”的包:…/icons/heart.png
AssetImage('icons/heart.png', package: 'my_icons')
Image.asset('icons/heart.png', package: 'my_icons')
如果要给我们的应用设置APP图标或者添加启动图,那我们必须使用特定平台的assets
Android导航到…/android/app/src/main/res目录设置
IOS 导航到…/ios/Runner/Assets.xcassets/AppIcon.appiconset
如果不在main()方法中调用runApp 启动屏幕将永远持续显示。
print: 输出到系统控制台
debugPrint:如果你一次输出太多,那么Android有时会丢弃一些日志行。为了避免这种情况,我们可以使用Flutter的foundation库中的debugPrint()
flutter logs:查看日志
插入编程式断点使用debugger
要转储Widgets树的状态,请调用debugDumpApp()
I/flutter ( 6559): WidgetsFlutterBinding - CHECKED MODE
I/flutter ( 6559): RenderObjectToWidgetAdapter<RenderBox>([GlobalObjectKey RenderView(497039273)]; renderObject: RenderView)
I/flutter ( 6559): └MaterialApp(state: _MaterialAppState(1009803148))
I/flutter ( 6559): └ScrollConfiguration()
I/flutter ( 6559): └AnimatedTheme(duration: 200ms; state: _AnimatedThemeState(543295893; ticker inactive; ThemeDataTween(ThemeData(Brightness.light Color(0xff2196f3) etc...) → null)))
I/flutter ( 6559): └Theme(ThemeData(Brightness.light Color(0xff2196f3) etc...))
I/flutter ( 6559): └WidgetsApp([GlobalObjectKey _MaterialAppState(1009803148)]; state: _WidgetsAppState(552902158))
I/flutter ( 6559): └CheckedModeBanner()
I/flutter ( 6559): └Banner()
I/flutter ( 6559): └CustomPaint(renderObject: RenderCustomPaint)
I/flutter ( 6559): └DefaultTextStyle(inherit: true; color: Color(0xd0ff0000); family: "monospace"; size: 48.0; weight: 900; decoration: double Color(0xffffff00) TextDecoration.underline)
I/flutter ( 6559): └MediaQuery(MediaQueryData(size: Size(411.4, 683.4), devicePixelRatio: 2.625, textScaleFactor: 1.0, padding: EdgeInsets(0.0, 24.0, 0.0, 0.0)))
I/flutter ( 6559): └LocaleQuery(null)
I/flutter ( 6559): └Title(color: Color(0xff2196f3))
如果我们尝试调试布局问题,那么Widget树可能不够详细。在这种情况下,我们可以通过调用debugDumpRenderTree()转储渲染树
I/flutter ( 6559): RenderView
I/flutter ( 6559): │ debug mode enabled - android
I/flutter ( 6559): │ window size: Size(1080.0, 1794.0) (in physical pixels)
I/flutter ( 6559): │ device pixel ratio: 2.625 (physical pixels per logical pixel)
I/flutter ( 6559): │ configuration: Size(411.4, 683.4) at 2.625x (in logical pixels)
I/flutter ( 6559): │
I/flutter ( 6559): └─child: RenderCustomPaint
I/flutter ( 6559): │ creator: CustomPaint ← Banner ← CheckedModeBanner ←
I/flutter ( 6559): │ WidgetsApp-[GlobalObjectKey _MaterialAppState(1009803148)] ←
I/flutter ( 6559): │ Theme ← AnimatedTheme ← ScrollConfiguration ← MaterialApp ←
I/flutter ( 6559): │ [root]
I/flutter ( 6559): │ parentData: <none>
I/flutter ( 6559): │ constraints: BoxConstraints(w=411.4, h=683.4)
I/flutter ( 6559): │ size: Size(411.4, 683.4)
当调试布局问题时,关键要看的是size和constraints字段。约束沿着树向下传递,尺寸向上传递
渲染树是可以分层的,而最终绘制需要将不同的层合成起来,而Layer则是绘制时需要合成的层,如果我们尝试调试合成问题,则可以使用debugDumpLayerTree()
I/flutter : TransformLayer
I/flutter : │ creator: [root]
I/flutter : │ offset: Offset(0.0, 0.0)
I/flutter : │ transform:
I/flutter : │ [0] 3.5,0.0,0.0,0.0
I/flutter : │ [1] 0.0,3.5,0.0,0.0
I/flutter : │ [2] 0.0,0.0,1.0,0.0
I/flutter : │ [3] 0.0,0.0,0.0,1.0
I/flutter : │
I/flutter : ├─child 1: OffsetLayer
I/flutter : │ │ creator: RepaintBoundary ← _FocusScope ← Semantics ← Focus-[GlobalObjectKey MaterialPageRoute(560156430)] ← _ModalScope-[GlobalKey 328026813] ← _OverlayEntry-[GlobalKey 388965355] ← Stack ← Overlay-[GlobalKey 625702218] ← Navigator-[GlobalObjectKey _MaterialAppState(859106034)] ← Title ← ⋯
I/flutter : │ │ offset: Offset(0.0, 0.0)
I/flutter : │ │
I/flutter : │ └─child 1: PictureLayer
I/flutter : │
I/flutter : └─child 2: PictureLayer
要找出相对于帧的开始/结束事件发生的位置,可以切换debugPrintBeginFrameBanner (opens new window)和debugPrintEndFrameBanner (opens new window)布尔值以将帧的开始和结束打印到控制台。
我们也可以通过设置debugPaintSizeEnabled为true以可视方式调试布局问题。 这是来自rendering库的布尔值。它可以在任何时候启用,并在为true时影响绘制。 设置它的最简单方法是在void main()的顶部设置。
调试动画最简单的方法是减慢它们的速度。为此,请将timeDilation (opens new window)变量(在scheduler库中)设置为大于1.0的数字,例如50.0。 最好在应用程序启动时只设置一次
要了解我们的应用程序导致重新布局或重新绘制的原因,我们可以分别设置debugPrintMarkNeedsLayoutStacks (opens new window)和 debugPrintMarkNeedsPaintStacks (opens new window)标志。 每当渲染盒被要求重新布局和重新绘制时,这些都会将堆栈跟踪记录到控制台。如果这种方法对我们有用,我们可以使用services库中的debugPrintStack()方法按需打印堆栈痕迹
$ flutter run --trace-startup --profile
Flutter DevTools 是 Flutter 可视化调试工具
在 Java 和 Objective-C(以下简称“OC”)中,如果程序发生异常且没有被捕获,那么程序将会终止,但是这在Dart或JavaScript中则不会!这和它们的运行机制有关系。
Java 和 OC 都是多线程模型的编程语言,任意一个线程触发异常且该异常未被捕获时,就会导致整个进程退出。但 Dart 和 JavaScript 不会,它们都是单线程模型
Dart 在单线程中是以消息循环机制来运行的,其中包含两个任务队列,一个是“微任务队列” microtask queue,另一个叫做“事件队列” event queue。从图中可以发现,微任务队列的执行优先级高于事件队列。
Dart线程运行过程,如上图中所示,入口函数 main() 执行完后,消息循环机制便启动了。首先会按照先进先出的顺序逐个执行微任务队列中的任务,事件任务执行完毕后程序便会退出,但是,在事件任务执行的过程中也可以插入新的微任务和事件任务,在这种情况下,整个线程的执行过程便是一直在循环,不会退出,而Flutter中,主线程的执行过程正是如此,永不终止。
在Dart中,所有的外部事件任务都在事件队列中,如IO、计时器、点击、以及绘制事件等,而微任务通常来源于Dart内部,并且微任务非常少,之所以如此,是因为微任务队列优先级高,如果微任务太多,执行时间总和就越久,事件队列任务的延迟也就越久,对于GUI应用来说最直观的表现就是比较卡,所以必须得保证微任务队列不会太长。值得注意的是,我们可以通过Future.microtask(…)方法向微任务队列插入一个任务。
在事件循环中,当某个任务发生异常并没有被捕获时,程序并不会退出,而直接导致的结果是当前任务的后续代码就不会被执行了,也就是说一个任务中的异常是不会影响其他任务执行的
Dart中可以通过try/catch/finally来捕获代码块异常
@override
void performRebuild() {
...
try {
//执行build方法
built = build();
} catch (e, stack) {
// 有异常时则弹出错误提示
built = ErrorWidget.builder(_debugReportException('building $this', e, stack));
}
...
}
在Dart中,异常分两类:同步异常和异步异常,同步异常可以通过try/catch捕获,而异步异常则比较麻烦,如下面的代码是捕获不了Future的异常的:
try{
Future.delayed(Duration(seconds: 1)).then((e) => Future.error("xxx"));
}catch (e){
print(e)
}
Dart中有一个runZoned(…) 方法,Zone中可以捕获日志输出、Timer创建、微任务调度的行为,同时Zone也可以捕获所有未处理的异常
//我们 APP 中所有调用print方法输出日志的行为都会被拦截,通过这种方式,我们也可以在应用中记录日志,等到应用触发未捕获的异常时,将异常信息和日志统一上报。
另外我们还拦截了未被捕获的异步错误,这样一来,结合上面的 FlutterError.onError 我们就可以捕获我们Flutter应用错误了并进行上报了!
void collectLog(String line){
... //收集日志
}
void reportErrorAndLog(FlutterErrorDetails details){
... //上报错误和日志逻辑
}
FlutterErrorDetails makeDetails(Object obj, StackTrace stack){
...// 构建错误信息
}
void main() {
var onError = FlutterError.onError; //先将 onerror 保存起来
FlutterError.onError = (FlutterErrorDetails details) {
onError?.call(details); //调用默认的onError
reportErrorAndLog(details); //上报
};
runZoned(
() => runApp(MyApp()),
zoneSpecification: ZoneSpecification(
// 拦截print
print: (Zone self, ZoneDelegate parent, Zone zone, String line) {
collectLog(line);
parent.print(zone, "Interceptor: $line");
},
// 拦截未处理的异步错误
handleUncaughtError: (Zone self, ZoneDelegate parent, Zone zone,
Object error, StackTrace stackTrace) {
reportErrorAndLog(details);
parent.print(zone, '${error.toString()} $stackTrace');
},
),
);
}