Widget简介
Widget概念
我们知道在Flutter中几乎所有的对象都是一个 widget 。与原生开发中“控件”不同的是,Flutter 中的 widget 的概念更广泛,它不仅可以表示UI元素,也可以表示一些功能性的组件如:用于手势检测的 GestureDetector
、用于APP主题数据传递的 Theme
等等,而原生开发中的控件通常只是指UI元素。
Flutter 中是通过 Widget 嵌套 Widget 的方式来构建UI和进行事件处理的,所以记住,Flutter 中万物皆为Widget。
Widget接口
在 Flutter 中, widget 的功能是“描述一个UI元素的配置信息”,它就是说, Widget 其实并不是表示最终绘制在设备屏幕上的显示元素,所谓的配置信息就是 Widget 接收的参数,比如对于 Text 来讲,文本的内容、对齐方式、文本样式都是它的配置信息。下面我们先来看一下 Widget 类的声明:
@immutable // 不可变的
abstract class Widget extends DiagnosticableTree {
const Widget({ this.key });
final Key? key;
@protected
@factory
Element createElement();
@override
String toStringShort() {
final String type = objectRuntimeType(this, 'Widget');
return key == null ? type : '$type-$key';
}
@override
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
super.debugFillProperties(properties);
properties.defaultDiagnosticsTreeStyle = DiagnosticsTreeStyle.dense;
}
@override
@nonVirtual
bool operator ==(Object other) => super == other;
@override
@nonVirtual
int get hashCode => super.hashCode;
static bool canUpdate(Widget oldWidget, Widget newWidget) {
return oldWidget.runtimeType == newWidget.runtimeType
&& oldWidget.key == newWidget.key;
}
...
}
- @immutable: 代表 Widget 是不可变的,这会限制 Widget 中定义的属性(即配置信息)必须是不可变的(final),为什么不允许 Widget 中定义的属性变化呢?这是因为,Flutter 中如果属性发生变化则会重新构建Widget树,即重新创建新的 Widget 实例来替换旧的 Widget 实例,所以允许 Widget 的属性变化是没有意义的,因为一旦 Widget 自己的属性变了自己就会被替换。这也是为什么 Widget 中定义的属性必须是 final 的原因。
- widget:类继承自
DiagnosticableTree
,DiagnosticableTree
即“诊断树”,主要作用是提供调试信息。 - Key:这个
key
属性类似于 React/Vue 中的key
,主要的作用是决定是否在下一次build
时复用旧的 widget ,决定的条件在canUpdate()
方法中。 - createElement():正如前文所述“一个 widget 可以对应多个
Element
”;Flutter 框架在构建UI树时,会先调用此方法生成对应节点的Element
对象。此方法是 Flutter 框架隐式调用的,在我们开发过程中基本不会调用到。 - debugFillProperties(...):复写父类的方法,主要是设置诊断树的一些特性。
- canUpdate(...):是一个静态方法,它主要用于在 widget 树重新
build
时复用旧的 widget ,其实具体来说,应该是:是否用新的 widget 对象去更新旧UI树上所对应的Element
对象的配置;通过其源码我们可以看到,只要newWidget
与oldWidget
的runtimeType
和key
同时相等时就会用new widget
去更新Element
对象的配置,否则就会创建新的Element
。
另外Widget类本身是一个抽象类,其中最核心的就是定义了createElement()接口,在 Flutter 开发中,我们一般都不用直接继承Widget类来实现一个新组件,相反,我们通常会通过继承StatelessWidget或StatefulWidget来间接继承widget类来实现。StatelessWidget和StatefulWidget都是直接继承自Widget类,而这两个类也正是 Flutter 中非常重要的两个抽象类,它们引入了两种 widget 模型。
Flutter中的四棵树
既然 Widget 只是描述一个UI元素的配置信息,那么真正的布局、绘制是由谁来完成的呢?Flutter 框架的处理流程是这样的:
- 根据 Widget 树生成一个 Element 树,Element 树中的节点都继承自
Element
类。 - 根据 Element 树生成 Render 树(渲染树),渲染树中的节点都继承自
RenderObject
类。 - 根据渲染树生成 Layer 树,然后上屏显示,Layer 树中的节点都继承自
Layer
类。
真正的布局和渲染逻辑在 Render 树中,Element 是 Widget 和 RenderObject 的粘合剂,可以理解为一个中间代理。我们通过一个例子来说明,假设有如下 Widget 树:
Container( // 一个容器 widget
color: Colors.blue, // 设置容器背景色
child: Row( // 可以将子widget沿水平方向排列
children: [
Image.network('https://www.example.com/1.png'), // 显示图片的 widget
const Text('A'),
],
),
);
注意,如果 Container 设置了背景色,Container 内部会创建一个新的 ColoredBox 来填充背景,相关逻辑如下:
if (color != null)
current = ColoredBox(color: color!, child: current);
而 Image 内部会通过 RawImage 来渲染图片、Text 内部会通过 RichText 来渲染文本,所以最终的 Widget树、Element 树、渲染树结构如下图:
这里需要注意:
- 三棵树中,Widget 和 Element 是一一对应的,但并不和 RenderObject 一一对应。比如
StatelessWidget
和StatefulWidget
都没有对应的 RenderObject。 - 渲染树在上屏前会生成一棵 Layer 树。
StatelessWidget
StatelessWidget
相对比较简单,它继承自widget
类,重写了createElement()
方法:
@override
StatelessElement createElement() => StatelessElement(this);
StatelessElement
间接继承自Element
类,与StatelessWidget
相对应(作为其配置数据)。StatelessWidget
用于不需要维护状态的场景,它通常在build
方法中通过嵌套其他 widget 来构建UI,在构建过程中会递归的构建其嵌套的 widget 。我们看一个简单的例子:
class Echo extends StatelessWidget{
final String text;
final Color bgColor;
const Echo({super.key, required this.text, this.bgColor = Colors.grey});
@override
Widget build(BuildContext context) {
return
Center(
child: Container(
color: bgColor,
child: Text(text),
),
);
}
}
上面的代码,实现了一个回显字符串的Echo
widget 。 然后我们可以通过如下方式使用它:
Widget build(BuildContext context) {
return Echo(text: "hello world");
}
运行后效果如下所示:
Context
build
方法有一个context
参数,它是BuildContext
类的一个实例,表示当前 widget 在 widget 树中的上下文,每一个 widget 都会对应一个 context 对象(因为每一个 widget 都是 widget 树上的一个节点)。实际上,context
是当前 widget 在 widget 树中执行”相关操作“的一个句柄(handle),比如它提供了从当前 widget 开始向上遍历 widget 树以及按照 widget 类型查找父级 widget 的方法。下面是在子树中获取父级 widget 的一个示例:
import 'package:flutter/material.dart';
class ContextRoute extends StatelessWidget{
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Context测试'),
),
body: Container(
child: Builder(builder: (context) {
Scaffold scaffold = context.findAncestorWidgetOfExactType<Scaffold>()!;
// 直接返回 AppBar的title, 此处实际上是Text("Context测试")
return (scaffold.appBar as AppBar).title!;
})
),
);
}
}
运行后效果如下:
StatefulWidget
和StatelessWidget
一样,StatefulWidget
也是继承自widget
类,并重写了createElement()
方法,不同的是返回的Element
对象并不相同;另外StatefulWidget
类中添加了一个新的接口createState()
。
下面我们看看StatefulWidget
的类定义:
abstract class StatefulWidget extends Widget {
const StatefulWidget({ Key key }) : super(key: key);
@override
StatefulElement createElement() => StatefulElement(this);
@protected
State createState();
}
- StatefulElement: 间接继承自
Element
类,与StatefulWidget
相对应(作为其配置数据)。StatefulElement
中可能会多次调用createState()
来创建状态(State)对象。 - createState():用于创建和 StatefulWidget 相关的状态,它在StatefulWidget 的生命周期中可能会被多次调用。例如,当一个 StatefulWidget 同时插入到 widget 树的多个位置时,Flutter 框架就会调用该方法为每一个位置生成一个独立的State实例,其实,本质上就是一个
StatefulElement
对应一个State实例。
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 实例。
- context:StatefulWidget对应的 BuildContext,作用同StatelessWidget 的BuildContext。
State生命周期
理解State的生命周期对Flutter开发非常重要,为了加深印象,我们通过一个实例来演示一个State的生命周期。我们以计数器功能为例,实现一个计数器CounterWidget组件,点击它可以使计数器加1,由于要保存计数器的数值状态,所以我们应继承StatefulWidget,代码如下
class CounterWidget extends StatefulWidget{
final int initValue;
const CounterWidget({super.key, this.initValue = 0});
@override
State<StatefulWidget> createState() {
return _CounterwidgetState();
}
}
CounterWidget
接收一个initValue
整型参数,它表示计数器的初始值。下面我们看一下State的代码:
class _CounterwidgetState extends State<CounterWidget>{
int _counter = 0;
@override
Widget build(BuildContext context) {
print("build");
return Scaffold(
body: Center(
child: TextButton(onPressed: () =>{
setState(() {
++_counter;
})
}, child: Text('$_counter')),
),
);
}
@override
void initState() {
super.initState();
_counter = widget.initValue;
print("initState");
}
@override
void didUpdateWidget(covariant CounterWidget oldWidget) {
super.didUpdateWidget(oldWidget);
print("didUpdateWidget");
}
@override
void deactivate() {
super.deactivate();
print("deactivate");
}
@override
void dispose() {
super.dispose();
print("dispose");
}
@override
void reassemble() {
super.reassemble();
print("reassemble");
}
@override
void didChangeDependencies() {
super.didChangeDependencies();
print("didChangeDependencies");
}
}
接下来,我们创建一个新路由,在新路由中,我们只显示一个CounterWidget
:
class StateLifecycleTest extends StatelessWidget {
const StateLifecycleTest({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return CounterWidget();
}
}
我们运行应用并打开该路由页面,在新路由页打开后,屏幕中央就会出现一个数字0,然后控制台日志输出:
I/flutter ( 7865): initState
I/flutter ( 7865): didChangeDependencies
I/flutter ( 7865): build
可以看到,在 StatefulWidget插入到widget树时首先initState方法会被调用。
然后我们点击⚡️按钮热重载,控制台输出日志如下:
I/flutter ( 7865): reassemble
I/flutter ( 7865): didUpdateWidget
I/flutter ( 7865): build
可以看到此时initState
和didChangeDependencies
都没有被调用,而此时didUpdateWidget
被调用。 接下来,我们在 widget 树中移除CounterWidget
,将 StateLifecycleTest 的 build
方法改为:
@override
Widget build(BuildContext context) {
// return CounterWidget();
return Text("hello world");
}
然后热重载,日志如下:
I/flutter ( 7865): reassemble
I/flutter ( 7865): deactive
I/flutter ( 7865): dispose
我们可以看到,在CounterWidget
从 widget 树中移除时,deactive
和dispose
会依次被调用。
下面我们来看看各个回调函数:
- 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
也会被调用。 - build():它主要是用于构建 widget 子树的,会在如下场景被调用:在调用
initState()
之后。
- 在调用
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生命周期如下图所示:
在widget树中获取State对象
由于 StatefulWidget 的具体逻辑都在其 State 中,所以很多时候,我们需要获取 StatefulWidget 对应的State 对象来调用一些方法,比如Scaffold
组件对应的状态类ScaffoldState
中就定义了打开 SnackBar(路由页底部提示条)的方法。我们有两种方法在子 widget 树中获取父级 StatefulWidget 的State 对象。
通过Context获取
context
对象有一个findAncestorStateOfType()
方法,该方法可以从当前节点沿着 widget 树向上查找指定类型的 StatefulWidget 对应的 State 对象。下面是实现打开 SnackBar 的示例:
class GetStateObjectRoute extends StatefulWidget{
@override
State<StatefulWidget> createState() {
return _GetStateObjectRouteState();
}
}
class _GetStateObjectRouteState extends State<GetStateObjectRoute>{
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('子树中获取State对象'),
),
body: Center(
child: Column(
children: [
Builder(builder: (context){
return ElevatedButton(onPressed: (){
ScaffoldState _state = context.findAncestorStateOfType<ScaffoldState>()!;
_state.openDrawer();
}, child: Text('打开抽屉菜单1'));
})
],
),
),
drawer: Drawer(),
);
}
}
一般来说,如果 StatefulWidget 的状态是私有的(不应该向外部暴露),那么我们代码中就不应该去直接获取其 State 对象;如果StatefulWidget的状态是希望暴露出的(通常还有一些组件的操作方法),我们则可以去直接获取其State对象。但是通过 context.findAncestorStateOfType
获取 StatefulWidget 的状态的方法是通用的,我们并不能在语法层面指定 StatefulWidget 的状态是否私有,所以在 Flutter 开发中便有了一个默认的约定:如果 StatefulWidget 的状态是希望暴露出的,应当在 StatefulWidget 中提供一个of
静态方法来获取其 State 对象,开发者便可直接通过该方法来获取;如果 State不希望暴露,则不提供of
方法。这个约定在 Flutter SDK 里随处可见。所以,上面示例中的Scaffold
也提供了一个of
方法,我们其实是可以直接调用它的:
Builder(builder: (context){
return ElevatedButton(onPressed: (){
ScaffoldState _state = Scaffold.of(context);
_state.openDrawer();
}, child: Text('打开抽屉菜单2'));
}),
又比如我们想显示 snack bar 的话可以通过下面代码调用:
Builder(builder: (context){
return ElevatedButton(onPressed: (){
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('我是SnackBar'))
);
}, child: Text('显示SnackBar'));
}),
上面示例运行后,点击”显示SnackBar“,效果如下图所示:
通过GlobalKey
Flutter还有一种通用的获取State
对象的方法——通过GlobalKey来获取! 步骤分两步:
- 给目标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 树中必须是唯一的,不能重复。
通过RenderObject自定义Widget
StatelessWidget
和 StatefulWidget
都是用于组合其他组件的,它们本身没有对应的 RenderObject。Flutter 组件库中的很多基础组件都不是通过StatelessWidget
和 StatefulWidget
来实现的,比如 Text 、Column、Align等,就好比搭积木,StatelessWidget
和 StatefulWidget
可以将积木搭成不同的样子,但前提是得有积木,而这些积木都是通过自定义 RenderObject 来实现的。实际上Flutter 最原始的定义组件的方式就是通过定义RenderObject 来实现,而StatelessWidget
和 StatefulWidget
只是提供的两个帮助类。下面我们简单演示一下通过RenderObject定义组件的方式:
class CustomWidget extends LeafRenderObjectWidget{
@override
RenderObject createRenderObject(BuildContext context) {
// TODO: implement createRenderObject
return RenderCustomObject();
}
@override
void updateRenderObject(BuildContext context, covariant RenderObject renderObject) {
// TODO: implement updateRenderObject
super.updateRenderObject(context, renderObject);
}
}
class RenderCustomObject extends RenderBox{
@override
void performLayout() {
// 实现布局逻辑
}
@override
void paint(PaintingContext context, Offset offset) {
// 实现绘制
}
}
如果组件不会包含子组件,则我们可以直接继承自 LeafRenderObjectWidget ,它是 RenderObjectWidget 的子类,而 RenderObjectWidget 继承自 Widget ,我们可以看一下它的实现:
abstract class LeafRenderObjectWidget extends RenderObjectWidget {
/// Abstract const constructor. This constructor enables subclasses to provide
/// const constructors so that they can be used in const expressions.
const LeafRenderObjectWidget({super.key});
@override
LeafRenderObjectElement createElement() => LeafRenderObjectElement(this);
}
很简单,就是帮 widget 实现了createElement 方法,它会为组件创建一个 类型为 LeafRenderObjectElement 的 Element对象。如果自定义的 widget 可以包含子组件,则可以根据子组件的数量来选择继承SingleChildRenderObjectWidget 或 MultiChildRenderObjectWidget,它们也实现了createElement() 方法,返回不同类型的 Element 对象。
然后我们重写了 createRenderObject 方法,它是 RenderObjectWidget 中定义方法,该方法被组件对应的 Element 调用(构建渲染树时)用于生成渲染对象。我们的主要任务就是来实现 createRenderObject 返回的渲染对象类,本例中是 RenderCustomObject 。updateRenderObject 方法是用于在组件树状态发生变化但不需要重新创建 RenderObject 时用于更新组件渲染对象的回调。
RenderCustomObject 类是继承自 RenderBox,而 RenderBox 继承自 RenderObject,我们需要在 RenderCustomObject 中实现布局、绘制、事件响应等逻辑。
Flutter SDK内置组件库介绍
Flutter 提供了一套丰富、强大的基础组件,在基础组件库之上 Flutter 又提供了一套 Material 风格( Android 默认的视觉风格)和一套 Cupertino 风格(iOS视觉风格)的组件库。要使用基础组件库,需要先导入:
import 'package:flutter/widgets.dart';
基础组件
- Text:该组件可让您创建一个带格式的文本。
- Row、Column:这些具有弹性空间的布局类 widget 可让您在水平(Row)和垂直(Column)方向上创建灵活的布局。其设计是基于 Web 开发中的 Flexbox 布局模型。
- Stack:取代线性布局 (译者语:和 Android 中的
FrameLayout
相似),[Stack
](https://docs.flutter.dev/flutter/ widgets/Stack-class.html)允许子 widget 堆叠, 你可以使用 Positioned (opens new window)来定位他们相对于Stack
的上下左右四条边的位置。Stacks是基于Web开发中的绝对定位(absolute positioning )布局模型设计的。 - Container:可让您创建矩形视觉元素。Container 可以装饰一个BoxDecoration (opens new window), 如 background、一个边框、或者一个阴影。 Container (opens new window)也可以具有边距(margins)、填充(padding)和应用于其大小的约束(constraints)。另外, Container (opens new window)可以使用矩阵在三维空间中对其进行变换。
Material组件
Flutter 提供了一套丰富 的Material 组件,它可以帮助我们构建遵循 Material Design 设计规范的应用程序。Material 应用程序以MaterialApp (opens new window) 组件开始, 该组件在应用程序的根部创建了一些必要的组件,比如Theme
组件,它用于配置应用的主题。 是否使用MaterialApp (opens new window)完全是可选的,但是使用它是一个很好的做法。在之前的示例中,我们已经使用过多个 Material 组件了,如:Scaffold
、AppBar
、TextButton
等。要使用 Material 组件,需要先引入它:
import 'package:flutter/material.dart';
Cupertino组件
Flutter 也提供了一套丰富的 Cupertino 风格的组件,尽管目前还没有 Material 组件那么丰富,但是它仍在不断的完善中。值得一提的是在 Material 组件库中有一些组件可以根据实际运行平台来切换表现风格,比如MaterialPageRoute
,在路由切换时,如果是 Android 系统,它将会使用 Android 系统默认的页面切换动画(从底向上);如果是 iOS 系统,它会使用 iOS 系统默认的页面切换动画(从右向左)。由于在前面的示例中还没有Cupertino组件的示例,下面我们实现一个简单的 Cupertino 组件风格的页面:
import 'package:flutter/cupertino.dart';
class CupertinoTestRoute extends StatelessWidget {
@override
Widget build(BuildContext context) {
// TODO: implement build
return CupertinoPageScaffold(
navigationBar: const CupertinoNavigationBar(
middle: Text("Cupertino Demo"),
),
child: Center(
child: CupertinoButton(
color: CupertinoColors.activeBlue,
child: const Text("Press"),
onPressed: () {}
),
),
);
}
}
状态管理
响应式的编程框架中都会有一个永恒的主题——“状态(State)管理”,无论是在 React/Vue(两者都是支持响应式编程的 Web 开发框架)还是 Flutter 中,他们讨论的问题和解决的思想都是一致的。StatefulWidget
的状态应该被谁管理?Widget本身?父 Widget ?都会?还是另一个对象?答案是取决于实际情况!以下是管理状态的最常见的方法:
- Widget 管理自己的状态。
- Widget 管理子 Widget 状态。
- 混合管理(父 Widget 和子 Widget 都管理状态)。
如何决定使用哪种管理方法?下面是官方给出的一些原则可以帮助你做决定:
- 如果状态是用户数据,如复选框的选中状态、滑块的位置,则该状态最好由父 Widget 管理。
- 如果状态是有关界面外观效果的,例如颜色、动画,那么状态最好由 Widget 本身来管理。
- 如果某一个状态是不同 Widget 共享的则最好由它们共同的父 Widget 管理。
在 Widget 内部管理状态封装性会好一些,而在父 Widget 中管理会比较灵活。有些时候,如果不确定到底该怎么管理状态,那么推荐的首选是在父 Widget 中管理(灵活会显得更重要一些)。
接下来,我们将通过创建三个简单示例TapboxA、TapboxB和TapboxC来说明管理状态的不同方式。 这些例子功能是相似的 ——创建一个盒子,当点击它时,盒子背景会在绿色与灰色之间切换。状态 _active
确定颜色:绿色为true
,灰色为false
,
Widget管理自身状态
我们实现一个TapboxA,在它对应的状态管理类_TapboxAState,管理TapboxA的状态,定义一个_active属性,确定盒子的当前颜色状态,定义_handleTap()
函数,该函数在点击该盒子时更新_active
,并调用setState()
更新UI。具体实现代码如下:
class TapBoxA extends StatefulWidget{
const TapBoxA({super.key});
@override
State<StatefulWidget> createState() {
return _TapBoxAState();
}
}
class _TapBoxAState extends State<TapBoxA>{
bool _active = false;
void _handleTap(){
setState(() {
_active = !_active;
});
}
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: _handleTap,
child: Center(
child: Container(
child: Center(
child: Text(
_active?'Active':'Inactive',
style: TextStyle(fontSize: 32.0,color: Colors.white),
),
),
width: 200.0,
height: 200.0,
decoration: BoxDecoration(
color: _active?Colors.lightGreen[700]:Colors.grey[600]
),
),
),
);
}
}
运行效果如下,可以看出通过_active控制盒子的背景状态变化。
父Widget管理子Widget的状态
对于父Widget来说,管理状态并告诉其子Widget何时更新通常是比较好的方式。在以下示例中,TapboxB通过回调将其状态导出到其父组件,状态由父组件管理,因此它的父组件为StatefulWidget
。但是由于TapboxB不管理任何状态,所以TapboxB
为StatelessWidget
。
ParentWidgetState
类:
- 为TapboxB 管理
_active
状态。 - 实现
_handleTapboxChanged()
,当盒子被点击时调用的方法。 - 当状态改变时,调用
setState()
更新UI。
class ParentWidget extends StatefulWidget{
@override
State<StatefulWidget> createState() {
return _ParentWidgetState();
}
}
class _ParentWidgetState extends State<ParentWidget>{
bool _active = false;
_handleTapboxChanged(bool newValue){
setState(() {
_active = newValue;
});
}
@override
Widget build(BuildContext context) {
return Column(
children: [
SizedBox(
height:50,
),
TapBoxB(
active: _active,
onChanged: _handleTapboxChanged),
SizedBox(
height: 10,
),
TapBoxB(
active: _active,
onChanged: _handleTapboxChanged)
],
)
;
}
}
TapboxB 类:
- 继承
StatelessWidget
类,因为所有状态都由其父组件处理。 - 当检测到点击时,它会通知父组件。
class TapBoxB extends StatelessWidget{
final bool active;
final ValueChanged<bool> onChanged;
const TapBoxB({super.key, this.active=false,required this.onChanged});
void _handleTap(){
onChanged(!active);
}
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: _handleTap,
child: Center(
child: Container(
child: Text(
active?'Active':'Inactive',
style: TextStyle(fontSize: 32,color: Colors.white),
),
width:200.0,
height: 200.0,
decoration: BoxDecoration(
color: active?Colors.lightGreen[700] : Colors.grey[600]
),
),
),
);
}
}
运行效果如下,单点击其中任意一个盒子,两个盒子的背景色跟着改变。
混合状态管理
对于一些组件来说,混合管理的方式会非常有用。在这种情况下,组件自身管理一些内部状态,而父组件管理一些其他外部状态。
下面 TapboxC 示例中,手指按下时,盒子的周围会出现一个深绿色的边框,抬起时,边框消失。点击完成后,盒子的颜色改变。 TapboxC 将其_active
状态导出到其父组件中,但在内部管理其_highlight
状态。这个例子有两个状态对象_ParentWidgetState
和_TapboxCState
。
_ParentWidgetStateC
类:
- 管理
_active
状态。 - 实现
_handleTapboxChanged()
,当盒子被点击时调用。 - 当点击盒子并且
_active
状态改变时调用setState()
更新UI。
class ParentWidgetC extends StatefulWidget{
@override
State<StatefulWidget> createState() {
return _ParentWidgetCState();
}
}
class _ParentWidgetCState extends State<ParentWidgetC>{
bool _active = false;
void _handleTapboxChanged(bool newValue){
setState(() {
_active = newValue;
});
}
@override
Widget build(BuildContext context) {
return TapBoxC(
active: _active,
onChanged: _handleTapboxChanged,
);
}
}
_TapboxCState
对象:
- 管理
_highlight
状态。 GestureDetector
监听所有tap事件。当用户点下时,它添加高亮(深绿色边框);当用户释放时,会移除高亮。- 当按下、抬起、或者取消点击时更新
_highlight
状态,调用setState()
更新UI。 - 当点击时,将状态的改变传递给父组件。
class TapBoxC extends StatefulWidget{
final bool active;
final ValueChanged<bool> onChanged;
const TapBoxC({super.key,this.active = false,required this.onChanged});
@override
State<StatefulWidget> createState() {
return _TapBoxCState();
}
}
class _TapBoxCState extends State<TapBoxC>{
bool _higlight = false;
void _handleTapDown(TapDownDetails details){
setState(() {
_higlight = true;
});
}
void _handleTapUp(TapUpDetails details){
setState(() {
_higlight = false;
});
}
void _handleTap(){
widget.onChanged(!widget.active);
}
void _handleTapCancel(){
setState(() {
_higlight = false;
});
}
@override
Widget build(BuildContext context) {
return GestureDetector(
onTapDown: _handleTapDown,
onTapUp: _handleTapUp,
onTapCancel: _handleTapCancel,
onTap: _handleTap,
child: Center(
child: Container(
child: Text(
widget.active ? 'Active':'Inactive',
style: TextStyle(fontSize: 32,color: Colors.white),
),
width: 200,
height: 200,
decoration: BoxDecoration(
color: widget.active?Colors.lightGreen[700] : Colors.grey[600],
border: _higlight?Border.all(
color: Colors.teal[700]!,
width: 10
):null
),
),
),
);
}
}
运行效果如下所示:
全局状态管理
当应用中需要一些跨组件(包括跨路由)的状态需要同步时,上面介绍的方法便很难胜任了。比如,我们有一个设置页,里面可以设置应用的语言,我们为了让设置实时生效,我们期望在语言状态发生改变时,App中依赖应用语言的组件能够重新 build 一下,但这些依赖应用语言的组件和设置页并不在一起,所以这种情况用上面的方法很难管理。这时,正确的做法是通过一个全局状态管理器来处理这种相距较远的组件之间的通信。目前主要有两种办法:
- 实现一个全局的事件总线,将语言状态改变对应为一个事件,然后在APP中依赖应用语言的组件的
initState
方法中订阅语言改变的事件。当用户在设置页切换语言后,我们发布语言改变事件,而订阅了此事件的组件就会收到通知,收到通知后调用setState(...)
方法重新build
一下自身即可。 - 使用一些专门用于状态管理的包,如 Provider、Redux。
路由管理
路由(Route)在移动开发中通常指页面(Page),这跟 Web 开发中单页应用的 Route 概念意义是相同的,Route 在 Android中 通常指一个 Activity,在 iOS 中指一个 ViewController。所谓路由管理,就是管理页面之间如何跳转,通常也可被称为导航管理。Flutter 中的路由管理和原生开发类似,无论是 Android 还是 iOS,导航管理都会维护一个路由栈,路由入栈(push)操作对应打开一个新页面,路由出栈(pop)操作对应页面关闭操作,而路由管理主要是指如何来管理路由栈。
我们创建一个页面,命名为"Page1",继承自StatelessWidget
,界面很简单,在页面添加一个Text和TextButton,具体代码实现如下:
class Page1 extends StatelessWidget{
@override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: Column(
children: [
SizedBox(
height: 100,
),
Text('Page 1',style: TextStyle(fontSize: 32),),
TextButton(
onPressed: (){
Navigator.push(
context,
MaterialPageRoute(builder:(context){
return Page2();
})
);
},
child: Text('Next'))
],
),
)
);
}
}
再新建一个页面,命名为"Page2", 也继承自StatelessWidget,
在页面中间显示一句"Page 2",具体代码如下:
class Page2 extends StatelessWidget{
@override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: Container(
child: Text('Page 2',
style: TextStyle(fontSize: 32),),
)
),
)
;
}
}
运行之后的效果如下:
MaterialPageRoute
Navigator
路由传值
命名路由
路由生成钩子
总结
包管理
资源管理
调试Flutter应用
Flutter异常捕获
引用声明:简介 | 《Flutter实战·第二版》