我主要的Flutter知识来源并不是知识繁多俱全的官方文档,也不是视频软件上的教程(讲的内容感觉太少不全面),而是一位大佬的电子书《Flutter实战·第二版》。这本书循环渐进的讲了Flutter的运行原理和常用组件,有解释有源码,是我看过的关于Flutter的最好的一本书。是这本书带我走进Flutter的大门,我打心底里非常感谢这位大佬,因为这本书是免费的,代码也是开源的,在这个知识付费的时代堪称一股清流。
这本书的解释很专业,大佬不愧为大佬,我请教过老师和学长,不过对我最有用的还得是Chat GPT😏。刚刚入门的我有很多地方花了很长时间,快学完才发现电子书的版序中有这么一段话:
本书读者对象
- 读者至少熟悉一种编程语言。
- 读者最好接触过PC客户端、移动开发或Web前端开发中的一种。
- 本书不适合做为编程的入门读物。
😂但是我结合了电子书和Chat GPT以及其他大佬的帮助还是克服了困难,总结成了自己的一篇笔记,其中引用了很多书中的知识和代码,为此按照大佬书中的要求注明出处及作者信息并附上本书网址。
文章参考了Flutter中国开源项目发起人杜文(网名wendux)创作的一本系统介绍Flutter技术的中文书籍《Flutter实战·第二版》,网址:第二版序 | 《Flutter实战·第二版》 https://book.flutterchina.club/#第二版变化
但是我的这个笔记的阅读对象最好也是上面的…
总之我的建议是先去看视频软件上的教程(视频自己找吧,B站一堆),看完再过来看笔记,视频虽然(讲的内容感觉太少不全面),但是有些视频面向的就是入门小白,讲师讲的非常小白化,看完视频再来翻翻与视频对应的笔记就能理解很多东西。
这是我Flutter的第一篇笔记,我用了大量文字和代码来解释,但还是觉得大部分人看了可能都会吃个闭门羹🫠,而且第二篇笔记还会雪中送炭。所以看不懂的地方可以跳过,等用到了再过来研究,那时候自然也就看懂了🙃。下面是电子书中大佬的建议:
学习资源
- 官网:阅读Flutter官网的资源是快速入门的最佳方式,同时官网也是了解最新Flutter发展动态的地方,由于目前 Flutter 仍然处于快速发展阶段,所以建议读者还是时不时的去官网看看有没有新的动态。
- 源码及注释:源码注释应作为学习 Flutter 的第一文档,Flutter SDK 的源码是包含在 Flutter 工程中的,并且注释非常详细且有很多示例,我们可以通过 IDE 的跳转功能快速定位到源码。实际上,Flutter 官方的组件文档就是通过注释生成的。根据笔者经验,源码结合注释可以帮我们解决大多数问题。
- Github:如果遇到的问题在StackOverflow上也没有找到答案,可以去 Github flutter 项目下提 issue。
- Gallery源码:Gallery 是 Flutter 官方示例 APP,里面有丰富的示例,读者可以在网上下载安装。Gallery 的源码在 Flutter 源码 “examples” 目录下。
- StackOverflow: StackOverflow 是目前全球最大的程序员问答社区,现在也是活跃度最高的 Flutter 问答社区。StackOverflow 上面除了世界各地的 Flutter开发者会在上面交流之外,Flutter 开发团队的成员也经常会在上面回答问题。
一、Flutter框架结构
Flutter 从上到下可以分为三层:框架层、引擎层和嵌入层
1.框架层
Flutter Framework,即框架层。这是一个纯 Dart 实现的 SDK,它实现了一套基础库,自底向上介绍下:
- 底下两层(Foundation 和 Animation、Painting、Gestures)在 Google 的一些视频中被合并为一个dart UI层,对应的是Flutter中的
dart:ui
包,它是 Flutter Engine 暴露的底层UI库,提供动画、手势及绘制能力。 - Rendering 层,即渲染层,这一层是一个抽象的布局层,它依赖于 Dart UI 层,渲染层会构建一棵由可渲染对象组成的渲染树,当动态更新这些对象时,渲染树会找出变化的部分,然后更新渲染。渲染层可以说是Flutter 框架层中最核心的部分,它除了确定每个渲染对象的位置、大小之外还要进行坐标变换、绘制(调用底层 dart:ui )。
- Widgets 层是 Flutter 提供的一套基础组件库,在基础组件库之上,Flutter 还提供了 Material 和 Cupertino 两种视觉风格的组件库,它们分别实现了 Material 和 iOS 设计规范。
Flutter 框架相对较小,因为一些开发者可能会使用到的更高层级的功能已经被拆分到不同的软件包中,使用 Dart 和 Flutter 的核心库实现,其中包括平台插件,例如 [camera 和 [webview ,以及和平台无关的功能,例如 [animations 。
我们进行Flutter 开发时,大多数时候都是和 Flutter Framework 打交道。
2.引擎层
Engine,即引擎层。毫无疑问是 Flutter 的核心, 该层主要是 C++ 实现,其中包括了 Skia 引擎、Dart 运行时(Dart runtime)、文字排版引擎等。在代码调用 dart:ui
库时,调用最终会走到引擎层,然后实现真正的绘制和显示。
3.嵌入层
- Embedder,即嵌入层。Flutter 最终渲染、交互是要依赖其所在平台的操作系统 API,嵌入层主要是将 Flutter 引擎 ”安装“ 到特定平台上。
- 嵌入层采用了当前平台的语言编写,例如 Android 使用的是 Java 和 C++, iOS 和 macOS 使用的是 Objective-C 和 Objective-C++,Windows 和 Linux 使用的是 C++。
- Flutter 代码可以通过嵌入层,以模块方式集成到现有的应用中,也可以作为应用的主体。Flutter 本身包含了各个常见平台的嵌入层,假如以后 Flutter 要支持新的平台,则需要针对该新的平台编写一个嵌入层。
二、Flutter入门
- 在 Flutter 中,大多数东西都是 widget(后同“组件”或“部件”),包括对齐(Align)、填充(Padding)、手势处理(GestureDetector)等,它们都是以 widget 的形式提供。
1.文件目录
文件夹 | 作用 |
---|---|
android | android平台相关代码 |
ios | ios平台相关代码 |
linux | Linux平台相关的代码 |
macos | macos平台相关的代码 |
web | web相关的代码 |
windows | windows相关的代码 |
lib | flutter相关代码,我们编写的代码就在这个文件夹 |
test | 用于存放测试代码 |
pubspec.yaml | 配置文件,一般存放一些第三方库的依赖。 |
analysis_options.yaml | 分析dart语法的文件,老项目升级成新项目有警告信息的话可以删掉此文件 |
2.入口文件详解
- 每一个flutter项目的lib目录里面都有一个
flutter
的入口文件main.dart
// main.dart
import 'package:flutter/material.dart';
// 导入 Material UI 组件库。Material (opens new window)是一种标准的移动端和web端的视觉设计语言, Flutter 默认提供了一套丰富的 Material 风格的UI组件。
// 应用入口,启动Flutter应用。runApp接受一个 Widget 参数(MyApp对象, MyApp()是 Flutter 应用的根组件)。
void main() {
runApp(const MyApp());
}
// 速写成void main() => runApp(const MyApp());
// main函数使用了(=>)符号,这是 Dart 中单行函数或方法的简写。
class MyApp extends StatelessWidget {
// MyApp 类代表 Flutter 应用,它继承了 StatelessWidget 类,这也就意味着应用本身也是一个widget。
const MyApp({Key? key}) : super(key: key);
Widget build(BuildContext context) {
// Flutter 在构建页面时,会调用组件的build方法,widget 主要提供一个 build() 方法来描述如何构建 UI 界面(通常是通过组合、拼装其他基础 widget )。
return MaterialApp(
// MaterialApp 是 Material 库中提供的 Flutter APP 框架,通过它可以设置应用的名称、主题、语言、首页及路由列表等。MaterialApp也是一个 widget。
title: 'Welcome to Flutter1',
home: Scaffold(
// home 为 Flutter 应用的首页,它也是一个 widget。
appBar: AppBar(
title: const Text('Welcome to Flutter2'),
),
body: const Center(
child: Text('Hello World'),
),
),
);
}
}
3.Flutter四棵树
Flutter 框架的处理流程:
- 根据 Widget 树生成一个 Element 树,Element 树(是 Widget 和 RenderObject 的中间代理)中的节点都继承自
Element
类。 - 根据 Element 树生成 Render 树(渲染树,负责布局和渲染逻辑),渲染树中的节点都继承自
RenderObject
类。 - 根据渲染树生成 Layer 树,然后上屏显示,Layer 树中的节点都继承自
Layer
类。
- 三棵树中,Widget 和 Element 是一一对应的,但并不和 RenderObject 一一对应。比如
StatelessWidget
和StatefulWidget
都没有对应的 RenderObject。 - 渲染树在上屏前会生成一棵 Layer 树。
4.Widget类
Flutter几乎所有对象都是一个 widget 。与原生开发中“控件”不同,它不仅可以表示UI元素,也可以表示一些功能性的组件,而原生开发中的控件通常只指UI元素。
Flutter 中是通过 Widget 嵌套 Widget 的方式来构建UI和进行事件处理,Flutter 中万物皆Widget。
Widget 可以定义为
- 一个界面组件(如按钮或输入框)
- 一个文本样式(如字体或颜色)
- 一种布局(如填充或滚动)
- 一种动画处理(如缓动)
- 一种手势处理(Gesture Dtector)
Widget 类内容:
// 不可变的
// @immutable 代表 Widget 不可变,限制 Widget 中定义属性(即配置信息)必须不可变(final)
abstract class Widget extends DiagnosticableTree {
// widget 类继承的 DiagnosticableTree(诊断树) 主要作用是提供调试信息。
const Widget({ this.key });
final Key? key;
// key 属性类似 React/Vue 中的 key,决定是否在下一次build时复用旧的 widget,决定的条件在canUpdate()方法中
Element createElement();
// “一个 widget 对应多个 Element ”, Flutter 框架在构建UI树时会先调用此方法生成对应节点的 Element 对象。此方法在 Flutter 框架隐式调用,开发中基本不会调用。
String toStringShort() {
final String type = objectRuntimeType(this, 'Widget');
return key == null ? type : '$type-$key';
}
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
// debugFillProperties(...) 复写父类,主要设置诊断树一些特性。
super.debugFillProperties(properties);
properties.defaultDiagnosticsTreeStyle = DiagnosticsTreeStyle.dense;
}
bool operator ==(Object other) => super == other;
int get hashCode => super.hashCode;
// canUpdate(...)是一个静态方法,用于在 widget 树重新 build 时复用旧的 widget,既是否用新的 widget 对象去更新旧UI树上所对应的 Element 对象的配置;只要 newWidget 与 oldWidget 的 runtimeType 和 key 同时相等时就会用 new widget 去更新 Element 对象的配置,否则就会创建新的 Element 。
static bool canUpdate(Widget oldWidget, Widget newWidget) {
return oldWidget.runtimeType == newWidget.runtimeType
&& oldWidget.key == newWidget.key;
}
...
}
Flutter 开发中一般不直接继承Widget
类实现一个新组件,通常通过继承StatelessWidget
或StatefulWidget
来间接继承widget
类来实现。StatelessWidget
和StatefulWidget
都是直接继承自Widget
类,而这两个类也正是 Flutter 中非常重要的两个抽象类,它们引入了两种 widget 模型。
1)build构建组件
可重写 Widget 的 build 方法来构建一个组件。build 即为创建 Widget ,返回值也是 Widget 对象,不管返回的是单个组件还是返回通过嵌套的方式组合的组件都是 Widget 的实例。
Widget build(BuildContext context);
context
参数是BuildContext
类的一个实例,表示当前 widget 在 widget 树中的上下文,每一个 widget 都对应一个 context 对象(因为每一个 widget 都是 widget 树上的一个节点)。context
是当前 widget 在 整颗 widget 树中执行操作的一个句柄(handle),如它提供从当前 widget 开始向上遍历 widget 树及按照 widget 类型查找父级 widget 的方法:
// 在子树中获取父级 widget 的一个示例
class ContextRoute extends StatelessWidget {
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;
}),
),
);
}
}
2)属性和方法
- 属性通常用来改变组件的状态(颜色、大小等)及回调方法的处理(单击 件回调、手势事件 回调等)
- 方法主要是提供一些组件的功能扩展。
- widget 的属性应尽可能的被声明为
final
,防止被意外改变。
如 TextBox 一个矩形的文本组件属性及方法如下:
- bottom:底部间距属性
- direction:文本排列方向属性
- left:侧间距属性
- right:右侧间距属性
- top:上部间距属性
- toRect:导出矩形方法
- toString:转换成字符串方法
3)组件嵌套
- 功能界面常由简单功能的组件组装完成的,如负责布局,定位,调整大小,渐变处理等等的组件,这种嵌套组合的方式带来的最大好处就是解祸
- 如果 widget 需要接收子 widget ,那么
child
或children
参数通常应被放在参数列表的最后。 - 如界面中添加了一个居中组件 Center ,居中组件里嵌 了一个容器组件 Container,容器组件里嵌套了一个文本组件 Text 和一个装饰器 BoxDecoration
return new Center(
//添加容器
child: new Container(
//添加装饰器
decoration: new BoxDecoration(
),
child: new Text(
//添加文本组件
),
),
),
5.状态
- Widget 分为 有状态StatefulWidget 和 ⽆状态StatelessWidget 两种,在 Flutter 中每个⻚⾯都是⼀帧,⽆状态就是保持在那⼀帧,⽽有状态的 Widget 当数据更新时会绘制新的 Widget,由 State 实现跨帧的数据同步保存。
- 输⼊ stl 创建⽆状态控件的模板选项
- 输⼊ stf 创建有状态 Widget 的模板选项
1)Stateless
-
无状态组件 (Stateless Widget )不可变,属性和值固定,用于不需要维护状态的场景。
-
StatelessWidget
继承自widget
类,重写了createElement()
方法StatelessElement createElement() => StatelessElement(this);
使用:继承 StatelessWidget,通过 build ⽅法返回⼀个布局好的控件。
- 每个 Widget 间通过
child:
进⾏嵌套。- 有的 Widget 可有多个
child
既children:
,如 Column 布局 - 有的 Widget 只能有⼀个 child,如下⽅的
Container
。 Container Widget 嵌套了 Text Widget。
- 有的 Widget 可有多个
import 'package:flutter/material.dart';
class DEMOWidget extends StatelessWidget {
final String text;
//数据可以通过构造⽅法传递进来
DEMOWidget(this.text);
Widget build(BuildContext context) {
//这⾥返回你需要的控件
//这⾥末尾有没有的逗号,对于格式化代码⽽已是不⼀样的。
return Container(
//⽩⾊背景
color: Colors.white,
//Dart语法中,?? 表示如果text为空,就返回尾号后的内容。
child: Text(text ?? "这就是⽆状态DMEO"),
);
}
}
2)Stateful
-
有状态的 Widget 根据用户交互或其他因素进行更改。如计数器,当值发生变化时要重新构建 Widget 以更新 UI 。
-
有状态组件( StatefulWidget )持有的状态可能在 Widget 生命周期中发生实现 StatefulWidget 至少需要两个类:
- 一个 StatefulWidget 类。
- 一个State 类。 StatefulWidget 类本身不变,但 State 类在 Widget 生命周期中始终存在。
-
和
StatelessWidget
一样StatefulWidget
也继承自widget
类并重写createElement()
方法,但返回的Element
对象不同;另外StatefulWidget
类中添加了新的接口createState()
。
// StatefulWidget的类定义
abstract class StatefulWidget extends Widget {
const StatefulWidget({ Key key }) : super(key: key);
StatefulElement createElement() => StatefulElement(this);
// StatefulElement 间接继承自 Element 类,与 StatefulWidget 相对应(作为其配置数据)。StatefulElement 中可能会多次调用 createState() 来创建状态(State)对象。
// “从树中移除 State 对象”或“插入 State 对象到树中”的树指通过 widget 树生成的 Element 树。
State createState();
// createState() 用于创建和 StatefulWidget 相关的状态(State 实例),在StatefulWidget 的生命周期中可能会被多次调用。
// 如一个 StatefulWidget 同时插入到 widget 树的多个位置时 Flutter 会调用该方法为每一个位置生成一个独立的State 实例,本质上就是一个 StatefulElement 对应一个 State 实例。
}
- 有了独立的状态和 Widget 对象,其他 Widget 可以同样方式处理无状态和有状态的 Widget ,而不必担心丢失状态。
- 父 Widget 可自由创造子 Widget 的新实例且不会失去子 Widget 的状态,而不是通过持有 Widget 来维持其状态,框架在适当时完成查找和重用现有状态对象的所有工作。
3)RenderObject
在Flutter中,RenderObject 是一种用于控制尺寸、布局和绘制的对象,它对于将小部件绘制到屏幕上并形成应用程序的用户界面(UI)非常重要。让我们深入了解一下 RenderObject 的作用和重要性。
- Widgets 和 Elements:
- 在Flutter中,我们经常听到“一切都是小部件”这样的说法。实际上在屏幕上看到的所有内容,包括文本、图像等,都是小部件。
- 小部件包含配置信息,但不直接使用这些信息。它们被实例化并转换为元素(Element),然后插入到元素树中。
- 每个元素都与一个 RenderObject 相关联。
- RenderObjects 的作用:
- RenderObjects 负责控制小部件的尺寸、布局和绘制逻辑,从而形成我们在屏幕上看到的UI。
- 它们实际上是小部件的渲染引擎,处理诸如大小调整、绘制到屏幕上以及其他参数的操作。
- 尽管大多数情况下不需要直接使用 RenderObjects,但了解它们对于构建高质量的移动应用程序至关重要。
- RenderObjects 的使用场景:
- 自定义布局:如果需要创建自定义布局,例如自定义容器或特定的绘制效果,RenderObjects 可能会派上用场。
- 高度定制的绘制:需要更精细的绘制控制时,RenderObjects 可以实现。
- 优化性能:RenderObjects 可以优化布局和绘制过程,提高应用程序的性能。
总之,RenderObjects 是Flutter中的关键概念,它们在小部件和屏幕之间扮演着重要角色,确保应用程序的UI正确渲染和交互。
StatelessWidget
和StatefulWidget
都用于组合其他组件,本身没有对应的 RenderObject。- Flutter 组件库中很多基础组件都不是通过
StatelessWidget
和StatefulWidget
来实现,如 Text 、Column、Align等,就好比搭积木,StatelessWidget
和StatefulWidget
可以将积木搭成不同的样子,但前提是得有积木,而这些积木都是通过自定义 RenderObject 来实现的。 - 实际上 Flutter 最原始的定义组件的方式就是通过定义RenderObject 来实现,而
StatelessWidget
和StatefulWidget
只是提供的两个帮助类。下面简单演示通过RenderObject定义组件的方式:
class CustomWidget extends LeafRenderObjectWidget{
RenderObject createRenderObject(BuildContext context) {
// 创建 RenderObject
return RenderCustomObject();
}
void updateRenderObject(BuildContext context, RenderCustomObject renderObject) {
// 更新 RenderObject
super.updateRenderObject(context, renderObject);
}
}
class RenderCustomObject extends RenderBox{
void performLayout() {
// 实现布局逻辑
}
void paint(PaintingContext context, Offset offset) {
// 实现绘制
}
}
如果组件不包含子组件则可直接继承自 LeafRenderObjectWidget ,它是 RenderObjectWidget 的子类,而 RenderObjectWidget 继承自 Widget :
// 实现RenderObjectWidget
abstract class LeafRenderObjectWidget extends RenderObjectWidget {
const LeafRenderObjectWidget({ Key? key }) : super(key: key);
// 帮 widget 实现 createElement 方法,它会为组件创建一个类型为 LeafRenderObjectElement 的 Element对象。如果自定义的 widget 可以包含子组件,则可以根据子组件的数量来选择继承SingleChildRenderObjectWidget 或 MultiChildRenderObjectWidget,它们也实现了createElement() 方法,返回不同类型的 Element 对象。
LeafRenderObjectElement createElement() => LeafRenderObjectElement(this);
}
- 重写的 createRenderObject 方法是 RenderObjectWidget 中定义方法,该方法被组件对应的 Element 调用(构建渲染树时)用于生成渲染对象。
- 主要任务就是实现 createRenderObject 返回的渲染对象类,本例中是 RenderCustomObject 。updateRenderObject 方法是用于在组件树状态发生变化但不需要重新创建 RenderObject 时用于更新组件渲染对象的回调。
- RenderCustomObject 类是继承自 RenderBox,而 RenderBox 继承自 RenderObject,我们需要在 RenderCustomObject 中实现布局、绘制、事件响应等逻辑
6.State类
一个 StatefulWidget 类对应一个 State 类,State表示与其对应的 StatefulWidget 要维护的状态,State 中的保存的状态信息可以:
- 在 widget 构建时可以被同步读取。
- 在 widget 生命周期中可以被改变,当State被改变时,可手动调用其
setState()
方法通知Flutter 框架状态发生改变,Flutter 框架在收到消息后,会重新调用其build
方法重新构建 widget 树,从而达到更新UI的目的。
State 中有两个常用属性:
widget
- 表示与该 State 实例关联的 widget 实例,由Flutter 框架动态设置。关联并非永久,因为在应用生命周期中UI树上的某一个节点的 widget 实例在重新构建时可能会变化,但State实例只会在第一次插入到树中时被创建,当在重新构建时,如果 widget 被修改了,Flutter 框架会动态设置
State.widget
为新的 widget 实例。
- 表示与该 State 实例关联的 widget 实例,由Flutter 框架动态设置。关联并非永久,因为在应用生命周期中UI树上的某一个节点的 widget 实例在重新构建时可能会变化,但State实例只会在第一次插入到树中时被创建,当在重新构建时,如果 widget 被修改了,Flutter 框架会动态设置
context
- StatefulWidget 对应的 BuildContext,作用同 StatelessWidget 的BuildContext。
1)计数器详解
以下是构建项目自带的计数器:
import 'package:flutter/material.dart';
void main() => runApp(const MyApp());
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: const MyHomePage(title: 'Flutter Demo Home Page'),
);
}
}
class MyHomePage extends StatefulWidget {
// _MyHomePageState类是MyHomePage类对应的状态类。
const MyHomePage({Key? key, required this.title}) : super(key: key);
final String title;
_MyHomePageState createState() => _MyHomePageState();
// MyHomePage类中没有build方法
}
class _MyHomePageState extends State<MyHomePage> {
int _counter = 0;
// 维护一个点击次数计数器,所以定义一个_counter状态,记录按钮点击总次数
void _incrementCounter() { // 设置状态的自增函数。
// 当按钮点击时调用此函数,先自增 _counter 然后调用setState 方法。
setState(() {
// setState 会通知 Flutter 有状态发生改变,Flutter 收到后执行 build 方法来根据新的状态重新构建界面,无需分别去修改各个 widget。
_counter++;
});
}
Widget build(BuildContext context) {
// build方法移动到了这里
// 构建UI界面的逻辑在 build 方法中,MyHomePage第一次创建时_MyHomePageState类会被创建,初始化完成后 Flutter 框架调用 widget 的 build 方法构建 widget 树并渲染到设备屏幕上。
return Scaffold(
// Scaffold 是 Material 库中提供的页面脚手架,提供默认的导航栏、标题和包含主屏幕 widget 树(后同“组件树”或“部件树”)的body属性,路由默认都是通过Scaffold创建。
appBar: AppBar(
title: Text(widget.title),
),
body: Center(
// body的组件树中包含了一个Center 组件,Center 可将其子组件树对齐到屏幕中心。
child: Column(
// Center 子组件是一个Column 组件,作用是将其所有子组件沿屏幕垂直方向依次排列;
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
// Column 子组件是两个 Text,第一个Text 显示单引号固定文本,第二个 Text 显示 _counter 状态数值。
const Text('You have pushed the button this many times:'),
Text(
'$_counter',
style: Theme.of(context).textTheme.headlineMedium,
),
],
),
),
floatingActionButton: FloatingActionButton(
// floatingActionButton是页面右下角的带“+”的悬浮按钮
onPressed: _incrementCounter,
// onPressed属性接受一个回调函数,代表它被点击后的处理器,这里直接将_incrementCounter方法作为其处理函数。
tooltip: 'Increment',
child: const Icon(Icons.add),
),
);
}
}
当右下角的floatingActionButton
按钮被点击之后,会调用_incrementCounter
方法。在_incrementCounter
方法中,首先会自增_counter
计数器(状态),然后setState
会通知 Flutter 框架状态发生变化,接着,Flutter 框架会调用build
方法以新的状态重新构建UI,最终显示在设备屏幕上。
2)再谈build()
上述 build()
方法放在 State 而不是StatefulWidget
中主要为了提高开发的灵活性。将build()
方法放在StatefulWidget
中会有两个问题:
-
状态访问不便
-
如果
StatefulWidget
有很多状态,每次状态改变都要调用build
,此时状态保存在 State 类中,而build
方法在StatefulWidget
类中,build
方法和状态分别在两个类中构建时读取状态很不方便! -
若将
build
方法放在 StatefulWidget 中,由于构建用户界面过程需要依赖 State,所以build
方法将必须加一个State
参数,此时只能将State的所有状态声明为公开的状态才能在State类外部访问状态!将状态设置为公开后状态不再具有私密性,会导致对状态的修改变的不可控。Widget build(BuildContext context, State state){ //state.counter ... }
-
如果将
build()
方法放在State中的,构建过程不仅可直接访问状态,而且也无需公开私有状态,非常方便。
-
-
继承
StatefulWidget
不便- 如 Flutter 中有一个动画 widget 的基类
AnimatedWidget
继承自StatefulWidget
类。AnimatedWidget
中引入了一个抽象方法build(BuildContext context)
- 继承自
AnimatedWidget
的动画 widget 都要实现这个build
方法。如果StatefulWidget
类中已有一个build
方法,此时这个build
方法需要接收一个 State 对象,这就意味着AnimatedWidget
必须将自己的 State 对象(记为_animatedWidgetState)提供给其子类,因为子类需要在其build
方法中调用父类的build
方法, - 而
AnimatedWidget
的状态对象是AnimatedWidget
内部实现细节,不应该暴露给外部。如果要将父类状态暴露给子类,那必须得有一种传递机制,而做这一套传递机制是无意义的,因为父子类间状态的传递和子类本身逻辑无关。
- 如 Flutter 中有一个动画 widget 的基类
3)setState()
-
每当改变一个 State 对象时(例如增加计数器)必须调用 setState() 来通知框架,框架会再次调用 State 构建方法更新用户界面。
void _incrementCounter() { setState(() { _counter++; }); }
通过 State 的 build ⽅法去构建控件。 State 中可动态改变数据,在 setState 之后改变的数据触发 Widget 重新构建刷新。
4)State生命周期
- State 中主要的生命周期有 :
- initState :初始化,理论上只有初始化⼀次,特殊情况除外。
- didChangeDependencies:在 initState 之后调⽤,此时可获取其他 State 。
- dispose :销毁,只会调⽤⼀次。
State 回调函数:
initState
:当 widget 第一次插入到 widget 树时会被调用,对于每一个State对象,Flutter 框架只会调用一次该回调,所以通常在该回调中做一些一次性的操作,如状态初始化、订阅子树的事件通知等。- 不能在该回调中调用
BuildContext.dependOnInheritedWidgetOfExactType
(该方法用于在 widget 树上获取离当前 widget 最近的一个父级InheritedWidget
),原因是在初始化完成后, widget 树中的InheritFrom widget
也可能会发生变化,所以正确的做法应该在在build()
方法或didChangeDependencies()
中调用它。
- 不能在该回调中调用
didChangeDependencies()
:当State对象的依赖发生变化时会被调用;如在之前build()
中包含了一个InheritedWidget
,然后在之后的build()
中Inherited widget
发生了变化,那么此时InheritedWidget
的子 widget 的didChangeDependencies()
回调都会被调用。- 典型的场景是当系统语言 Locale 或应用主题改变时,Flutter 框架会通知 widget 调用此回调。需要注意,组件第一次被创建后挂载的时候(包括重创建)对应的
didChangeDependencies
也会被调用。
- 典型的场景是当系统语言 Locale 或应用主题改变时,Flutter 框架会通知 widget 调用此回调。需要注意,组件第一次被创建后挂载的时候(包括重创建)对应的
build()
:此回调主要用于构建 widget 子树,会在如下场景被调用:- 在调用
initState()
之后。 - 在调用
didUpdateWidget()
之后。 - 在调用
setState()
之后。 - 在调用
didChangeDependencies()
之后。 - 在State对象从树中一个位置移除后(会调用deactivate)又重新插入到树的其他位置之后。
- 在调用
reassemble()
:此回调专门为开发调试而提供,在热重载(hot reload)时会被调用,此回调在Release模式下永远不会被调用。didUpdateWidget()
:在 widget 重新构建时,Flutter 框架会调用widget.canUpdate
来检测 widget 树中同一位置的新旧节点,然后决定是否需要更新,如果widget.canUpdate
返回true
则会调用此回调。widget.canUpdate
会在新旧 widget 的key
和runtimeType
同时相等时会返回true,也就是说在在新旧 widget 的key和runtimeType同时相等时didUpdateWidget()
就会被调用。
deactivate()
:当 State 对象从树中被移除时,会调用此回调。在一些场景下,Flutter 框架会将 State 对象重新插到树中,如包含此 State 对象的子树在树的一个位置移动到另一个位置时(可以通过GlobalKey 来实现)。如果移除后没有重新插入到树中则紧接着会调用dispose()
方法。dispose()
:当 State 对象从树中被永久移除时调用;通常在此回调中释放资源。
注意:在继承StatefulWidget
重写其方法时,对于包含@mustCallSuper
标注的父类方法,都要在子类方法中调用父类方法。
5)树中获取对象
StatefulWidget 的具体逻辑都在其 State 中,需要获取 StatefulWidget 对应的State 对象来调用一些方法,如Scaffold
组件对应的状态类ScaffoldState
定义的打开 SnackBar(路由页底部提示条)的方法。有两种方法在子 widget 树中获取父级 StatefulWidget 的State 对象。
context获取
context
是一个非常重要的概念,它表示**构建部件树的上下文环境*:
- 什么是
context
?- 在Flutter中,
context
是一个对象,它包含了有关当前部件的信息,例如部件的位置、主题、父级部件等。 - 每个部件都有一个关联的
context
,用于在部件树中定位和访问其他部件。
- 在Flutter中,
context
的作用:- 构建部件树:
context
用于构建部件树。当你在build
方法中创建部件时,你会使用context
来访问父级部件、主题、局部化信息等。 - 导航:通过
context
,你可以使用Navigator
来导航到其他页面。 - 访问主题和样式:你可以使用
Theme.of(context)
来获取当前主题的样式。 - 访问局部化信息:通过
Localizations.of(context, ...)
,你可以获取本地化信息。
- 构建部件树:
context
对象有一个findAncestorStateOfType()
方法,该方法可从当前节点沿着 widget 树向上查找指定类型的 StatefulWidget 对应的 State 对象。下面是实现打开 SnackBar 的示例:
import 'package:flutter/material.dart';
void main() => runApp(const GetStateObjectRoute());
class GetStateObjectRoute extends StatefulWidget {
const GetStateObjectRoute({Key? key}) : super(key: key);
State<GetStateObjectRoute> createState() => _GetStateObjectRouteState();
}
class _GetStateObjectRouteState extends State<GetStateObjectRoute> {
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
appBar: AppBar(
title: const Text("子树中获取State对象"),
),
body: Center(
child: Column(
children: [
Builder(builder: (context) {
return ElevatedButton(
onPressed: () {
// 查找父级最近的Scaffold对应的ScaffoldState对象
ScaffoldState state =
context.findAncestorStateOfType<ScaffoldState>()!;
// 直接通过of静态方法来获取ScaffoldState
// ScaffoldState state=Scaffold.of(context);
// 打开抽屉菜单
state.openDrawer();
},
child: const Text('打开抽屉菜单1'),
);
}),
],
),
),
drawer: const Drawer(),
),
);
}
}
- 一般如果 StatefulWidget 的状态私有(不应该向外部暴露)就不该直接获取其 State 对象;如果StatefulWidget的状态希望暴露(通常还有一些组件的操作方法)则可以去直接获取其State对象。
- 但通过
context.findAncestorStateOfType
获取 StatefulWidget 的状态的方法是通用的,并不能在语法层面指定 StatefulWidget 的状态是否私有, - 所以在 Flutter 开发中便有一个默认约定:
- 如果 StatefulWidget 的状态是希望暴露出的,应当在 StatefulWidget 中提供一个
of
静态方法来获取其 State 对象,开发者便可直接通过该方法来获取; - 如果 State不希望暴露,则不提供
of
方法。这个约定在 Flutter SDK 里随处可见。
- 如果 StatefulWidget 的状态是希望暴露出的,应当在 StatefulWidget 中提供一个
所以上面示例中的Scaffold
也提供了一个of
方法可以直接调用。如显示 snack bar
Builder(builder: (context) {
return ElevatedButton(
onPressed: () {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text("我是SnackBar")),
);
},import 'package:flutter/material.dart';
void main() => runApp(const GetStateObjectRoute());
class GetStateObjectRoute extends StatefulWidget {
const GetStateObjectRoute({Key? key}) : super(key: key);
State<GetStateObjectRoute> createState() => _GetStateObjectRouteState();
}
class _GetStateObjectRouteState extends State<GetStateObjectRoute> {
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
appBar: AppBar(
title: const Text("子树中获取State对象"),
),
body: Center(
child: Column(
children: [
Builder(builder: (context) {
return ElevatedButton(
onPressed: () {
// 查找父级最近的Scaffold对应的ScaffoldState对象
ScaffoldState state =
context.findAncestorStateOfType<ScaffoldState>()!;
// 直接通过of静态方法来获取ScaffoldState
// ScaffoldState state=Scaffold.of(context);
// 打开抽屉菜单
state.openDrawer();
},
child: const Text('打开抽屉菜单1'),
);
}),
Builder(builder: (context) {
return ElevatedButton(
onPressed: () {
// 直接通过of静态方法来获取ScaffoldState
ScaffoldState state0=Scaffold.of(context);
// 打开抽屉菜单
state0.openDrawer();
},
child: const Text('打开抽屉菜单2'),
);
}),
Builder(builder: (context) {
return ElevatedButton(
onPressed: () {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text("我是SnackBar")),
);
},
child: const Text('显示SnackBar'),
);
}),
],
),
),
drawer: const Drawer(),
),
);
}
}
child: Text('显示SnackBar'),
);
}),
GlobalKey获取
步骤分两步:
-
给目标
StatefulWidget
添加GlobalKey
。//定义一个globalKey, 由于GlobalKey要保持全局唯一性,故使用静态变量存储 static GlobalKey<ScaffoldState> _globalKey= GlobalKey(); ... Scaffold( key: _globalKey , //设置key ... )
-
通过
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组件介绍
1.主题
为了在整个应用中使用同一套颜色和字体样式,可以使用“主题”这种方式,定义主题有两种方式:
-
全局主题,由应用程序根 MaterialApp 创建的主题(Theme)
- Flutter 提供了一套丰富的 Material 组件如:Scaffold、AppBar、TextButton等,Material 组件可构建遵循 Material Design 设计规范的应用程序。
- Material 应用程序以 MaterialApp 组件开始, 该组件在应用程序的根部创建了一些必要的组件如 Theme 组件(用于配置应用的主题)。 是否使用MaterialApp 完全可选,但使用它是一个很好的做法。
-
使用 Theme 定义应用程序局部的颜色和字体样式
定义一个主题可在 Widget 使用它, Flutter 提供的 Material Widgets 使用主题为 AppBars Buttons Checkbox 等设置背景颜色和字体样式。
1)创建
创建主题的方法是将 ThemeData 提供给 MaterialApp 构造函数,这样就可以在整个应用程序中共享包含颜色和字体样式的主题。
属性名 | 类型 | ThemeData属性说明 |
---|---|---|
accentColor | Color | 前景色(文本、按钮等) |
accentColorBrightness | Brightness | accentColor的亮度。用于确定放置在突出颜色顶部的文 本和图标的颜色(例如FloatingButton上的图标) |
accentIconTheme | IconThemeData | 与突出颜色对照的图片主题 |
accentTextTheme | TextTheme | 与突出颜色对照的文本主题 |
backgroundColor | Color | 与primaryColor对比的颜色(例如:用作进度条的剩余部分 ) |
bottomAppBarColor | Color | BottomAppBar的默认颜色 |
brightness | Brightness | 应用程序整体主题的亮度。由按钮等Widget使用,以确 定在不使用主色或强调色时要选择的颜色 |
buttonColor | Color | Material中RaisedButtons使用的默认填充色 |
buttonTheme | ButtonThemeData | 定义了按钮等控件的默认配置,如RaisedButton和 FlatButton |
canvasColor | Color | MaterialType.canvas Material的默认颜色 |
cardColor | Color | Material被用作Card时的颜色 |
chipTheme | ChipThemeData | 用于渲染Chip的颜色和样式 |
dialogBackgroundColor | Color | Dialog元素的背景色 |
disabledColor | Color | 用于Widget无效的颜色,包括任何状态。例如禁用复选 框 |
dividerColor | Color | Dividers和PopupMenuDividers的颜色,也用于ListTiles 中间和DataTables的每行中间 |
errorColor | Color | 用于输入验证错误的颜色,例如在TextField中 |
hashCode | int | 对象的哈希值 |
highlightColor | Color | 用于类似墨水喷溅动画或指示菜单被选中的高亮颜色 |
iconTheme | IconThemeData | 与卡片和画布颜色形成对比的图标主题 |
indicatorColor | Color | TabBar中选项选中的指示器颜色 |
inputDecorationTheme | InputDecorationTheme | InputDecorator、TextField和TextFormField的默认Input- Decoration值基于此主题 |
platform | TargetPlatform | Widget需要适配的目标类型 |
primaryColor | Color | App主要部分的背景色(ToolBar、Tabbar等) |
primaryColorBrightness | Brightness | primaryColor的亮度 |
primaryColorDark | Color | primaryColor的较暗版本 |
primaryColorLight | Color | primaryColor的较亮版本 |
primaryIconTheme | IconThemeData | 一个与主色对比的图片主题 |
primaryTextTheme | TextThemeData | 一个与主色对比的文本主题 |
scaffoldBackgroundColor | Color | 作为Scaffold基础的Material默认颜色,典型Material应 用或应用内页面的背景颜色 |
secondaryHeaderColor | Color | 有选定行时PaginatedDataTable标题的颜色 |
selectedRowColor | Color | 选中行时的高亮颜色 |
sliderTheme | SliderThemeData | 用于渲染Slider的颜色和形状 |
splashColor | Color | 墨水喷溅的颜色 |
splashFactory | InteractiveInkFeatureFactory | 定义InkWall和InkResponse生成的墨水喷溅的外观 |
textSelectionColor | Color | 文本字段中选中文本的颜色,例如TextField |
textSelectionHandleColor | Color | 用于调整当前文本的哪个部分的句柄颜色 |
textTheme | TextTheme | 与卡片和画布对比的文本颜色 |
toggleableActiveColor | Color | 用于突出显示切换Widget(如Switch、Radio和Checkbox) 的活动状态的颜色 |
unselectedWidgetColor | Color | 用于Widget处于非活动(但已启用)状态的颜色。例如, 未选中的复选框。通常与accentColor形成对比 |
runtimeType | Type | 表示对象的运行时类型 |
如果没有提供主题将创建一个默认主题
MaterialApp(
title: title,
theme: ThemeData(
brightness: Brightness.dark,
primaryColor: Colors.lightBlue[800],
accentColor: Colors.cyan[600],
),
);
2)局部主题
在应用程序的某一部分使用特殊的颜色,需要覆盖全局的主题。两种方法可以解决这个问题:创建特有的主题数据或扩展父主题。
创建特有的主题数据
实例化一个 ThemeData 井将其传递给 Theme 对象
Theme (
//创建一个特有的主题数据
data: ThemeData(
accentColor: Colors.yellow,
),
child: FloatingActionButton(
onPressed : () {} ,
child: Icon(Icons.add),
),
);
扩展父主题
扩展父主题时无须覆盖所有的主题属性,可通过使用 copyWith 方法实现
Theme(
// 覆盖 accentColor Colors.yellow
data: Theme.of(context).copyWith(accentColor: Colors.yellow),
child: new FloatingActionButton(
onPressed: null,
child: new con (Icons. add),
),
);
3)使用
函数 Theme.of(context)可以通过上下文来获取主题 ,方法是查找最近的 主题,找不到就找整个应用的主题。
import 'package:flutter/material.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
Widget build(BuildContext context) {
const appName = '自定义主题';
return MaterialApp(
title: appName,
theme: ThemeData(
brightness: Brightness.light, //应用程序整体主题的亮度
primaryColor: Colors.lightGreen[600], // App 主要部分的背景色
colorScheme: ColorScheme.fromSwatch()
.copyWith(secondary: Colors.orange[600]), // 前景色(文本、按钮等)
),
home: const MyHomePage(
title: appName,
),
);
}
}
class MyHomePage extends StatelessWidget {
final String title;
const MyHomePage({Key? key, required this.title}) : super(key: key);
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(title),
),
body: Center(
child: Container(
//获取主题的 accentColor
color: Theme.of(context).colorScheme.secondary,
child: const Text(
'带有背景颜色的文本组件',
),
),
),
floatingActionButton: Theme(
//使用 copyWith 的方式获取 accentColor
data: Theme.of(context).copyWith(
colorScheme:
ColorScheme.fromSwatch().copyWith(secondary: Colors.grey)),
child: const FloatingActionButton(
onPressed: null,
child: Icon(Icons.computer),
),
),
);
}
}
2.Flutter 布局
Flutter 中拥有需要将近30种内置的布局Widget
类型 | 作⽤特点 |
---|---|
Container | 只有⼀个⼦Widget。默认充满,包含了padding、margin、color、宽⾼、decoration等配置。 Container 可创建矩形视觉元素,装饰一个 BoxDecoration 如 background、一个边框、或者一个阴影。 Container也可以具有边距(margins)、填充(padding)和应用于其大小的约束(constraints)。另外 Container 可使用矩阵在三维空间中对其进行变换。 |
Padding | 只有⼀个⼦Widget。只⽤于设置Padding,常⽤于嵌套child,给child设置padding。 |
Center | 只有⼀个⼦Widget。只⽤于居中显示,常⽤于嵌套child,给child设置居中。 |
Stack | 可以有多个⼦Widget。⼦Widget堆叠在⼀起。取代线性布局 (和 Android 中的FrameLayout 相似),Stack允许子 widget 堆叠, 可使用 Positioned 来定位他们相对于Stack 的上下左右四条边的位置。Stacks是基于Web开发中的绝对定位(absolute positioning)布局模型设计的。 |
Column | 可以有多个⼦Widget。垂直布局。 |
Row | 可以有多个⼦Widget。⽔平布局。 |
Expanded | 只有⼀个⼦Widget。在Column和Row中充满。 |
ListView | 可以有多个⼦Widget。 |
import 'package:flutter/material.dart';
void main() {
runApp(MaterialApp(
home: Scaffold(
appBar: AppBar(title: const Text("你好Flutter")),
body: const MyApp()),
));
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
Widget build(BuildContext context) {
return 布局样式;
}
}
1)Container
- 最常⽤的默认布局,只包含⼀个
child:
,⽀持配置 padding, margin, color, 宽⾼*, decoration*(⼀般配置边框和阴影)等配置, - Flutter 中不是所有的控件都有 宽⾼、padding、margin、color 等属性,所以才会有 Padding、Center 等 Widget 。
Container(
///四周10⼤⼩的maring
margin: const EdgeInsets.all(10.0),
height: 120.0,
width: 500.0,
///透明白⾊遮罩
decoration: BoxDecoration(
///弧度为4.0
borderRadius: const BorderRadius.all(Radius.circular(4.0)),
///设置了decoration的color,就不能设置Container的color。
color: Colors.black,
///边框
border: Border.all(color: const Color(0xFFB7382D), width: 3)),
child: const Text("666666"));
2)Column、Row
必备横竖布局。常⽤属性配置:
- 主轴⽅向是 start 或 center 等;
- 副轴⽅向⽅向是 start 或 center 等;
- mainAxisSize 是充满最⼤尺⼨,或者只根据⼦ Widget 显示最⼩尺⼨。
//主轴⽅向,Column的竖向、Row我的横向
mainAxisAlignment: MainAxisAlignment.start,
//默认是最⼤充满、还是根据child显示最⼩⼤⼩
mainAxisSize: MainAxisSize.max,
//副轴⽅向,Column的横向、Row我的竖向
crossAxisAlignment :CrossAxisAlignment.center,
3)Expanded
平均充满,当有两个存在的时候默认均分充满。可设置 flex 属性决定⽐例。
⾸先我们创建⼀个私有⽅法 _getBottomItem ,返回⼀个Expanded Widget。布局内主要是现实⼀个居中的Icon图标和⽂本,中间间隔5.0的 padding
///返回⼀个居中带图标和⽂本的Item
_getBottomItem(IconData icon, String text) {
///充满 Row 横向的布局
return Expanded(
flex: 1,
///居中显示
child: Center(
///横向布局
child: Row(
///主轴居中,即是横向居中
mainAxisAlignment: MainAxisAlignment.center,
///⼤⼩按照最⼤充满
mainAxisSize: MainAxisSize.max,
///竖向也居中
crossAxisAlignment: CrossAxisAlignment.center,
children: <Widget>[
///⼀个图标,⼤⼩16.0,灰⾊
Icon(
icon,
size: 16.0,
color: Colors.grey,
),
///间隔
const Padding(padding: EdgeInsets.only(left: 5.0)),
///显示⽂本
Text(
text,
//设置字体样式:颜⾊灰⾊,字体⼤⼩14.0
style: const TextStyle(color: Colors.grey, fontSize: 14.0),
//超过的省略为...显示
overflow: TextOverflow.ellipsis,
//最⻓⼀⾏
maxLines: 1,
),
],
),
),
);
}
接着把⽅法放到新的布局⾥。⾸先 Card 快速简单的实现圆⻆和阴影。然后接下来包含了 MaterialButton 实现了点击,通过Padding实现了边距。接着通过 Column 垂直包含了两个⼦Widget,⼀个是 Container 、⼀个是 Row 。Row 内使⽤的就是 _getBottomItem ⽅法返回的 Widget 。
import 'package:flutter/material.dart';
void main() {
runApp(MaterialApp(
home: Scaffold(
appBar: AppBar(title: const Text("Flutter练习")), body: const MyApp()),
));
}
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
Widget build(BuildContext context) {
return Card(
///增加点击效果
child: MaterialButton(
onPressed: () {
print("点击了哦");
},
child: Padding(
padding: const EdgeInsets.only(
left: 0.0, top: 10.0, right: 10.0, bottom: 10.0),
child: Column(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
///⽂本描述
Container(
margin: const EdgeInsets.only(top: 6.0, bottom: 2.0),
alignment: Alignment.topLeft,
child: const Text(
"这是⼀点描述",
style: TextStyle(
color: Color(0xff4caf50),
fontSize: 14.0,
),
///最⻓三⾏,超过 ... 显示
maxLines: 3,
overflow: TextOverflow.ellipsis,
)),
const Padding(padding: EdgeInsets.all(10.0)),
///三个平均分配的横向图标⽂字
Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
_getBottomItem(Icons.star, "1000"),
_getBottomItem(Icons.link, "1000"),
_getBottomItem(Icons.subject, "1000"),
],
),
],
),
)));
}
}
///返回⼀个居中带图标和⽂本的Item
_getBottomItem(IconData icon, String text) {
///充满 Row 横向的布局
return Expanded(
flex: 1,
///居中显示
child: Center(
///横向布局
child: Row(
///主轴居中,即是横向居中
mainAxisAlignment: MainAxisAlignment.center,
///⼤⼩按照最⼤充满
mainAxisSize: MainAxisSize.max,
///竖向也居中
crossAxisAlignment: CrossAxisAlignment.center,
children: <Widget>[
///⼀个图标,⼤⼩16.0,灰⾊
Icon(
icon,
size: 16.0,
color: Colors.grey,
),
///间隔
const Padding(padding: EdgeInsets.only(left: 5.0)),
///显示⽂本
Text(
text,
//设置字体样式:颜⾊灰⾊,字体⼤⼩14.0
style: const TextStyle(color: Colors.grey, fontSize: 14.0),
//超过的省略为...显示
overflow: TextOverflow.ellipsis,
//最⻓⼀⾏
maxLines: 1,
),
],
),
),
);
}
3.Flutter⻚⾯
Flutter 中除了布局的 Widget,还有交互显示的 Widget 和完整⻚⾯呈现的Widget。其中常⻅的有MaterialApp、Scaffold、Appbar、Text、Image、FlatButton等。
类型 | 作⽤特点 |
---|---|
MaterialApp | ⼀般作为APP顶层的主⻚⼊⼝,可配置主题,多语⾔,路由等 |
Scaffold | ⼀般⽤户⻚⾯的承载Widget,包含appbar、snackbar、drawer等materialdesign的设定。 |
Appbar | ⼀般⽤于Scaffold的appbar,内有标题,⼆级⻚⾯返回按键等,tabbar等也会需要它。 |
Text | 显示⽂本,⼏乎都会⽤到,主要是通过style设置TextStyle来设置字体样式等。 |
RichText | 富⽂本,通过设置TextSpan,可以拼接出富⽂本场景。 |
TextField | ⽂本输⼊框:new TextField(controller://⽂本控制器,obscureText:“hint⽂本”); |
Image | 图⽚加载: new FadeInImage.assetNetwork(placeholder:“预览图”,fit:BoxFit.fitWidth,image:“url”); |
FlatButton | 按键点击: new FlatButton(onPressed:(){},child:newContainer()); |
实现⼀个简单完整的⻚⾯:
- ⾸先创建⼀个StatefulWidget: DemoPage 。
- 然后在 _DemoPageState中,通过 build 创建了⼀个 Scaffold 。
- Scaffold内包含了⼀个 AppBar 和⼀个 ListView 。
- AppBar类似标题了区域,其中设置了 title 为 Text Widget。
- body是 ListView ,返回20个之前创建过的 DemoItem Widget。
class DemoPage extends StatefulWidget {
const DemoPage({Key? key}) : super(key: key);
_DemoPageState createState() => _DemoPageState();
}
class _DemoPageState extends State<DemoPage> {
Widget build(BuildContext context) {
///⼀个⻚⾯的开始
///如果是新⻚⾯,会⾃带返回按键
return Scaffold(
///背景样式
backgroundColor: Colors.blue,
///标题栏,当然不仅仅是标题栏
appBar: AppBar(
///这个title是⼀个Widget
title: const Text("Title"),
),
///正式的⻚⾯开始
///⼀个ListView,20个Item
body: ListView.builder(
itemBuilder: (context, index) {
return Card(
///增加点击效果
child: MaterialButton(
onPressed: () {
print("点击了哦");
},
child: Padding(
padding: const EdgeInsets.only(
left: 0.0, top: 10.0, right: 10.0, bottom: 10.0),
child: Column(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
///⽂本描述
Container(
margin: const EdgeInsets.only(top: 6.0, bottom: 2.0),
alignment: Alignment.topLeft,
child: const Text(
"这是⼀点描述",
style: TextStyle(
color: Color(0xff4caf50),
fontSize: 14.0,
),
///最⻓三⾏,超过 ... 显示
maxLines: 3,
overflow: TextOverflow.ellipsis,
)),
const Padding(padding: EdgeInsets.all(10.0)),
///三个平均分配的横向图标⽂字
Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
_getBottomItem(Icons.star, "1000"),
_getBottomItem(Icons.link, "1000"),
_getBottomItem(Icons.subject, "1000"),
],
),
],
),
)));
},
itemCount: 20,
),
);
}
}
///返回⼀个居中带图标和⽂本的Item
_getBottomItem(IconData icon, String text) {
///充满 Row 横向的布局
return Expanded(
flex: 1,
///居中显示
child: Center(
///横向布局
child: Row(
///主轴居中,即是横向居中
mainAxisAlignment: MainAxisAlignment.center,
///⼤⼩按照最⼤充满
mainAxisSize: MainAxisSize.max,
///竖向也居中
crossAxisAlignment: CrossAxisAlignment.center,
children: <Widget>[
///⼀个图标,⼤⼩16.0,灰⾊
Icon(
icon,
size: 16.0,
color: Colors.grey,
),
///间隔
const Padding(padding: EdgeInsets.only(left: 5.0)),
///显示⽂本
Text(
text,
//设置字体样式:颜⾊灰⾊,字体⼤⼩14.0
style: const TextStyle(color: Colors.grey, fontSize: 14.0),
//超过的省略为...显示
overflow: TextOverflow.ellipsis,
//最⻓⼀⾏
maxLines: 1,
),
],
),
),
);
}
最后我们创建⼀个StatelessWidget作为⼊⼝⽂件,实现⼀个 MaterialApp 将上⽅的 DemoPage 设置为home⻚⾯,通过 main ⼊⼝执⾏⻚⾯。
import 'package:flutter/material.dart';
void main() {
runApp(const DemoApp());
}
class DemoApp extends StatelessWidget {
const DemoApp({Key? key}) : super(key: key);
Widget build(BuildContext context) {
return const MaterialApp(home: DemoPage());
}
}
完整代码如下:
import 'package:flutter/material.dart';
void main() {
runApp(const DemoApp());
}
class DemoApp extends StatelessWidget {
const DemoApp({Key? key}) : super(key: key);
Widget build(BuildContext context) {
return const MaterialApp(home: DemoPage());
}
}
class DemoPage extends StatefulWidget {
const DemoPage({Key? key}) : super(key: key);
_DemoPageState createState() => _DemoPageState();
}
class _DemoPageState extends State<DemoPage> {
Widget build(BuildContext context) {
///⼀个⻚⾯的开始
///如果是新⻚⾯,会⾃带返回按键
return Scaffold(
///背景样式
backgroundColor: Colors.blue,
///标题栏,当然不仅仅是标题栏
appBar: AppBar(
///这个title是⼀个Widget
title: const Text("Title"),
),
///正式的⻚⾯开始
///⼀个ListView,20个Item
body: ListView.builder(
itemBuilder: (context, index) {
return Card(
///增加点击效果
child: MaterialButton(
onPressed: () {
print("点击了哦");
},
child: Padding(
padding: const EdgeInsets.only(
left: 0.0, top: 10.0, right: 10.0, bottom: 10.0),
child: Column(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
///⽂本描述
Container(
margin:
const EdgeInsets.only(top: 6.0, bottom: 2.0),
alignment: Alignment.topLeft,
child: const Text(
"这是⼀点描述",
style: TextStyle(
color: Color(0xff4caf50),
fontSize: 14.0,
),
///最⻓三⾏,超过 ... 显示
maxLines: 3,
overflow: TextOverflow.ellipsis,
)),
const Padding(padding: EdgeInsets.all(10.0)),
///三个平均分配的横向图标⽂字
Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
_getBottomItem(Icons.star, "1000"),
_getBottomItem(Icons.link, "1000"),
_getBottomItem(Icons.subject, "1000"),
],
),
],
),
)));
},
itemCount: 20,
),
);
}
}
///返回⼀个居中带图标和⽂本的Item
_getBottomItem(IconData icon, String text) {
///充满 Row 横向的布局
return Expanded(
flex: 1,
///居中显示
child: Center(
///横向布局
child: Row(
///主轴居中,即是横向居中
mainAxisAlignment: MainAxisAlignment.center,
///⼤⼩按照最⼤充满
mainAxisSize: MainAxisSize.max,
///竖向也居中
crossAxisAlignment: CrossAxisAlignment.center,
children: <Widget>[
///⼀个图标,⼤⼩16.0,灰⾊
Icon(
icon,
size: 16.0,
color: Colors.grey,
),
///间隔
const Padding(padding: EdgeInsets.only(left: 5.0)),
///显示⽂本
Text(
text,
//设置字体样式:颜⾊灰⾊,字体⼤⼩14.0
style: const TextStyle(color: Colors.grey, fontSize: 14.0),
//超过的省略为...显示
overflow: TextOverflow.ellipsis,
//最⻓⼀⾏
maxLines: 1,
),
],
),
),
);
}
四、异常捕获
1.任务队列
Dart 和 JavaScript 都是单线程模型,如果程序发生异常且没有被捕获,程序不会终止。Dart 在单线程中以消息循环机制来运行的,其中包含两个任务队列:
- “微任务队列” microtask queue,
- 在Dart中所有外部事件任务都在事件队列中,如IO、计时器、点击、以及绘制事件等
- “事件队列” event queue。
- 微任务来源于Dart内部,且微任务非常少,因为微任务队列优先级高,微任务太多执行时间总和越久,事件队列任务延迟也越久,对于GUI应用来说最直观的表现就是比较卡,
- 必须保证微任务队列不会太长。可通过
Future.microtask(…)
向微任务队列插入一个任务。
从图中可发现,微任务队列的执行优先级高于事件队列:
如图所示,入口函数 main() 执行完后启动消息循环机制。首先按先进先出逐个执行微任务队列中任务,事件任务执行完毕后程序便会退出,但在事件任务执行过程中也可插入新的微任务和事件任务,此情况下整个线程一直循环不会退出,Flutter主线程的执行过程正是如此,永不终止。
事件循环中某个任务发生异常并没有被捕获时程序并不会退出,结果是当前任务的后续代码不会执行。
2.异常捕获
Dart中可通过try/catch/finally
来捕获代码块异常,下面为Flutter中的异常捕获。
1)Flutter异常
Flutter 框架在很多关键方法进行了异常捕获。如布局发生越界或不合规范时Flutter会自动弹出一个错误界面,这是因为Flutter已经在执行build方法时添加了异常捕获,最终的源码如下:
void performRebuild() {
...
try {
//执行build方法
built = build();
} catch (e, stack) {
// 有异常时则弹出错误提示
built = ErrorWidget.builder(_debugReportException('building $this', e, stack));
}
...
}
可看到在发生异常时,Flutter默认处理方式是弹一个ErrorWidget,要主动捕获异常并上报到报警平台只需要提供一个自定义的错误处理回调
查看_debugReportException()
方法:
FlutterErrorDetails _debugReportException(
String context,
dynamic exception,
StackTrace stack, {
InformationCollector informationCollector
}) {
//构建错误详情对象
final FlutterErrorDetails details = FlutterErrorDetails(
exception: exception,
stack: stack,
library: 'widgets library',
context: context,
informationCollector: informationCollector,
);
//报告错误
FlutterError.reportError(details);
return details;
}
错误通过FlutterError.reportError
方法上报,继续跟踪:
static void reportError(FlutterErrorDetails details) {
...
if (onError != null)
onError(details); //调用了onError回调
}
onError
是FlutterError
的一个静态属性,有一个默认的处理方法 dumpErrorToConsole
,如果想自己上报异常只需要提供一个自定义的错误处理回调即可,如:
void main() {
FlutterError.onError = (FlutterErrorDetails details) {
reportError(details);
};
...
}
void main() {
FlutterError.onError = (FlutterErrorDetails details) {
reportError(details);
};
runApp(MyApp());
}
void reportError(FlutterErrorDetails details) {
// 在这里执行你的异常处理逻辑
print('Caught an error: ${details.exception}');
// 发送错误报告到服务器或其他操作...
}
这样就可处理那些Flutter捕获的异常,接下来我们看看如何捕获其他异常。
2)其他异常
在Flutter中,还有一些Flutter没有为我们捕获的异常,如调用空对象方法异常、Future中的异常。在Dart中,异常分两类:同步异常和异步异常,同步异常可通过try/catch
捕获,而异步异常较麻烦,如下面的代码捕获不了Future
的异常:
try{
Future.delayed(Duration(seconds: 1)).then((e) => Future.error("xxx"));
}catch (e){
print(e)
}
Dart中runZoned(...)
方法可给执行对象指定一个Zone。Zone表示一个代码执行的环境范围,可将Zone类比为一个代码执行沙箱,不同沙箱的之间是隔离的,沙箱可以捕获、拦截或修改一些代码行为,如Zone中可以捕获日志输出、Timer创建、微任务调度的行为,同时Zone也可以捕获所有未处理的异常。下面我们看看runZoned(...)
方法定义:
R runZoned<R>(R body(), {
Map zoneValues,
ZoneSpecification zoneSpecification,
})
-
zoneValues
: Zone 的私有数据,可以通过实例zone[key]
获取,可以理解为每个“沙箱”的私有数据。 -
zoneSpecification
:Zone的一些配置,可以自定义一些代码行为,比如拦截日志输出和错误等,举个例子:runZoned( () => runApp(MyApp()), zoneSpecification: ZoneSpecification( // 拦截print 蜀西湖 print: (Zone self, ZoneDelegate parent, Zone zone, String line) { parent.print(zone, "Interceptor: $line"); }, // 拦截未处理的异步错误 handleUncaughtError: (Zone self, ZoneDelegate parent, Zone zone, Object error, StackTrace stackTrace) { parent.print(zone, '${error.toString()} $stackTrace'); }, ), );
这样一来,我们 APP 中所有调用
print
方法输出日志的行为都会被拦截,通过这种方式,我们也可以在应用中记录日志,等到应用触发未捕获的异常时,将异常信息和日志统一上报。另外我们还拦截了未被捕获的异步错误,这样一来,结合上面的
FlutterError.onError
我们就可以捕获我们Flutter应用错误了并进行上报了!
3. 最终的错误上报代码
我们最终的异常捕获和上报代码大致如下:
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');
},
),
);
}
五、调试Flutter
有各种各样的工具和功能来帮助调试Flutter应用程序。
1.日志与断点
1)debugger()
使用Dart Observatory(或另一个Dart调试器,例如IntelliJ IDE中的调试器)时可使用该debugger()
语句插入编程式断点。须添加import 'dart:developer';
到相关文件顶部。
debugger()
语句采用一个可选when
参数,可指定该参数仅在特定条件为真时中断:
void someFunction(double offset) {
debugger(when: offset > 30.0);
// ...
}
2)print
Dart print()
功能将输出到系统控制台,我们可以使用flutter logs
来查看它(基本上是一个包装adb logcat
)。
3)debugPrint
print()
一次输出太多有时会丢弃一些日志行。可使用Flutter的foundation
库中的debugPrint()
,它封装了 print,将一次输出的内容长度限制在一个级别(内容过多时会分批输出),避免被Android内核丢弃。
4)flutter logs
Flutter框架中的许多类都有toString
实现,输出信息包括对象运行时类型 、类名以及关键字段等信息。
树中的一些类也具有toStringDeep
实现,从该点返回整个子树的多行描述。
一些具有详细信息toString
的类会实现一个toStringShort
,它只返回对象的类型或其他非常简短的(一个或两个单词)描述。
2.调试断言
- 在Flutter应用调试过程中,Dart
assert
语句被启用,并且 Flutter 框架使用它来执行许多运行时检查来验证是否违反一些不可变的规则。 - 当一个某个规则被违反时,就会在控制台打印错误日志,并带上一些上下文信息来帮助追踪问题的根源。
- 要关闭调试模式并使用发布模式使用
flutter run --release
运行应用程序。 这也关闭了Observatory调试器。 - 一个中间模式可关闭除Observatory之外所有调试辅助工具的,称为“profile mode”,用
--profile
替代--release
即可。
3.断点
开发过程中,断点是最实用的调试工具之一。
在 93 行打了一个断点,代码执行到这就会暂停,这时可以看到当前上下文所有变量的值,然后选择一步一步的执行代码。
4.调试应用程序层
Flutter框架的每一层都提供了将其当前状态或事件转储(dump)到控制台(使用debugPrint
)的功能。
1)Widget树
调用debugDumpApp()
转储Widgets树的状态 。 只要应用程序已经构建了至少一次(即在调用build()
之后的任何时间)就可以在应用程序未处于构建阶段(即,不在build()
方法内调用 )的任何时间调用此方法(在调用runApp()
之后)。
如, 这个应用程序:
import 'package:flutter/material.dart';
void main() {
runApp(
MaterialApp(
home: AppHome(),
),
);
}
class AppHome extends StatelessWidget {
Widget build(BuildContext context) {
return Material(
child: Center(
child: TextButton(
onPressed: () {
debugDumpApp();
},
child: Text('Dump App'),
),
),
);
}
}
…会输出这样的内容(精确的细节会根据框架的版本、设备的大小等等而变化):
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(如果你在widget树的根中调用
toStringDeepwidget
,这是你获得的树)。 - 很多在应用源代码中没有出现的widget是被框架中widget的
build()
函数插入的。如InkFeature
是Material widget的一个实现细节 。 - 当按钮从被按下变为被释放时 debugDumpApp() 被调用,TextButton对象同时调用
setState()
,并将自己标记为"dirty"。 - 还可查看已注册了哪些手势监听器; 在这种情况下,一个单一的GestureDetector被列出,并且监听“tap”手势(“tap”是
TapGestureDetector
的toStringShort
函数输出的)。 - 编写自己的widget可通过覆盖
debugFillProperties()
来添加信息。 将DiagnosticsProperty对象作为方法参数,并调用父类方法。 该函数是该toString
方法用来填充小部件描述信息的。
2)渲染树
调试布局问题对于Widget树不够详细。可通过调用debugDumpRenderTree()
转储渲染树。 正如debugDumpApp()
,除布局或绘制阶段外可随时调用此函数。从frame 回调或事件处理器中调用它是最佳解决方案。
要调用debugDumpRenderTree()
,需要添加import'package:flutter/rendering.dart';
到源文件。
上面这个小例子的输出结果如下所示:
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)
... # 省略
这是根RenderObject
对象的toStringDeep
函数的输出。
当调试布局问题时,关键要看的是size
和constraints
字段。约束沿着树向下传递,尺寸向上传递。
如果编写渲染对象可通过覆盖debugFillProperties()
将信息添加到转储。 将DiagnosticsProperty对象作为方法的参数,并调用父类方法。
3)Layer树
渲染树可分层,最终绘制需要将不同的层合成起来,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
这是根Layer
的toStringDeep
输出的。
根部的变换是应用设备像素比的变换; 在这种情况下,每个逻辑像素代表3.5个设备像素。
RepaintBoundary
widget在渲染树的层中创建了一个RenderRepaintBoundary
。这用于减少需要重绘的需求量。
4)语义树
可调用debugDumpSemanticsTree()
获取语义树(呈现给系统可访问性API的树)的转储。 要使用此功能,必须首先启用辅助功能,例如启用系统辅助工具或SemanticsDebugger
。
对于上面的例子,它会输出:
I/flutter : SemanticsNode(0; Rect.fromLTRB(0.0, 0.0, 411.4, 683.4))
I/flutter : ├SemanticsNode(1; Rect.fromLTRB(0.0, 0.0, 411.4, 683.4))
I/flutter : │ └SemanticsNode(2; Rect.fromLTRB(0.0, 0.0, 411.4, 683.4); canBeTapped)
I/flutter : └SemanticsNode(3; Rect.fromLTRB(0.0, 0.0, 411.4, 683.4))
I/flutter : └SemanticsNode(4; Rect.fromLTRB(0.0, 0.0, 82.0, 36.0); canBeTapped; "Dump App")
5.其他调试
1)调度
要找出相对于帧的开始/结束事件发生的位置,可切换debugPrintBeginFrameBanner
和debugPrintEndFrameBanner
布尔值以将帧的开始和结束打印到控制台。
例如:
I/flutter : ▄▄▄▄▄▄▄▄ Frame 12 30s 437.086ms ▄▄▄▄▄▄▄▄
I/flutter : Debug print: Am I performing this work more than once per frame?
I/flutter : Debug print: Am I performing this work more than once per frame?
I/flutter : ▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
debugPrintScheduleFrameStacks
还可以用来打印导致当前帧被调度的调用堆栈。
2)可视化调试
可通过设置debugPaintSizeEnabled
为true
以可视方式调试布局问题。 这是来自rendering
库的布尔值。可在任何时候启用,并在为true时影响绘制。 设置它的最简单方法是在void main()
的顶部设置。
当它被启用时,所有的盒子都会得到一个明亮的深青色边框,padding(来自widget如Padding)显示为浅蓝色,子widget周围有一个深蓝色框, 对齐方式(来自widget如Center和Align)显示为黄色箭头. 空白(如没有任何子节点的Container)以灰色显示。
debugPaintBaselinesEnabled
做了类似的事情,但对于具有基线的对象,文字基线以绿色显示,表意(ideographic)基线以橙色显示。
debugPaintPointersEnabled
标志打开一个特殊模式,任何正在点击的对象都会以深青色突出显示。 这可以帮助我们确定某个对象是否以某种不正确的方式进行hit测试(Flutter检测点击的位置是否有能响应用户操作的widget),例如,如果它实际上超出了其父项的范围,首先不会考虑通过hit测试。
如果尝试调试合成图层,例如以确定是否以及在何处添加RepaintBoundary
widget,则可以使用debugPaintLayerBordersEnabled
标志, 该标志用橙色或轮廓线标出每个层的边界,或者使用debugRepaintRainbowEnabled
标志, 只要他们重绘时,这会使该层被一组旋转色所覆盖。
所有这些标志只能在调试模式下工作。通常,Flutter框架中以“debug...
” 开头的任何内容都只能在调试模式下工作。
3)调试动画
调试动画最简单的方法是减慢它们的速度。为此,请将timeDilation
变量(在scheduler库中)设置为大于1.0的数字,例如50.0。 最好在应用程序启动时只设置一次。如果我们在运行中更改它,尤其是在动画运行时将其值改小,则在观察时可能会出现倒退,这可能会导致断言命中,并且这通常会干扰我们的开发工作。
4)调试性能问题
要了解我们的应用程序导致重新布局或重新绘制的原因,我们可以分别设置debugPrintMarkNeedsLayoutStacks
和 debugPrintMarkNeedsPaintStacks
标志。 每当渲染盒被要求重新布局和重新绘制时,这些都会将堆栈跟踪记录到控制台。如果这种方法对我们有用,我们可以使用services
库中的debugPrintStack()
方法按需打印堆栈痕迹。
5)应用启动时间
要收集有关Flutter应用程序启动所需时间的详细信息,可以在运行flutter run
时使用trace-startup
和profile
选项。
$ flutter run --trace-startup --profile
跟踪输出保存为start_up_info.json
,在Flutter工程目录在build目录下。输出列出了从应用程序启动到这些跟踪事件(以微秒捕获)所用的时间:
- 进入Flutter引擎时.
- 展示应用第一帧时.
- 初始化Flutter框架时.
- 完成Flutter框架初始化时.
如 :
{
"engineEnterTimestampMicros": 96025565262,
"timeToFirstFrameMicros": 2171978,
"timeToFrameworkInitMicros": 514585,
"timeAfterFrameworkInitMicros": 1657393
}
6)跟踪代码性能
要执行自定义性能跟踪和测量Dart任意代码段的wall/CPU时间(类似于在Android上使用 systrace 。 使用dart:developer
的 Timeline 工具来包含你想测试的代码块,例如:
Timeline.startSync('interesting function');
// iWonderHowLongThisTakes();
Timeline.finishSync();
然后打开你应用程序的Observatory timeline页面,在“Recorded Streams”中选择‘Dart’复选框,并执行你想测量的功能。
刷新页面将在Chrome的 跟踪工具 中显示应用按时间顺序排列的timeline记录。
请确保运行flutter run
时带有--profile
标志,以确保运行时性能特征与我们的最终产品差异最小。
6.DevTools
Flutter DevTools 是 Flutter 可视化调试工具。它将各种调试工具和能力集成在一起,并提供可视化调试界面,它的功能很强大,掌握它会对我们开发和优化 Flutter 应用有很大帮助。