学习 https://flutter.cn/docs/development/ui/interactive
有状态和无状态的 widgets
有些 widgets 是有状态的, 有些是无状态的。如果用户与 widget 交互,widget 会发生变化,那么它就是_有状态的_。
Stateless widget 不会发生变化。Icon、IconButton 和 Text 都是无状态 widget,它们都是 StatelessWidget 的子类。
而 stateful widget 是动态的。例如,可以通过与用户的交互或是随着数据的改变而导致外观形态的变化。 Checkbox、Radio、Slider、InkWell、Form 和 TextField 都是有状态 widget,它们都是 StatefulWidget 的子类。
一个 widget 的状态保存在一个 State 对象中, 它和 widget 的显示分离。Widget 的状态是一些可以更改的值, 如一个滑动条的当前值或一个复选框是否被选中。当 widget 状态改变时, State 对象调用 setState()
, 告诉框架去重绘 widget。
创建一个有状态的 widget
重点是什么?
-
实现一个有状态 widget 需要创建两个类:一个
StatefulWidget
的子类和一个State
的子类。 -
State 类包含该 widget 的可变状态并定义该 widget 的
build()
方法. -
当 widget 状态改变时, State 对象调用
setState()
, 告诉框架去重绘 widget。
在本节中,您将创建一个自定义有状态的 widget。您将使用一个自定义有状态 widget 来替换两个无状态 widget —红色实心星形图标和其旁边的数字计数—该 widget 用两个子 widget 管理一行 IconButton
和 Text
。
实现一个自定义的有状态 widget 需要创建两个类:
-
一个 StatefulWidget 的子类,用来定义一个 widget 类。
-
一个
State
的子类,包含该widget状态并定义该 widget 的build()
方法.
这一节展示如何为 Lakes 应用程序构建一个名为 FavoriteWidget
的 StatefulWidget。第一步是选择如何管理 FavoriteWidget
的状态。
步骤 0: 开始
如果你已经在 Layout tutorial (step 6) 中成功创建了应用程序,你可以跳过下面的部分。
-
确保你已经 设置 好了你的环境.
-
用 GitHub 上的 main.dart 替换
lib/main.dart
文件。 -
用 GitHub 上的 pubspec.yaml 替换
pubspec.yaml
文件。 -
在你的工程中创建一个
images
文件夹, 并添加 lake.jpg。
如果你有一个连接并可用的设备,或者你已经启动了 iOS simulator(Flutter 安装部分介绍过),你就可以开始了
这些都是前置环境设置和代码编写,之前一节已经学习了,可以直接跳过
Step 1: 决定哪个对象管理 widget 的状态
一个 widget 的状态可以通过多种方式进行管理,但在我们的示例中,widget 本身,FavoriteWidget
,将管理自己的状态。在这个例子中,切换星形图标是一个独立的操作,不会影响父窗口 widget 或其他用户界面,因此该 widget 可以在内部处理它自己的状态。
你可以在 状态管理 中了解更多关于 widget 和状态的分离以及如何管理状态的信息。
Step 2: 创建 StatefulWidget 的子类
FavoriteWidget
类管理自己的状态,因此它通过重写 createState()
来创建状态对象。框架会在构建 widget 时调用 createState()
。在这个例子中,createState()
创建 _FavoriteWidgetState
的实例,您将在下一步中实现该实例。
lib/main.dart (FavoriteWidget)
class FavoriteWidget extends StatefulWidget { @override _FavoriteWidgetState createState() => _FavoriteWidgetState(); }
备忘 Members or classes that start with an underscore (_
) are private. For more information, see Libraries and visibility, a section in the Dart language tour.
以下划线(_
)开头的成员或类是私有的。有关更多信息,请参阅 Dart language tour 中的 Libraries and visibility 部分。
Step 3: 创建 State 的子类
_FavoriteWidgetState
类存储可变信息;可以在 widget 的生命周期内改变逻辑和内部状态。当应用第一次启动时,用户界面显示一个红色实心的星星形图标,表明该湖已经被收藏,并有 41 个“喜欢”。状态对象存储这些信息在 _isFavorited
和 _favoriteCount
变量中。
lib/main.dart (_FavoriteWidgetState fields)
class _FavoriteWidgetState extends State<FavoriteWidget> { bool _isFavorited = true; int _favoriteCount = 41; // ··· }
状态对象也定义了 build()
方法。这个 build()
方法创建一个包含红色 IconButton
和 Text
的行。该 widget 使用 IconButton(而不是 Icon
),因为它具有一个 onPressed
属性,该属性定义了处理点击的回调方法(_toggleFavorite
)。你将会在接下来的步骤中尝试定义它。
lib/main.dart (_FavoriteWidgetState build)
class _FavoriteWidgetState extends State<FavoriteWidget> { // ··· @override Widget build(BuildContext context) { return Row( mainAxisSize: MainAxisSize.min, children: [ Container( padding: EdgeInsets.all(0), child: IconButton( icon: (_isFavorited ? Icon(Icons.star) : Icon(Icons.star_border)), color: Colors.red[500], onPressed: _toggleFavorite, ), ), SizedBox( width: 18, child: Container( child: Text('$_favoriteCount'), ), ), ], ); } }
小提示 Placing the Text
in a SizedBox and setting its width prevents a discernible “jump” when the text changes between the values of 40 and 41 — a jump would otherwise occur because those values have different widths.
当 Text
在 40 和 41 之间变化时,将文本放在 SizedBox 中并设置其宽度可防止出现明显的“跳跃”,因为这些值具有不同的宽度。
按下 IconButton
时会调用 _toggleFavorite()
方法,然后它会调用 setState()
。调用 setState()
是至关重要的,因为这告诉框架,widget 的状态已经改变,应该重绘。 setState()
在如下两种状态中切换 UI:
-
实心的星形图标和数字 ‘41’
-
轮廓线的星形图标和数字 ‘40’ 之间切换 UI
void _toggleFavorite() { setState(() { if (_isFavorited) { _favoriteCount -= 1; _isFavorited = false; } else { _favoriteCount += 1; _isFavorited = true; } }); }
Step 4: 将有 stateful widget 插入 widget 树中
将您自定义 stateful widget 在 build()
方法中添加到 widget 树中。首先,找到创建图标
和文本
的代码,并删除它,在相同的位置创建 stateful widget:
layout/lakes/{step6 → interactive}/lib/main.dart
@@ -10,2 +5,2 @@ | |
10 5 | class MyApp extends StatelessWidget { |
11 6 | @override |
@@ -38,11 +33,7 @@ | |
38 33 | ], |
39 34 | ), |
40 35 | ), |
41 | - Icon( |
36 | + FavoriteWidget(), |
42 | - Icons.star, |
43 | - color: Colors.red[500], |
44 | - ), |
45 | - Text('41'), |
46 37 | ], |
47 38 | ), |
48 39 | ); |
@@ -114,3 +105,5 @@ | |
114 105 | ), |
115 106 | ), |
116 107 | ], |
108 | + ); |
109 | + } |
就是这样!当您热重载应用后,星形图标就会响应点击了.
效果
状态管理
重点是什么?
-
有多种方法可以管理状态。
-
您作为 widget 的设计者,需要选择使用何种管理方法。
-
如果不是很清楚时, 就在父 widget 中管理状态。
谁管理着 stateful widget 的状态?widget 本身?父 widget?双方?另一个对象?答案是……这取决于实际情况。有几种有效的方法可以给你的 widget 添加互动。作为 widget 设计师,你可以基于你所期待的表现 widget 的方式来做决定。以下是一些管理状态的最常见的方法:
如何决定使用哪种管理方法?以下原则可以帮助您决定:
-
如果状态是用户数据,如复选框的选中状态、滑块的位置,则该状态最好由父 widget 管理。
-
如果所讨论的状态是有关界面外观效果的,例如动画,那么状态最好由 widget 本身来管理。
如果有疑问,首选是在父 widget 中管理状态。
我们将通过创建三个简单示例来举例说明管理状态的不同方式:TapboxA、TapboxB 和 TapboxC。这些例子功能是相似的 - 每创建一个容器,当点击时,在绿色或灰色框之间切换。 _active
确定颜色:绿色为 true,灰色为 false。
这些示例使用 GestureDetector 捕获 Container 上的用户动作。
widget 管理自己的状态
有时,widget 在内部管理其状态是最好的。例如,当 ListView 的内容超过渲染框时, ListView 自动滚动。大多数使用 ListView 的开发人员不想管理 ListView 的滚动行为,因此 ListView 本身管理其滚动偏移量。
_TapboxAState
类:
-
管理
TapboxA
的状态. -
定义布尔值
_active
确定盒子的当前颜色. -
定义
_handleTap()
函数,该函数在点击该盒子时更新_active
,并调用setState()
更新 UI。 -
实现 widget 的所有交互式行为.
// TapboxA manages its own state. // TapboxA 管理自身状态. //------------------------- TapboxA ---------------------------------- class TapboxA extends StatefulWidget { TapboxA({Key key}) : super(key: key); @override _TapboxAState createState() => _TapboxAState(); } class _TapboxAState extends State<TapboxA> { bool _active = false; void _handleTap() { setState(() { _active = !_active; }); } Widget build(BuildContext context) { return GestureDetector( onTap: _handleTap, 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], ), ), ); } } //------------------------- MyApp ---------------------------------- class MyApp extends StatelessWidget { @override Widget build(BuildContext context) { return MaterialApp( title: 'Flutter Demo', home: Scaffold( appBar: AppBar( title: Text('Flutter Demo'), ), body: Center( child: TapboxA(), ), ), ); } }
父 widget 管理 widget 的 state
一般来说父 widget 管理状态并告诉其子 widget 何时更新通常是最合适的。例如,IconButton 允许您将图标视为可点按的按钮。 IconButton 是一个无状态的小部件,因为我们认为父 widget 需要知道该按钮是否被点击来采取相应的处理。
在以下示例中,TapboxB 通过回调将其状态到其父类。由于 TapboxB 不管理任何状态,因此它继承自 StatelessWidget。
ParentWidgetState 类:
-
为 TapboxB 管理
_active
状态. -
实现
_handleTapboxChanged()
,当盒子被点击时调用的方法. -
当状态改变时,调用
setState()
更新 UI.
TapboxB 类:
-
继承 StatelessWidget 类,因为所有状态都由其父 widget 处理.
-
当检测到点击时,它会通知父 widget.
// ParentWidget manages the state for TapboxB. // ParentWidget 为 TapboxB 管理状态. //------------------------ ParentWidget -------------------------------- class ParentWidget extends StatefulWidget { @override _ParentWidgetState createState() => _ParentWidgetState(); } class _ParentWidgetState extends State<ParentWidget> { bool _active = false; void _handleTapboxChanged(bool newValue) { setState(() { _active = newValue; }); } @override Widget build(BuildContext context) { return Container( child: TapboxB( active: _active, onChanged: _handleTapboxChanged, ), ); } } //------------------------- TapboxB ---------------------------------- class TapboxB extends StatelessWidget { TapboxB({Key key, this.active: false, @required this.onChanged}) : super(key: key); final bool active; final ValueChanged<bool> onChanged; void _handleTap() { onChanged(!active); } Widget build(BuildContext context) { return GestureDetector( onTap: _handleTap, 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], ), ), ); } }
小提示 When creating API, consider using the @required
annotation for any parameters that your code relies on. To use @required
, import the foundation library (which re-exports Dart’s meta.dart library):
在创建 API 时,请考虑使用 @required
为代码所依赖的任何参数使用注解。要使用 @required
注解,请导入 foundation library(该库重新导出 Dart 的 meta.dart):
import 'package:flutter/foundation.dart';
父状态管理相比自己管理其实差别就在于事件处理和setState的逻辑实现上移到了父状态
混搭管理
对于一些 widget 来说,混搭管理的方法最合适的。在这种情况下,有状态的 widget 自己管理一些状态,同时父 widget 管理其他方面的状态。
在 TapboxC
示例中,点击时,盒子的周围会出现一个深绿色的边框。点击时,边框消失,盒子的颜色改变。TapboxC
将其 _active
状态导出到其父 widget 中,但在内部管理其 _highlight
状态。这个例子有两个状态对象 _ParentWidgetState
和 _TapboxCState
。
_ParentWidgetState
对象:
-
管理
_active
状态。 -
实现
_handleTapboxChanged()
, 此方法在盒子被点击时调用。 -
当点击盒子并且
_active
状态改变时调用setState()
来更新UI。
_TapboxCState
对象:
-
管理
_highlight
state。 -
GestureDetector
监听所有 tap 事件。当用户点下时,它添加高亮(深绿色边框);当用户释放时,会移除高亮。 -
当按下、抬起、或者取消点击时更新
_highlight
状态,调用setState()
更新UI。 -
当点击时,widget 属性将状态的改变传递给父 widget 并进行合适的操作。
//---------------------------- ParentWidget ---------------------------- class ParentWidget extends StatefulWidget { @override _ParentWidgetState createState() => _ParentWidgetState(); } class _ParentWidgetState extends State<ParentWidget> { bool _active = false; void _handleTapboxChanged(bool newValue) { setState(() { _active = newValue; }); } @override Widget build(BuildContext context) { return Container( child: TapboxC( active: _active, onChanged: _handleTapboxChanged, ), ); } } //----------------------------- TapboxC ------------------------------ class TapboxC extends StatefulWidget { TapboxC({Key key, this.active: false, @required this.onChanged}) : super(key: key); final bool active; final ValueChanged<bool> onChanged; _TapboxCState createState() => _TapboxCState(); } class _TapboxCState extends State<TapboxC> { bool _highlight = false; void _handleTapDown(TapDownDetails details) { setState(() { _highlight = true; }); } void _handleTapUp(TapUpDetails details) { setState(() { _highlight = false; }); } void _handleTapCancel() { setState(() { _highlight = false; }); } void _handleTap() { widget.onChanged(!widget.active); } Widget build(BuildContext context) { // This example adds a green border on tap down. // On tap up, the square changes to the opposite state. return GestureDetector( onTapDown: _handleTapDown, // Handle the tap events in the order that onTapUp: _handleTapUp, // they occur: down, up, tap, cancel onTap: _handleTap, onTapCancel: _handleTapCancel, child: Container( child: Center( child: Text(widget.active ? 'Active' : 'Inactive', style: TextStyle(fontSize: 32.0, color: Colors.white)), ), width: 200.0, height: 200.0, decoration: BoxDecoration( color: widget.active ? Colors.lightGreen[700] : Colors.grey[600], border: _highlight ? Border.all( color: Colors.teal[700], width: 10.0, ) : null, ), ), ); } }
另一种实现可能会将高亮状态导出到父 widget,同时保持 active 状态为内部,但如果您要求某人使用该 TapBox,他们可能会抱怨说没有多大意义。开发人员只会关心该框是否处于活动状态。开发人员可能不在乎高亮显示是如何管理的,并且倾向于让 TapBox 处理这些细节。
这样看来是内部管理状态还是外部管理状态在于外部是否需要知晓该状态,若不需要其实放在内部实现比较好,这样代码封闭性和可移植性会更好些。
其他交互式 widgets
Flutter 提供各种按钮和类似的交互式 widget。这些 widget 中的大多数都实现了 Material Design guidelines,它们定义了一组具有质感的 UI 组件。
如果你愿意,你可以使用 GestureDetector 来给任何自定义 widget 添加交互性。您可以在 管理状态 和 Flutter Gallery 中找到 GestureDetector 的示例。
小提示 Flutter also provides a set of iOS-style widgets called Cupertino.
Futter还提供了一组名为 Cupertino 的 iOS 风格的小部件。
当你需要交互性时,最容易的是使用预制的 widget。这是预置 widget 部分列表:
标准 widgets
质感组件
- Checkbox
- DropdownButton
- FlatButton
- FloatingActionButton
- IconButton
- Radio
- RaisedButton
- Slider
- Switch
- TextField
资源
以下资源可能会在给您的应用添加交互的时候有所帮助。
-
Gestures,Flutter Widget 框架总览 的一节
如何创建一个按钮并使其响应用户动作. -
Gestures in Flutter
Flutter 手势机制的描述 -
Flutter API documentation
所有 Flutter 库的参考文档. -
Flutter Gallery
一个 Demo 应用程序,展示了许多质感组件和其他 Flutter 功能 -
Flutter’s Layered Design (video)
此视频包含有关有状态和无状态 widget 的信息。由 Google 工程师 Ian Hickson 讲解。