前言
StatelessWidget、StatefulWidget中的build这篇文章, 我们一起学习了无状态组件StatelessWidget、有状态组件StatefulWidget调用build方法的过程。我们再一起回顾下这两个组件。
- Stateful widget 可以拥有状态,这些状态在 widget 生命周期中是可以变的,而 Stateless widget 是不可变的。
- Stateful widget 至少由两个类组成:
- 一个
StatefulWidget类。 - 一个
State类;StatefulWidget类本身是不变的,但是State类中持有的状态在 widget 生命周期中可能会发生变化。
- 一个
那么, 为什么要将 build 方法放在 State 中,而不是放在StatefulWidget中?带着问题, 我们一起学习下。
StatefulWidget的State类
@optionalTypeArgs
abstract class State<T extends StatefulWidget> with Diagnosticable {
/// The current configuration.
///
/// A [State] object's configuration is the corresponding [StatefulWidget]
/// instance. This property is initialized by the framework before calling
/// [initState]. If the parent updates this location in the tree to a new
/// widget with the same [runtimeType] and [Widget.key] as the current
/// configuration, the framework will update this property to refer to the new
/// widget and then call [didUpdateWidget], passing the old configuration as
/// an argument.
T get widget => _widget!;
T? _widget;
/// The current stage in the lifecycle for this state object.
///
/// This field is used by the framework when asserts are enabled to verify
/// that [State] objects move through their lifecycle in an orderly fashion.
_StateLifecycle _debugLifecycleState = _StateLifecycle.created;
/// Verifies that the [State] that was created is one that expects to be
/// created for that particular [Widget].
bool _debugTypesAreRight(Widget widget) => widget is T;
/// The location in the tree where this widget builds.
///
/// The framework associates [State] objects with a [BuildContext] after
/// creating them with [StatefulWidget.createState] and before calling
/// [initState]. The association is permanent: the [State] object will never
/// change its [BuildContext]. However, the [BuildContext] itself can be moved
/// around the tree.
///
/// After calling [dispose], the framework severs the [State] object's
/// connection with the [BuildContext].
BuildContext get context {
assert(() {
if (_element == null) {
throw FlutterError(
'This widget has been unmounted, so the State no longer has a context (and should be considered defunct). \n'
'Consider canceling any active work during "dispose" or using the "mounted" getter to determine if the State is still active.',
);
}
return true;
}());
return _element!;
}
StatefulElement? _element;
/// Whether this [State] object is currently in a tree.
///
/// After creating a [State] object and before calling [initState], the
/// framework "mounts" the [State] object by associating it with a
/// [BuildContext]. The [State] object remains mounted until the framework
/// calls [dispose], after which time the framework will never ask the [State]
/// object to [build] again.
///
/// It is an error to call [setState] unless [mounted] is true.
bool get mounted => _element != null;
/// Called when this object is inserted into the tree.
///
/// The framework will call this method exactly once for each [State] object
/// it creates.
///
/// Override this method to perform initialization that depends on the
/// location at which this object was inserted into the tree (i.e., [context])
/// or on the widget used to configure this object (i.e., [widget]).
///
/// {@template flutter.widgets.State.initState}
/// If a [State]'s [build] method depends on an object that can itself
/// change state, for example a [ChangeNotifier] or [Stream], or some
/// other object to which one can subscribe to receive notifications, then
/// be sure to subscribe and unsubscribe properly in [initState],
/// [didUpdateWidget], and [dispose]:
///
/// * In [initState], subscribe to the object.
/// * In [didUpdateWidget] unsubscribe from the old object and subscribe
/// to the new one if the updated widget configuration requires
/// replacing the object.
/// * In [dispose], unsubscribe from the object.
///
/// {@endtemplate}
///
/// You cannot use [BuildContext.dependOnInheritedWidgetOfExactType] from this
/// method. However, [didChangeDependencies] will be called immediately
/// following this method, and [BuildContext.dependOnInheritedWidgetOfExactType] can
/// be used there.
///
/// Implementations of this method should start with a call to the inherited
/// method, as in `super.initState()`.
@protected
@mustCallSuper
void initState() {
assert(_debugLifecycleState == _StateLifecycle.created);
}
/// Called whenever the widget configuration changes.
///
/// If the parent widget rebuilds and request that this location in the tree
/// update to display a new widget with the same [runtimeType] and
/// [Widget.key], the framework will update the [widget] property of this
/// [State] object to refer to the new widget and then call this method
/// with the previous widget as an argument.
///
/// Override this method to respond when the [widget] changes (e.g., to start
/// implicit animations).
///
/// The framework always calls [build] after calling [didUpdateWidget], which
/// means any calls to [setState] in [didUpdateWidget] are redundant.
///
/// {@macro flutter.widgets.State.initState}
///
/// Implementations of this method should start with a call to the inherited
/// method, as in `super.didUpdateWidget(oldWidget)`.
@mustCallSuper
@protected
void didUpdateWidget(covariant T oldWidget) { }
/// {@macro flutter.widgets.Element.reassemble}
///
/// In addition to this method being invoked, it is guaranteed that the
/// [build] method will be invoked when a reassemble is signaled. Most
/// widgets therefore do not need to do anything in the [reassemble] method.
///
/// See also:
///
/// * [Element.reassemble]
/// * [BindingBase.reassembleApplication]
/// * [Image], which uses this to reload images.
@protected
@mustCallSuper
void reassemble() { }
/// Notify the framework that the internal state of this object has changed.
///
/// Whenever you change the internal state of a [State] object, make the
/// change in a function that you pass to [setState]:
///
/// ```dart
/// setState(() { _myState = newValue; });
/// ```
///
/// The provided callback is immediately called synchronously. It must not
/// return a future (the callback cannot be `async`), since then it would be
/// unclear when the state was actually being set.
///
/// Calling [setState] notifies the framework that the internal state of this
/// object has changed in a way that might impact the user interface in this
/// subtree, which causes the framework to schedule a [build] for this [State]
/// object.
///
/// If you just change the state directly without calling [setState], the
/// framework might not schedule a [build] and the user interface for this
/// subtree might not be updated to reflect the new state.
///
/// Generally it is recommended that the `setState` method only be used to
/// wrap the actual changes to the state, not any computation that might be
/// associated with the change. For example, here a value used by the [build]
/// function is incremented, and then the change is written to disk, but only
/// the increment is wrapped in the `setState`:
///
/// ```dart
/// Future<void> _incrementCounter() async {
/// setState(() {
/// _counter++;
/// });
/// Directory directory = await getApplicationDocumentsDirectory();
/// final String dirName = directory.path;
/// await File('$dir/counter.txt').writeAsString('$_counter');
/// }
/// ```
///
/// It is an error to call this method after the framework calls [dispose].
/// You can determine whether it is legal to call this method by checking
/// whether the [mounted] property is true.
@protected
void setState(VoidCallback fn) {
assert(fn != null);
assert(() {
if (_debugLifecycleState == _StateLifecycle.defunct) {
throw FlutterError.fromParts(<DiagnosticsNode>[
ErrorSummary('setState() called after dispose(): $this'),
ErrorDescription(
'This error happens if you call setState() on a State object for a widget that '
'no longer appears in the widget tree (e.g., whose parent widget no longer '
'includes the widget in its build). This error can occur when code calls '
'setState() from a timer or an animation callback.',
),
ErrorHint(
'The preferred solution is '
'to cancel the timer or stop listening to the animation in the dispose() '
'callback. Another solution is to check the "mounted" property of this '
'object before calling setState() to ensure the object is still in the '
'tree.',
),
ErrorHint(
'This error might indicate a memory leak if setState() is being called '
'because another object is retaining a reference to this State object '
'after it has been removed from the tree. To avoid memory leaks, '
'consider breaking the reference to this object during dispose().',
),
]);
}
if (_debugLifecycleState == _StateLifecycle.created && !mounted) {
throw FlutterError.fromParts(<DiagnosticsNode>[
ErrorSummary('setState() called in constructor: $this'),
ErrorHint(
'This happens when you call setState() on a State object for a widget that '
"hasn't been inserted into the widget tree yet. It is not necessary to call "
'setState() in the constructor, since the state is already assumed to be dirty '
'when it is initially created.',
),
]);
}
return true;
}());
final Object? result = fn() as dynamic;
assert(() {
if (result is Future) {
throw FlutterError.fromParts(<DiagnosticsNode>[
ErrorSummary('setState() callback argument returned a Future.'),
ErrorDescription(
'The setState() method on $this was called with a closure or method that '
'returned a Future. Maybe it is marked as "async".',
),
ErrorHint(
'Instead of performing asynchronous work inside a call to setState(), first '
'execute the work (without updating the widget state), and then synchronously '
'update the state inside a call to setState().',
),
]);
}
// We ignore other types of return values so that you can do things like:
// setState(() => x = 3);
return true;
}());
_element!.markNeedsBuild();
}
/// Called when this object is removed from the tree.
///
/// The framework calls this method whenever it removes this [State] object
/// from the tree. In some cases, the framework will reinsert the [State]
/// object into another part of the tree (e.g., if the subtree containing this
/// [State] object is grafted from one location in the tree to another due to
/// the use of a [GlobalKey]). If that happens, the framework will call
/// [activate] to give the [State] object a chance to reacquire any resources
/// that it released in [deactivate]. It will then also call [build] to give
/// the [State] object a chance to adapt to its new location in the tree. If
/// the framework does reinsert this subtree, it will do so before the end of
/// the animation frame in which the subtree was removed from the tree. For
/// this reason, [State] objects can defer releasing most resources until the
/// framework calls their [dispose] method.
///
/// Subclasses should override this method to clean up any links between
/// this object and other elements in the tree (e.g. if you have provided an
/// ancestor with a pointer to a descendant's [RenderObject]).
///
/// Implementations of this method should end with a call to the inherited
/// method, as in `super.deactivate()`.
///
/// See also:
///
/// * [dispose], which is called after [deactivate] if the widget is removed
/// from the tree permanently.
@protected
@mustCallSuper
void deactivate() { }
/// Called when this object is reinserted into the tree after having been
/// removed via [deactivate].
///
/// In most cases, after a [State] object has been deactivated, it is _not_
/// reinserted into the tree, and its [dispose] method will be called to
/// signal that it is ready to be garbage collected.
///
/// In some cases, however, after a [State] object has been deactivated, the
/// framework will reinsert it into another part of the tree (e.g., if the
/// subtree containing this [State] object is grafted from one location in
/// the tree to another due to the use of a [GlobalKey]). If that happens,
/// the framework will call [activate] to give the [State] object a chance to
/// reacquire any resources that it released in [deactivate]. It will then
/// also call [build] to give the object a chance to adapt to its new
/// location in the tree. If the framework does reinsert this subtree, it
/// will do so before the end of the animation frame in which the subtree was
/// removed from the tree. For this reason, [State] objects can defer
/// releasing most resources until the framework calls their [dispose] method.
///
/// The framework does not call this method the first time a [State] object
/// is inserted into the tree. Instead, the framework calls [initState] in
/// that situation.
///
/// Implementations of this method should start with a call to the inherited
/// method, as in `super.activate()`.
///
/// See also:
///
/// * [Element.activate], the corresponding method when an element
/// transitions from the "inactive" to the "active" lifecycle state.
@protected
@mustCallSuper
void activate() { }
/// Called when this object is removed from the tree permanently.
///
/// The framework calls this method when this [State] object will never
/// build again. After the framework calls [dispose], the [State] object is
/// considered unmounted and the [mounted] property is false. It is an error
/// to call [setState] at this point. This stage of the lifecycle is terminal:
/// there is no way to remount a [State] object that has been disposed.
///
/// Subclasses should override this method to release any resources retained
/// by this object (e.g., stop any active animations).
///
/// {@macro flutter.widgets.State.initState}
///
/// Implementations of this method should end with a call to the inherited
/// method, as in `super.dispose()`.
///
/// See also:
///
/// * [deactivate], which is called prior to [dispose].
@protected
@mustCallSuper
void dispose() {
assert(_debugLifecycleState == _StateLifecycle.ready);
assert(() {
_debugLifecycleState = _StateLifecycle.defunct;
return true;
}());
}
/// Describes the part of the user interface represented by this widget.
///
/// The framework calls this method in a number of different situations. For
/// example:
///
/// * After calling [initState].
/// * After calling [didUpdateWidget].
/// * After receiving a call to [setState].
/// * After a dependency of this [State] object changes (e.g., an
/// [InheritedWidget] referenced by the previous [build] changes).
/// * After calling [deactivate] and then reinserting the [State] object into
/// the tree at another location.
///
/// This method can potentially be called in every frame and should not have
/// any side effects beyond building a widget.
///
/// The framework replaces the subtree below this widget with the widget
/// returned by this method, either by updating the existing subtree or by
/// removing the subtree and inflating a new subtree, depending on whether the
/// widget returned by this method can update the root of the existing
/// subtree, as determined by calling [Widget.canUpdate].
///
/// Typically implementations return a newly created constellation of widgets
/// that are configured with information from this widget's constructor, the
/// given [BuildContext], and the internal state of this [State] object.
///
/// The given [BuildContext] contains information about the location in the
/// tree at which this widget is being built. For example, the context
/// provides the set of inherited widgets for this location in the tree. The
/// [BuildContext] argument is always the same as the [context] property of
/// this [State] object and will remain the same for the lifetime of this
/// object. The [BuildContext] argument is provided redundantly here so that
/// this method matches the signature for a [WidgetBuilder].
///
/// ## Design discussion
///
/// ### Why is the [build] method on [State], and not [StatefulWidget]?
///
/// Putting a `Widget build(BuildContext context)` method on [State] rather
/// than putting a `Widget build(BuildContext context, State state)` method
/// on [StatefulWidget] gives developers more flexibility when subclassing
/// [StatefulWidget].
///
/// For example, [AnimatedWidget] is a subclass of [StatefulWidget] that
/// introduces an abstract `Widget build(BuildContext context)` method for its
/// subclasses to implement. If [StatefulWidget] already had a [build] method
/// that took a [State] argument, [AnimatedWidget] would be forced to provide
/// its [State] object to subclasses even though its [State] object is an
/// internal implementation detail of [AnimatedWidget].
///
/// Conceptually, [StatelessWidget] could also be implemented as a subclass of
/// [StatefulWidget] in a similar manner. If the [build] method were on
/// [StatefulWidget] rather than [State], that would not be possible anymore.
///
/// Putting the [build] function on [State] rather than [StatefulWidget] also
/// helps avoid a category of bugs related to closures implicitly capturing
/// `this`. If you defined a closure in a [build] function on a
/// [StatefulWidget], that closure would implicitly capture `this`, which is
/// the current widget instance, and would have the (immutable) fields of that
/// instance in scope:
///
/// ```dart
/// class MyButton extends StatefulWidget {
/// ...
/// final Color color;
///
/// @override
/// Widget build(BuildContext context, MyButtonState state) {
/// ... () { print("color: $color"); } ...
/// }
/// }
/// ```
///
/// For example, suppose the parent builds `MyButton` with `color` being blue,
/// the `$color` in the print function refers to blue, as expected. Now,
/// suppose the parent rebuilds `MyButton` with green. The closure created by
/// the first build still implicitly refers to the original widget and the
/// `$color` still prints blue even through the widget has been updated to
/// green.
///
/// In contrast, with the [build] function on the [State] object, closures
/// created during [build] implicitly capture the [State] instance instead of
/// the widget instance:
///
/// ```dart
/// class MyButtonState extends State<MyButton> {
/// ...
/// @override
/// Widget build(BuildContext context) {
/// ... () { print("color: ${widget.color}"); } ...
/// }
/// }
/// ```
///
/// Now when the parent rebuilds `MyButton` with green, the closure created by
/// the first build still refers to [State] object, which is preserved across
/// rebuilds, but the framework has updated that [State] object's [widget]
/// property to refer to the new `MyButton` instance and `${widget.color}`
/// prints green, as expected.
///
/// See also:
///
/// * [StatefulWidget], which contains the discussion on performance considerations.
@protected
Widget build(BuildContext context);
/// Called when a dependency of this [State] object changes.
///
/// For example, if the previous call to [build] referenced an
/// [InheritedWidget] that later changed, the framework would call this
/// method to notify this object about the change.
///
/// This method is also called immediately after [initState]. It is safe to
/// call [BuildContext.dependOnInheritedWidgetOfExactType] from this method.
///
/// Subclasses rarely override this method because the framework always
/// calls [build] after a dependency changes. Some subclasses do override
/// this method because they need to do some expensive work (e.g., network
/// fetches) when their dependencies change, and that work would be too
/// expensive to do for every build.
@protected
@mustCallSuper
void didChangeDependencies() { }
@override
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
super.debugFillProperties(properties);
assert(() {
properties.add(EnumProperty<_StateLifecycle>('lifecycle state', _debugLifecycleState, defaultValue: _StateLifecycle.ready));
return true;
}());
properties.add(ObjectFlagProperty<T>('_widget', _widget, ifNull: 'no widget'));
properties.add(ObjectFlagProperty<StatefulElement>('_element', _element, ifNull: 'not mounted'));
}
}
我们重点学习build方法注解。build方法调用时机是怎样的? 我们可以得到一下结论。
initState调用后didUpdateWidget调用后setState调用后- 在这个[State]对象的依赖关系改变之后(例如,一个[InheritedWidget]引用之前的[build]更改)
- 调用[deactivate](使当前页面失去活性, 从当前页面push到下一个页面时, 当前页面会失去活性),然后在另一个位置重新插入[State]对象到树中。
继承StatefulWidget不便
将Widget构建(BuildContext context)方法放到[State]上,而不是将Widget构建(BuildContext context, State State)方法放到[StatefulWidget]上,可以让开发人员在子类化时更灵活
[StatefulWidget]。
AnimatedWidget源码如下
abstract class AnimatedWidget extends StatefulWidget {
/// Creates a widget that rebuilds when the given listenable changes.
///
/// The [listenable] argument is required.
const AnimatedWidget({
Key? key,
required this.listenable,
}) : assert(listenable != null),
super(key: key);
/// The [Listenable] to which this widget is listening.
///
/// Commonly an [Animation] or a [ChangeNotifier].
final Listenable listenable;
/// Override this method to build widgets that depend on the state of the
/// listenable (e.g., the current value of the animation).
@protected
Widget build(BuildContext context);
/// Subclasses typically do not override this method.
@override
State<AnimatedWidget> createState() => _AnimatedState();
@override
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
super.debugFillProperties(properties);
properties.add(DiagnosticsProperty<Listenable>('animation', listenable));
}
}
例如,[AnimatedWidget]是[StatefulWidget]的子类,它为其子类引入了一个抽象的Widget构建(BuildContext context)方法来实现。继承自AnimatedWidget的动画 widget 都要实现这个build方法。如果[StatefulWidget]已经有一个带有[State]参数的[build]方法,[AnimatedWidget]将被迫提供它的[State]对象给子类,即使它的[State]对象是[AnimatedWidget]的内部实现细节。
class MyAnimationWidget extends AnimatedWidget{
@override
Widget build(BuildContext context, State state){
//由于子类要用到AnimatedWidget的状态对象_animatedWidgetState,
//所以AnimatedWidget必须通过某种方式将其状态对象_animatedWidgetState
//暴露给其子类
super.build(context, _animatedWidgetState)
}
}
改造后, 如果[StatefulWidget]已经有一个带有[State]参数的[build]方法, 那么, [AnimatedWidget]将被迫提供它的[State]对象给子类(也就是, 我们自定义的MyAnimationWidget)。
因为子类MyAnimationWidget需要在其build方法中调用父类的build方法。
这样很显然是不合理的,因为
AnimatedWidget的状态对象是AnimatedWidget内部实现细节,不应该暴露给外部。- 如果要将父类状态
_AnimatedState暴露给子类,那么必须得有一种传递机制,而做这一套传递机制是无意义的,因为父子类之间状态的传递和子类本身逻辑是无关的。
状态访问不便
试想一下,如果我们的StatefulWidget有很多状态,而每次状态改变都要调用build方法,由于状态是保存在 State 中的,如果build方法在StatefulWidget中,那么build方法和状态分别在两个类中,那么构建时读取状态将会很不方便!试想一下,如果真的将build方法放在 StatefulWidget 中的话,由于构建用户界面过程需要依赖 State,所以build方法将必须加一个State参数,大概是下面这样:
class MyWidget extends StatefulWidget {
@override
Widget build(BuildContext context, MyWidgetState state){
print('${state.color}');
//state.color
return Container();
}
}
class _MyWidgetState extends State<MyWidget> {
final Color color;
}
这样的话就只能将State的所有状态声明为公开的状态,这样才能在State类外部访问状态!但是,将状态设置为公开后,状态将不再具有私密性,这就会导致对状态的修改将会变的不可控。但如果将build()方法放在State中的话,构建过程不仅可以直接访问状态,而且也无需公开私有状态,这会非常方便。
闭包 this 指向异常
假设 build 方法在 StatefulWidget 中,StatefulWidget 的子类写法如下:
class MyWidget extends StatefulWidget {
final Color color;
@override
Widget build(BuildContext context, MyWidgetState state) {
print('${this.color}');
return Container();
}
}
此时的 this 指向的是 MyWidget 的实例,然后父组件改变颜色,重新构建 MyWidget 组件,前一个 MyWidget 的实例中的 this 依然指向前一个 MyWidget 的实例,颜色并未发生变化。
如果 build 方法在 State 中,代码如下:
class MyWidget extends StatefulWidget {
final Color color;
const MyWidget({Key key, this.color}) : super(key: key);
@override
_MyWidgetState createState() => _MyWidgetState();
}
class _MyWidgetState extends State<MyWidget> {
@override
Widget build(BuildContext context) {
print('${widget.color}');
return Container();
}
}
同样,父组件改变颜色,重新构建 MyWidget 组件,此时框架更新了 State 对象的 widget 属性的引用,新的 MyWidget 实例和 $ {widget.color} 将显示绿色。
总结
有状态的组件包含StatefulWidget 和 State,当有状态组件的配置发生更改时,StatefulWidget 将会被丢弃并重建,而 State 不会重建,框架会更新 State 对象中 widget 的引用,这极大的减轻了系统重建有状态组件的工作
此方式对动画来讲极为重要,因为 State 不会被重建,保留了前面的状态,不断的根据前一个状态计算下一个状态并重建其widget,达到动画的效果。