Fix the losing of state when pushing opaque routes (#5624)

Fixes https://github.com/flutter/flutter/issues/5283

Other changes in this patch:

Rename OffStage to Offstage.
Fixes https://github.com/flutter/flutter/issues/5378

Add a lot of docs.

Some minor punctuation and whitespace fixes.
This commit is contained in:
Ian Hickson 2016-08-29 11:28:37 -07:00 committed by GitHub
parent 43d0eeb8e1
commit ea6bf4706a
28 changed files with 1199 additions and 335 deletions

View File

@ -148,12 +148,17 @@ class _CupertinoBackGestureController extends NavigationGestureController {
///
/// [MaterialApp] creates material page routes for entries in the
/// [MaterialApp.routes] map.
///
/// By default, when a modal route is replaced by another, the previous route
/// remains in memory. To free all the resources when this is not necessary, set
/// [maintainState] to false.
class MaterialPageRoute<T> extends PageRoute<T> {
/// Creates a page route for use in a material design app.
MaterialPageRoute({
this.builder,
Completer<T> completer,
RouteSettings settings: const RouteSettings()
RouteSettings settings: const RouteSettings(),
this.maintainState: true,
}) : super(completer: completer, settings: settings) {
assert(builder != null);
assert(opaque);
@ -162,6 +167,9 @@ class MaterialPageRoute<T> extends PageRoute<T> {
/// Builds the primary contents of the route.
final WidgetBuilder builder;
@override
final bool maintainState;
@override
Duration get transitionDuration => const Duration(milliseconds: 300);

View File

@ -148,7 +148,53 @@ abstract class RendererBinding extends BindingBase implements SchedulerBinding,
/// Pump the rendering pipeline to generate a frame.
///
/// Called automatically by the engine when it is time to lay out and paint a frame.
/// This method is called by [handleBeginFrame], which itself is called
/// automatically by the engine when when it is time to lay out and paint a
/// frame.
///
/// Each frame consists of the following phases:
///
/// 1. The animation phase: The [handleBeginFrame] method, which is registered
/// with [ui.window.onBeginFrame], invokes all the transient frame callbacks
/// registered with [scheduleFrameCallback] and [addFrameCallback], in
/// registration order. This includes all the [Ticker] instances that are
/// driving [AnimationController] objects, which means all of the active
/// [Animation] objects tick at this point.
///
/// [handleBeginFrame] then invokes all the persistent frame callbacks, of which
/// the most notable is this method, [beginFrame], which proceeds as follows:
///
/// 2. The layout phase: All the dirty [RenderObject]s in the system are laid
/// out (see [RenderObject.performLayout]). See [RenderObject.markNeedsLayout]
/// for further details on marking an object dirty for layout.
///
/// 3. The compositing bits phase: The compositing bits on any dirty
/// [RenderObject] objects are updated. See
/// [RenderObject.markNeedsCompositingBitsUpdate].
///
/// 4. The paint phase: All the dirty [RenderObject]s in the system are
/// repainted (see [RenderObject.paint]). This generates the [Layer] tree. See
/// [RenderObject.markNeedsPaint] for further details on marking an object
/// dirty for paint.
///
/// 5. The compositing phase: The layer tree is turned into a [ui.Scene] and
/// sent to the GPU.
///
/// 6. The semantics phase: All the dirty [RenderObject]s in the system have
/// their semantics updated (see [RenderObject.semanticAnnotator]). This
/// generates the [SemanticsNode] tree. See
/// [RenderObject.markNeedsSemanticsUpdate] for further details on marking an
/// object dirty for semantics.
///
/// For more details on steps 2-6, see [PipelineOwner].
///
/// 7. The finalization phase: After [beginFrame] returns, [handleBeginFrame]
/// then invokes post-frame callbacks (registered with [addPostFrameCallback].
///
/// Some bindings (for example, the [WidgetsBinding]) add extra steps to this
/// list.
//
// When editing the above, also update widgets/binding.dart's copy.
void beginFrame() {
assert(renderView != null);
pipelineOwner.flushLayout();

View File

@ -12,7 +12,7 @@ import 'package:mojo_services/mojo/gfx/composition/scene_token.mojom.dart' as mo
import 'debug.dart';
/// A composited layer
/// A composited layer.
///
/// During painting, the render tree generates a tree of composited layers that
/// are uploaded into the engine and displayed by the compositor. This class is

View File

@ -1956,26 +1956,24 @@ abstract class RenderObject extends AbstractNode implements HitTestTarget {
/// Mark this node as needing an update to its semantics
/// description.
///
/// If the change did not involve a removal or addition of
/// semantics, only the change of semantics (e.g. isChecked changing
/// from true to false, as opposed to isChecked changing from being
/// true to not being changed at all), then you can pass the
/// onlyChanges argument with the value true to reduce the cost. If
/// semantics are being added or removed, more work needs to be done
/// to update the semantics tree. If you pass 'onlyChanges: true'
/// but this node, which previously had a SemanticsNode, no longer
/// has one, or previously did not set any semantics, but now does,
/// or previously had a child that returned annotators, but no
/// longer does, or other such combinations, then you will either
/// assert during the subsequent call to [flushSemantics()] or you
/// will have out-of-date information in the semantics tree.
/// If the change did not involve a removal or addition of semantics, only the
/// change of semantics (e.g. isChecked changing from true to false, as
/// opposed to isChecked changing from being true to not being changed at
/// all), then you can pass the onlyChanges argument with the value true to
/// reduce the cost. If semantics are being added or removed, more work needs
/// to be done to update the semantics tree. If you pass 'onlyChanges: true'
/// but this node, which previously had a SemanticsNode, no longer has one, or
/// previously did not set any semantics, but now does, or previously had a
/// child that returned annotators, but no longer does, or other such
/// combinations, then you will either assert during the subsequent call to
/// [PipelineOwner.flushSemantics()] or you will have out-of-date information
/// in the semantics tree.
///
/// If the geometry might have changed in any way, then again, more
/// work needs to be done to update the semantics tree (to deal with
/// clips). You can pass the noGeometry argument to avoid this work
/// in the case where only the labels or flags changed. If you pass
/// 'noGeometry: true' when the geometry did change, the semantic
/// tree will be out of date.
/// If the geometry might have changed in any way, then again, more work needs
/// to be done to update the semantics tree (to deal with clips). You can pass
/// the noGeometry argument to avoid this work in the case where only the
/// labels or flags changed. If you pass 'noGeometry: true' when the geometry
/// did change, the semantic tree will be out of date.
void markNeedsSemanticsUpdate({ bool onlyChanges: false, bool noGeometry: false }) {
assert(!attached || !owner._debugDoingSemantics);
if ((attached && owner._semanticsOwner == null) || (_needsSemanticsUpdate && onlyChanges && (_needsSemanticsGeometryUpdate || noGeometry)))
@ -2089,31 +2087,30 @@ abstract class RenderObject extends AbstractNode implements HitTestTarget {
visitChildren(visitor);
}
/// Returns functions that will annotate a SemanticsNode with the
/// semantics of this RenderObject.
/// Returns a function that will annotate a [SemanticsNode] with the semantics
/// of this [RenderObject].
///
/// To annotate a SemanticsNode for this node, return all the
/// annotators provided by the superclass, plus an annotator that
/// adds the annotations. When the behavior of the annotators would
/// To annotate a SemanticsNode for this node, return an annotator that
/// adds the annotations. When the behavior of the annotator would
/// change (e.g. the box is now checked rather than unchecked), call
/// [markNeedsSemanticsUpdate()] to indicate to the rendering system
/// [markNeedsSemanticsUpdate] to indicate to the rendering system
/// that the semantics tree needs to be rebuilt.
///
/// To introduce a new SemanticsNode, set hasSemantics to true for
/// this object. The functions returned by this function will be used
/// this object. The function returned by this function will be used
/// to annotate the SemanticsNode for this object.
///
/// Semantic annotations are persistent. Values set in one pass will
/// still be set in the next pass. Therefore it is important to
/// explicitly set fields to false once they are no longer true --
/// explicitly set fields to false once they are no longer true;
/// setting them to true when they are to be enabled, and not
/// setting them at all when they are not, will mean they remain set
/// once enabled once and will never get unset.
///
/// If the number of annotators you return will change from zero to
/// non-zero, and hasSemantics isn't true, then the associated call
/// to markNeedsSemanticsUpdate() must not have 'onlyChanges' set, as
/// it is possible that the node should be entirely removed.
/// If the value return will change from null to non-null (or vice versa), and
/// [hasSemantics] isn't true, then the associated call to
/// [markNeedsSemanticsUpdate] must not have `onlyChanges` set, as it is
/// possible that the node should be entirely removed.
SemanticAnnotator get semanticAnnotator => null;
@ -2247,6 +2244,12 @@ abstract class RenderObjectWithChildMixin<ChildType extends RenderObject> implem
_child.detach();
}
@override
void redepthChildren() {
if (_child != null)
redepthChild(_child);
}
@override
void visitChildren(RenderObjectVisitor visitor) {
if (_child != null)

View File

@ -32,7 +32,7 @@ export 'package:flutter/gestures.dart' show
/// the proxy box with its child. However, RenderProxyBox is a useful base class
/// for render objects that wish to mimic most, but not all, of the properties
/// of their child.
class RenderProxyBox extends RenderBox with RenderObjectWithChildMixin<RenderBox> {
class RenderProxyBox extends RenderBox with RenderObjectWithChildMixin<RenderBox>, RenderProxyBoxMixin {
/// Creates a proxy render box.
///
/// Proxy render boxes are rarely created directly because they simply proxy
@ -41,7 +41,15 @@ class RenderProxyBox extends RenderBox with RenderObjectWithChildMixin<RenderBox
RenderProxyBox([RenderBox child = null]) {
this.child = child;
}
}
/// Implementation of [RenderProxyBox].
///
/// This class can be used as a mixin for situations where the proxying behavior
/// of [RenderProxyBox] is desired but inheriting from [RenderProxyBox] is
/// impractical (e.g. because you want to mix in other classes as well).
// TODO(ianh): Remove this class once https://github.com/dart-lang/sdk/issues/15101 is fixed
abstract class RenderProxyBoxMixin implements RenderBox, RenderObjectWithChildMixin<RenderBox> {
@override
double computeMinIntrinsicWidth(double height) {
if (child != null)
@ -2016,9 +2024,9 @@ class RenderIgnorePointer extends RenderProxyBox {
/// Lays the child out as if it was in the tree, but without painting anything,
/// without making the child available for hit testing, and without taking any
/// room in the parent.
class RenderOffStage extends RenderProxyBox {
/// Creates an off-stage render object.
RenderOffStage({
class RenderOffstage extends RenderProxyBox {
/// Creates an offstage render object.
RenderOffstage({
bool offstage: true,
RenderBox child
}) : _offstage = offstage, super(child) {

View File

@ -422,14 +422,14 @@ class RenderPositionedBox extends RenderAligningShiftedBox {
/// ignoring the child's dimensions.
///
/// For example, if you wanted a box to always render 50 pixels high, regardless
/// of where it was rendered, you would wrap it in a RenderOverflow with
/// minHeight and maxHeight set to 50.0. Generally speaking, to avoid confusing
/// behavior around hit testing, a RenderOverflowBox should usually be wrapped
/// in a RenderClipRect.
/// of where it was rendered, you would wrap it in a
/// RenderConstrainedOverflowBox with minHeight and maxHeight set to 50.0.
/// Generally speaking, to avoid confusing behavior around hit testing, a
/// RenderConstrainedOverflowBox should usually be wrapped in a RenderClipRect.
///
/// The child is positioned at the top left of the box. To position a smaller
/// The child is positioned according to [alignment]. To position a smaller
/// child inside a larger parent, use [RenderPositionedBox] and
/// [RenderConstrainedBox] rather than RenderOverflowBox.
/// [RenderConstrainedBox] rather than RenderConstrainedOverflowBox.
class RenderConstrainedOverflowBox extends RenderAligningShiftedBox {
/// Creates a render object that lets its child overflow itself.
RenderConstrainedOverflowBox({

View File

@ -199,13 +199,13 @@ class StackParentData extends ContainerBoxParentDataMixin<RenderBox> {
}
}
/// Whether overflowing children should be clipped, or their overflows be
/// Whether overflowing children should be clipped, or their overflow be
/// visible.
enum Overflow {
/// Children's overflows will be visible.
/// Overflowing children will be visible.
visible,
/// Children's overflows will be clipped.
clip
/// Overflowing children will be clipped to the bounds of their parent.
clip,
}
/// Implements the stack layout algorithm

View File

@ -1077,9 +1077,9 @@ class SizedOverflowBox extends SingleChildRenderObjectWidget {
/// A widget that lays the child out as if it was in the tree, but without painting anything,
/// without making the child available for hit testing, and without taking any
/// room in the parent.
class OffStage extends SingleChildRenderObjectWidget {
class Offstage extends SingleChildRenderObjectWidget {
/// Creates a widget that visually hides its child.
OffStage({ Key key, this.offstage: true, Widget child })
Offstage({ Key key, this.offstage: true, Widget child })
: super(key: key, child: child) {
assert(offstage != null);
}
@ -1094,10 +1094,10 @@ class OffStage extends SingleChildRenderObjectWidget {
final bool offstage;
@override
RenderOffStage createRenderObject(BuildContext context) => new RenderOffStage(offstage: offstage);
RenderOffstage createRenderObject(BuildContext context) => new RenderOffstage(offstage: offstage);
@override
void updateRenderObject(BuildContext context, RenderOffStage renderObject) {
void updateRenderObject(BuildContext context, RenderOffstage renderObject) {
renderObject.offstage = offstage;
}
@ -1106,6 +1106,22 @@ class OffStage extends SingleChildRenderObjectWidget {
super.debugFillDescription(description);
description.add('offstage: $offstage');
}
@override
_OffstageElement createElement() => new _OffstageElement(this);
}
class _OffstageElement extends SingleChildRenderObjectElement {
_OffstageElement(Offstage widget) : super(widget);
@override
Offstage get widget => super.widget;
@override
void visitChildrenForSemantics(ElementVisitor visitor) {
if (!widget.offstage)
super.visitChildrenForSemantics(visitor);
}
}
/// A widget that attempts to size the child to a specific aspect ratio.
@ -2646,6 +2662,14 @@ class IgnorePointer extends SingleChildRenderObjectWidget {
..ignoring = ignoring
..ignoringSemantics = ignoringSemantics;
}
@override
void debugFillDescription(List<String> description) {
super.debugFillDescription(description);
description.add('ignoring: $ignoring');
if (ignoringSemantics != null)
description.add('ignoringSemantics: $ignoringSemantics');
}
}
/// A widget that absorbs pointers during hit testing.

View File

@ -201,6 +201,63 @@ abstract class WidgetsBinding extends BindingBase implements GestureBinding, Ren
bool _buildingDirtyElements = false;
/// Pump the build and rendering pipeline to generate a frame.
///
/// This method is called by [handleBeginFrame], which itself is called
/// automatically by the engine when when it is time to lay out and paint a
/// frame.
///
/// Each frame consists of the following phases:
///
/// 1. The animation phase: The [handleBeginFrame] method, which is registered
/// with [ui.window.onBeginFrame], invokes all the transient frame callbacks
/// registered with [scheduleFrameCallback] and [addFrameCallback], in
/// registration order. This includes all the [Ticker] instances that are
/// driving [AnimationController] objects, which means all of the active
/// [Animation] objects tick at this point.
///
/// [handleBeginFrame] then invokes all the persistent frame callbacks, of which
/// the most notable is this method, [beginFrame], which proceeds as follows:
///
/// 2. The build phase: All the dirty [Element]s in the widget tree are
/// rebuilt (see [State.build]). See [State.setState] for further details on
/// marking a widget dirty for building. See [BuildOwner] for more information
/// on this step.
///
/// 3. The layout phase: All the dirty [RenderObject]s in the system are laid
/// out (see [RenderObject.performLayout]). See [RenderObject.markNeedsLayout]
/// for further details on marking an object dirty for layout.
///
/// 4. The compositing bits phase: The compositing bits on any dirty
/// [RenderObject] objects are updated. See
/// [RenderObject.markNeedsCompositingBitsUpdate].
///
/// 5. The paint phase: All the dirty [RenderObject]s in the system are
/// repainted (see [RenderObject.paint]). This generates the [Layer] tree. See
/// [RenderObject.markNeedsPaint] for further details on marking an object
/// dirty for paint.
///
/// 6. The compositing phase: The layer tree is turned into a [ui.Scene] and
/// sent to the GPU.
///
/// 7. The semantics phase: All the dirty [RenderObject]s in the system have
/// their semantics updated (see [RenderObject.semanticAnnotator]). This
/// generates the [SemanticsNode] tree. See
/// [RenderObject.markNeedsSemanticsUpdate] for further details on marking an
/// object dirty for semantics.
///
/// For more details on steps 3-7, see [PipelineOwner].
///
/// 8. The finalization phase in the widgets layer: The widgets tree is
/// finalized. This causes [State.dispose] to be invoked on any objects that
/// were removed from the widgets tree this frame. See
/// [BuildOwner.finalizeTree] for more details.
///
/// 9. The finalization phase in the scheduler layer: After [beginFrame]
/// returns, [handleBeginFrame] then invokes post-frame callbacks (registered
/// with [addPostFrameCallback].
//
// When editing the above, also update rendering/binding.dart's copy.
@override
void beginFrame() {
assert(!_buildingDirtyElements);
@ -210,6 +267,7 @@ abstract class WidgetsBinding extends BindingBase implements GestureBinding, Ren
super.beginFrame();
buildOwner.finalizeTree();
// TODO(ianh): Following code should not be included in release mode, only profile and debug modes.
// See https://github.com/dart-lang/sdk/issues/27192
if (_needToReportFirstFrame) {
if (_thisFrameWasUseful) {
developer.Timeline.instantSync('Widgets completed first useful frame');
@ -298,7 +356,8 @@ class RenderObjectToWidgetAdapter<T extends RenderObject> extends RenderObjectWi
@override
void updateRenderObject(BuildContext context, RenderObject renderObject) { }
/// Inflate this widget and actually set the resulting [RenderObject] as the child of [container].
/// Inflate this widget and actually set the resulting [RenderObject] as the
/// child of [container].
///
/// If `element` is null, this function will create a new element. Otherwise,
/// the given element will be updated with this widget.

View File

@ -11,6 +11,12 @@ import 'table.dart';
/// Log the dirty widgets that are built each frame.
bool debugPrintRebuildDirtyWidgets = false;
/// Log when widgets with global keys are deactivated and log when they are
/// reactivated (retaken).
///
/// This can help track down framework bugs relating to the [GlobalKey] logic.
bool debugPrintGlobalKeyedWidgetLifecycle = false;
Key _firstNonUniqueKey(Iterable<Widget> widgets) {
Set<Key> keySet = new HashSet<Key>();
for (Widget widget in widgets) {

View File

@ -968,6 +968,16 @@ abstract class _ProxyWidget extends Widget {
final Widget child;
}
/// Base class for widgets that hook [ParentData] information to children of
/// [RenderObjectWidget]s.
///
/// This can be used to provide per-child configuration for
/// [RenderObjectWidget]s with more than one child. For example, [Stack] uses
/// the [Positioned] parent data widget to position each child.
///
/// A [ParentDataWidget] is specific to a particular kind of [RenderObject], and
/// thus also to a particular [RenderObjectWidget] class. That class is `T`, the
/// [ParentDataWidget] type argument.
abstract class ParentDataWidget<T extends RenderObjectWidget> extends _ProxyWidget {
const ParentDataWidget({ Key key, Widget child })
: super(key: key, child: child);
@ -989,7 +999,7 @@ abstract class ParentDataWidget<T extends RenderObjectWidget> extends _ProxyWidg
/// Subclasses should override this to describe the requirements for using the
/// ParentDataWidget subclass. It is called when debugIsValidAncestor()
/// returned false for an ancestor, or when there are extraneous
/// ParentDataWidgets in the ancestor chain.
/// [ParentDataWidget]s in the ancestor chain.
String debugDescribeInvalidAncestorChain({ String description, String ownershipChain, bool foundValidAncestor, Iterable<Widget> badAncestors }) {
assert(T != dynamic);
assert(T != RenderObjectWidget);
@ -1018,6 +1028,13 @@ abstract class ParentDataWidget<T extends RenderObjectWidget> extends _ProxyWidg
void applyParentData(RenderObject renderObject);
}
/// Base class for widgets that efficiently propagate information down the tree.
///
/// To obtain the nearest instance of a particular type of inherited widget from
/// a build context, use [BuildContext.inheritFromWidgetOfExactType].
///
/// Inherited widgets, when referenced in this way, will cause the consumer to
/// rebuild when the inherited widget itself changes state.
abstract class InheritedWidget extends _ProxyWidget {
const InheritedWidget({ Key key, Widget child })
: super(key: key, child: child);
@ -1088,6 +1105,11 @@ abstract class MultiChildRenderObjectWidget extends RenderObjectWidget {
assert(!children.any((Widget child) => child == null));
}
/// The widgets below this widget in the tree.
///
/// If this list is going to be mutated, it is usually wise to put [Key]s on
/// the widgets, so that the framework can match old configurations to new
/// configurations and maintain the underlying render objects.
final List<Widget> children;
@override
@ -1163,21 +1185,226 @@ class _InactiveElements {
/// this callback.
typedef void ElementVisitor(Element element);
/// A handle to the location of a widget in the widget tree.
///
/// This class presents a set of methods that can be used from
/// [StatelessWidget.build] methods and from methods on [State] objects.
///
/// [BuildContext] objects are passed to [WidgetBuilder] functions (such as
/// [StatelessWidget.build]), and are available from the [State.context] member.
/// Some static functions (e.g. [showDialog], [Theme.of], and so forth) also
/// take build contexts so that they can act on behalf of the calling widget, or
/// obtain data specifically for the given context.
///
/// Each widget has its own [BuildContext], which becomes the parent of the
/// widget returned by the [StatelessWidget.build] or [State.build] function.
/// (And similarly, the parent of any children for [RenderObjectWidget]s.)
///
/// In particular, this means that within a build method, the build context of
/// the widget of the build method is not the same as the build context of the
/// widgets returned by that build method. This can lead to some tricky cases.
/// For example, [Theme.of(context)] looks for the nearest enclosing [Theme] of
/// the given build context. If a build method for a widget Q includes a [Theme]
/// within its returned widget tree, and attempts to use [Theme.of] passing its
/// own context, the build method for Q will not find that [Theme] object. It
/// will instead find whatever [Theme] was an ancestor to the widget Q. If the
/// build context for a subpart of the returned tree is needed, a [Builder]
/// widget can be used: the build context passed to the [Builder.builder]
/// callback will be that of the [Builder] itself.
///
/// For example, in the following snippet, the [ScaffoldState.showSnackBar]
/// method is called on the [Scaffold] widget that the build method itself
/// creates. If a [Builder] had not been used, and instead the `context`
/// argument of the build method itself had been used, no [Scaffold] would have
/// been found, and the [Scaffold.of] function would have returned null.
///
/// ```dart
/// @override
/// Widget build(BuildContext context) {
/// // here, Scaffold.of(context) returns null
/// return new Scaffold(
/// appBar: new AppBar(title: new Text('Demo')),
/// body: new Builder(
/// builder: (BuildContext context) {
/// return new FlatButton(
/// child: new Text('BUTTON'),
/// onPressed: () {
/// // here, Scaffold.of(context) returns the locally created Scaffold
/// Scaffold.of(context).showSnackBar(new SnackBar(
/// content: new Text('Hello.')
/// ));
/// }
/// );
/// }
/// )
/// );
/// }
/// ```
///
/// The [BuildContext] for a particular widget can change location over time as
/// the widget is moved around the tree. Because of this, values returned from
/// the methods on this class should not be cached beyond the execution of a
/// single synchronous function.
///
/// [BuildContext] objects are actually [Element] objects. The [BuildContext]
/// interface is used to discourage direct manipulation of [Element] objects.
abstract class BuildContext {
/// The current configuration of the [Element] that is this [BuildContext].
Widget get widget;
/// The current [RenderObject] for the widget. If the widget is a
/// [RenderObjectWidget], this is the render object that the widget created
/// for itself. Otherwise, it is the render object of the first descendant
/// [RenderObjectWidget].
///
/// This method will only return a valid result after the build phase is
/// complete. It is therefore not valid to call this from the [build] function
/// itself. It should only be called from interaction event handlers (e.g.
/// gesture callbacks) or layout or paint callbacks.
///
/// If the render object is a [RenderBox], which is the common case, then the
/// size of the render object can be obtained from the [RenderBox.size]
/// getter. This is only valid after the layout phase, and should therefore
/// only be examined from paint callbacks or interaction event handlers (e.g.
/// gesture callbacks).
///
/// For details on the different phases of a frame, see
/// [WidgetsBinding.beginFrame].
///
/// Calling this method is theoretically relatively expensive (O(N) in the
/// depth of the tree), but in practice is usually cheap because the tree
/// usually has many render objects and therefore the distance to the nearest
/// render object is usually short.
RenderObject findRenderObject();
/// Obtains the nearest widget of the given type, which must be the type of a
/// concrete [InheritedWidget] subclass, and registers this build context with
/// that widget such that when that widget changes (or a new widget of that
/// type is introduced, or the widget goes away), this build context is
/// rebuilt so that it can obtain new values from that widget.
///
/// This is typically called implicitly from `of()` static methods, e.g.
/// [Theme.of].
///
/// This should not be called from widget constructors or from
/// [State.initState] methods, because those methods would not get called
/// again if the inherited value were to change. To ensure that the widget
/// correctly updates itself when the inherited value changes, only call this
/// (directly or indirectly) from build methods or layout and paint callbacks.
///
/// It is also possible to call this from interaction event handlers (e.g.
/// gesture callbacks) or timers, to obtain a value once, if that value is not
/// going to be cached and reused later.
///
/// Calling this method is O(1) with a small constant factor, but will lead to
/// the widget being rebuilt more often.
InheritedWidget inheritFromWidgetOfExactType(Type targetType);
/// Returns the nearest ancestor widget of the given type, which must be the
/// type of a concrete [Widget] subclass.
///
/// This should not be used from build methods, because the build context will
/// not be rebuilt if the value that would be returned by this method changes.
/// In general, [inheritFromWidgetOfExactType] is more useful. This method is
/// appropriate when used in interaction event handlers (e.g. gesture
/// callbacks), or for performing one-off tasks.
///
/// Calling this method is relatively expensive (O(N) in the depth of the
/// tree). Only call this method if the distance from this widget to the
/// desired ancestor is known to be small and bounded.
Widget ancestorWidgetOfExactType(Type targetType);
/// Returns the [State] object of the nearest ancestor [StatefulWidget] widget
/// that matches the given [TypeMatcher].
///
/// This should not be used from build methods, because the build context will
/// not be rebuilt if the value that would be returned by this method changes.
/// In general, [inheritFromWidgetOfExactType] is more appropriate for such
/// cases. This method is useful to change the state of an ancestor widget in
/// a one-off manner, for example, to cause an ancestor scrolling list to
/// scroll this build context's widget into view, or to move the focus in
/// response to user interaction.
///
/// In general, though, consider using a callback that triggers a stateful
/// change in the ancestor rather than using the imperative style implied by
/// this method. This will usually lead to more maintainable and reusable code
/// since it decouples widgets from each other.
///
/// Calling this method is relatively expensive (O(N) in the depth of the
/// tree). Only call this method if the distance from this widget to the
/// desired ancestor is known to be small and bounded.
State ancestorStateOfType(TypeMatcher matcher);
/// Returns the [RenderObject] object of the nearest ancestor [RenderObjectWidget] widget
/// that matches the given [TypeMatcher].
///
/// This should not be used from build methods, because the build context will
/// not be rebuilt if the value that would be returned by this method changes.
/// In general, [inheritFromWidgetOfExactType] is more appropriate for such
/// cases. This method is useful only in esoteric cases where a widget needs
/// to cause an ancestor to change its layout or paint behavior. For example,
/// it is used by [Material] so that [InkWell] widgets can trigger the ink
/// splash on the [Material]'s actual render object.
///
/// Calling this method is relatively expensive (O(N) in the depth of the
/// tree). Only call this method if the distance from this widget to the
/// desired ancestor is known to be small and bounded.
RenderObject ancestorRenderObjectOfType(TypeMatcher matcher);
/// Walks the ancestor chain, starting with the parent of this build context's
/// widget, invoking the argument for each ancestor. The callback is given a
/// reference to the ancestor widget's corresponding [Element] object. The
/// walk stops when it reaches the root widget or when the callback returns
/// false. The callback must not return null.
///
/// This is useful for inspecting the widget tree.
///
/// Calling this method is relatively expensive (O(N) in the depth of the tree).
void visitAncestorElements(bool visitor(Element element));
/// Walks the children of this widget.
///
/// This is useful for applying changes to children after they are built
/// without waiting for the next frame, especially if the children are known,
/// and especially if there is exactly one child (as is always the case for
/// [StatefulWidget]s or [StatelessWidget]s).
///
/// Calling this method is very cheap for build contexts that correspond to
/// [StatefulWidget]s or [StatelessWidget]s (O(1), since there's only one
/// child).
///
/// Calling this method is potentially expensive for build contexts that
/// correspond to [RenderObjectWidget]s (O(N) in the number of children).
///
/// Calling this method recursively is extremely expensive (O(N) in the number
/// of descendants), and should be avoided if possible. Generally it is
/// significantly cheaper to use an [InheritedWidget] and have the descendants
/// pull data down, than it is to use [visitChildElements] recursively to push
/// data down to them.
void visitChildElements(ElementVisitor visitor);
}
/// Manager class for the widgets framework.
///
/// This class tracks which widgets need rebuilding, and handles other tasks
/// that apply to widget trees as a whole, such as managing the inactive element
/// list for the tree and triggering the "reassemble" command when necessary
/// during debugging.
///
/// The main build owner is typically owned by the [WidgetsBinding], and is
/// driven from the operating system along with the rest of the
/// build/layout/paint pipeline.
///
/// Additional build owners can be built to manage off-screen widget trees.
///
/// To assign a build owner to a tree, use the
/// [RootRenderObjectElement.assignOwner] method on the root element of the
/// widget tree.
class BuildOwner {
BuildOwner({ this.onBuildScheduled });
/// Called on each build pass when the first buildable element is marked dirty
/// Called on each build pass when the first buildable element is marked
/// dirty.
VoidCallback onBuildScheduled;
final _InactiveElements _inactiveElements = new _InactiveElements();
@ -1185,7 +1412,7 @@ class BuildOwner {
final List<BuildableElement> _dirtyElements = <BuildableElement>[];
/// Adds an element to the dirty elements list so that it will be rebuilt
/// when buildDirtyElements is called.
/// when [buildDirtyElements] is called.
void scheduleBuildFor(BuildableElement element) {
assert(() {
if (_dirtyElements.contains(element)) {
@ -1271,10 +1498,11 @@ class BuildOwner {
return 0;
}
/// Builds all the elements that were marked as dirty using schedule(), in depth order.
/// If elements are marked as dirty while this runs, they must be deeper than the algorithm
/// has yet reached.
/// This is called by beginFrame().
/// Builds all the elements that were marked as dirty using
/// [scheduleBuildFor], in depth order. If elements are marked as dirty while
/// this runs, they must be deeper than the algorithm has yet reached.
///
/// This is called by [WidgetsBinding.beginFrame].
void buildDirtyElements() {
if (_dirtyElements.isEmpty)
return;
@ -1303,7 +1531,7 @@ class BuildOwner {
/// Complete the element build pass by unmounting any elements that are no
/// longer active.
///
/// This is called by beginFrame().
/// This is called by [WidgetsBinding.beginFrame].
///
/// In checked mode, this also verifies that each global key is used at most
/// once.
@ -1325,13 +1553,11 @@ class BuildOwner {
}
}
/// Cause the entire subtree rooted at the given [Element] to
/// be entirely rebuilt. This is used by development tools when
/// the application code has changed, to cause the widget tree to
/// pick up any changed implementations.
/// Cause the entire subtree rooted at the given [Element] to be entirely
/// rebuilt. This is used by development tools when the application code has
/// changed, to cause the widget tree to pick up any changed implementations.
///
/// This is expensive and should not be called except during
/// development.
/// This is expensive and should not be called except during development.
void reassemble(Element root) {
assert(root._parent == null);
assert(root.owner == this);
@ -1394,12 +1620,19 @@ abstract class Element implements BuildContext {
return result;
}
/// This is used to verify that Element objects move through life in an orderly fashion.
// This is used to verify that Element objects move through life in an
// orderly fashion.
_ElementLifecycle _debugLifecycleState = _ElementLifecycle.initial;
/// Calls the argument for each child. Must be overridden by subclasses that support having children.
/// Calls the argument for each child. Must be overridden by subclasses that
/// support having children.
void visitChildren(ElementVisitor visitor) { }
/// Calls the argument for each child that is relevant for semantics. By
/// default, defers to [visitChildren]. Classes like [Offstage] override this
/// to hide their children.
void visitChildrenForSemantics(ElementVisitor visitor) => visitChildren(visitor);
/// Wrapper around visitChildren for BuildContext.
@override
void visitChildElements(void visitor(Element element)) {
@ -1554,6 +1787,11 @@ abstract class Element implements BuildContext {
return null;
if (!Widget.canUpdate(element.widget, newWidget))
return null;
assert(() {
if (debugPrintGlobalKeyedWidgetLifecycle)
debugPrint('Attempting to take $element from ${element._parent ?? "inactive elements list"} to put in $this');
return true;
});
if (element._parent != null && !element._parent.detachChild(element))
return null;
assert(element._parent == null);
@ -1601,6 +1839,13 @@ abstract class Element implements BuildContext {
child._parent = null;
child.detachRenderObject();
owner._inactiveElements.add(child); // this eventually calls child.deactivate()
assert(() {
if (debugPrintGlobalKeyedWidgetLifecycle) {
if (child.widget.key is GlobalKey)
debugPrint('Deactivated $child (keyed child of $this)');
}
return true;
});
}
void _activateWithParent(Element parent, dynamic newSlot) {
@ -1623,6 +1868,11 @@ abstract class Element implements BuildContext {
/// instead of being unmounted (see [unmount]).
@mustCallSuper
void activate() {
assert(() {
if (debugPrintGlobalKeyedWidgetLifecycle)
debugPrint('Reactivating $this (child of $_parent).');
return true;
});
assert(_debugLifecycleState == _ElementLifecycle.inactive);
assert(widget != null);
assert(depth != null);
@ -2027,7 +2277,7 @@ abstract class ComponentElement extends BuildableElement {
built = _builder(this);
debugWidgetBuilderValue(widget, built);
} catch (e, stack) {
_debugReportException('building $_widget', e, stack);
_debugReportException('building $this', e, stack);
built = new ErrorWidget(e);
} finally {
// We delay marking the element as clean until after calling _builder so
@ -2039,7 +2289,7 @@ abstract class ComponentElement extends BuildableElement {
_child = updateChild(_child, built, slot);
assert(_child != null);
} catch (e, stack) {
_debugReportException('building $_widget', e, stack);
_debugReportException('building $this', e, stack);
built = new ErrorWidget(e);
_child = updateChild(null, built, slot);
}
@ -2247,6 +2497,7 @@ abstract class _ProxyElement extends ComponentElement {
void notifyClients(_ProxyWidget oldWidget);
}
/// Base class for instantiations of [ParentDataWidget] subclasses.
class ParentDataElement<T extends RenderObjectWidget> extends _ProxyElement {
ParentDataElement(ParentDataWidget<T> widget) : super(widget);
@ -2297,7 +2548,7 @@ class ParentDataElement<T extends RenderObjectWidget> extends _ProxyElement {
}
}
/// Base class for instantiations of [InheritedWidget] subclasses.
class InheritedElement extends _ProxyElement {
InheritedElement(InheritedWidget widget) : super(widget);
@ -2356,14 +2607,14 @@ class InheritedElement extends _ProxyElement {
}
}
/// Base class for instantiations of RenderObjectWidget subclasses
/// Base class for instantiations of [RenderObjectWidget] subclasses.
abstract class RenderObjectElement extends BuildableElement {
RenderObjectElement(RenderObjectWidget widget) : super(widget);
@override
RenderObjectWidget get widget => super.widget;
/// The underlying [RenderObject] for this element
/// The underlying [RenderObject] for this element.
@override
RenderObject get renderObject => _renderObject;
RenderObject _renderObject;
@ -2638,27 +2889,39 @@ abstract class RenderObjectElement extends BuildableElement {
}
}
/// Instantiation of RenderObjectWidgets at the root of the tree
/// Instantiation of RenderObjectWidgets at the root of the tree.
///
/// Only root elements may have their owner set explicitly. All other
/// elements inherit their owner from their parent.
abstract class RootRenderObjectElement extends RenderObjectElement {
RootRenderObjectElement(RenderObjectWidget widget): super(widget);
/// Set the owner of the element. The owner will be propagated to all the
/// descendants of this element.
///
/// The owner manages the dirty elements list.
///
/// The [WidgetsBinding] introduces the primary owner,
/// [WidgetsBinding.buildOwner], and assigns it to the widget tree in the call
/// to [runApp]. The binding is responsible for driving the build pipeline by
/// calling the build owner's [BuildOwner.buildDirtyElements] method. See
/// [WidgetsBinding.beginFrame].
void assignOwner(BuildOwner owner) {
_owner = owner;
}
@override
void mount(Element parent, dynamic newSlot) {
// Root elements should never have parents
// Root elements should never have parents.
assert(parent == null);
assert(newSlot == null);
super.mount(parent, newSlot);
}
}
/// Instantiation of RenderObjectWidgets that have no children
/// Instantiation of [RenderObjectWidget]s that have no children.
///
/// Such widgets are expected to inherit from [LeafRenderObjectWidget].
class LeafRenderObjectElement extends RenderObjectElement {
LeafRenderObjectElement(LeafRenderObjectWidget widget): super(widget);
@ -2678,7 +2941,13 @@ class LeafRenderObjectElement extends RenderObjectElement {
}
}
/// Instantiation of RenderObjectWidgets that have up to one child
/// Instantiation of [RenderObjectWidget]s that have up to one child.
///
/// The child is optional.
///
/// This element subclass can be used for RenderObjectWidgets whose
/// RenderObjects use the [RenderObjectWithChildMixin] mixin. Such widgets are
/// expected to inherit from [SingleChildRenderObjectWidget].
class SingleChildRenderObjectElement extends RenderObjectElement {
SingleChildRenderObjectElement(SingleChildRenderObjectWidget widget) : super(widget);
@ -2736,7 +3005,12 @@ class SingleChildRenderObjectElement extends RenderObjectElement {
}
}
/// Instantiation of RenderObjectWidgets that can have a list of children
/// Instantiation of [RenderObjectWidget]s that can have a list of children.
///
/// This element subclass can be used for RenderObjectWidgets whose
/// RenderObjects use the [ContainerRenderObjectMixin] mixin with a parent data
/// type that implements [ContainerParentDataMixin<RenderObject>]. Such widgets
/// are expected to inherit from [MultiChildRenderObjectWidget].
class MultiChildRenderObjectElement extends RenderObjectElement {
MultiChildRenderObjectElement(MultiChildRenderObjectWidget widget) : super(widget) {
assert(!debugChildrenHaveDuplicateKeys(widget, widget.children));
@ -2793,8 +3067,8 @@ class MultiChildRenderObjectElement extends RenderObjectElement {
super.mount(parent, newSlot);
_children = new List<Element>(widget.children.length);
Element previousChild;
for (int i = 0; i < _children.length; ++i) {
Element newChild = inflateWidget(widget.children[i], previousChild);
for (int i = 0; i < _children.length; i += 1) {
final Element newChild = inflateWidget(widget.children[i], previousChild);
_children[i] = newChild;
previousChild = newChild;
}

View File

@ -103,8 +103,8 @@ abstract class Route<T> {
/// route), then [isCurrent] will also be true.
///
/// If a later route is entirely opaque, then the route will be active but not
/// rendered. In particular, it's possible for a route to be active but for
/// stateful widgets within the route to not be instantiated.
/// rendered. It is even possible for the route to be active but for the stateful
/// widgets within the route to not be instatiated. See [ModalRoute.maintainState].
bool get isActive {
if (_navigator == null)
return false;
@ -118,7 +118,7 @@ class RouteSettings {
/// Creates data used to construct routes.
const RouteSettings({
this.name,
this.isInitialRoute: false
this.isInitialRoute: false,
});
/// The name of the route (e.g., "/settings").

View File

@ -2,9 +2,13 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'dart:collection';
import 'package:meta/meta.dart';
import 'package:flutter/rendering.dart';
import 'basic.dart';
import 'debug.dart';
import 'framework.dart';
/// A place in an [Overlay] that can contain a widget.
@ -31,6 +35,14 @@ import 'framework.dart';
/// [Draggable] removes the entry from the overlay to remove the drag avatar
/// from view.
///
/// By default, if there is an entirely [opaque] entry over this one, then this
/// one will not be included in the widget tree (in particular, stateful widgets
/// within the overlay entry will not be instantiated). To ensure that your
/// overlay entry is still built even if it is not visible, set [maintainState]
/// to true. This is more expensive, so should be done with care. In particular,
/// if widgets in an overlay entry with [maintainState] set to true repeatedly
/// call [setState], the user's battery will be drained unnecessarily.
///
/// See also:
///
/// * [Overlay]
@ -45,12 +57,16 @@ class OverlayEntry {
/// call [remove] on the overlay entry itself.
OverlayEntry({
@required this.builder,
bool opaque: false
}) : _opaque = opaque {
bool opaque: false,
bool maintainState: false,
}) : _opaque = opaque, _maintainState = maintainState {
assert(builder != null);
assert(opaque != null);
assert(maintainState != null);
}
/// This entry will include the widget built by this builder in the overlay at the entry's position.
/// This entry will include the widget built by this builder in the overlay at
/// the entry's position.
///
/// To cause this builder to be called again, call [markNeedsBuild] on this
/// overlay entry.
@ -58,8 +74,9 @@ class OverlayEntry {
/// Whether this entry occludes the entire overlay.
///
/// If an entry claims to be opaque, the overlay will skip building all the
/// entries below that entry for efficiency.
/// If an entry claims to be opaque, then, for efficiency, the overlay will
/// skip building entries below that entry unless they have [maintainState]
/// set.
bool get opaque => _opaque;
bool _opaque;
set opaque (bool value) {
@ -70,6 +87,31 @@ class OverlayEntry {
_overlay._didChangeEntryOpacity();
}
/// Whether this entry must be included in the tree even if there is a fully
/// [opaque] entry above it.
///
/// By default, if there is an entirely [opaque] entry over this one, then this
/// one will not be included in the widget tree (in particular, stateful widgets
/// within the overlay entry will not be instantiated). To ensure that your
/// overlay entry is still built even if it is not visible, set [maintainState]
/// to true. This is more expensive, so should be done with care. In particular,
/// if widgets in an overlay entry with [maintainState] set to true repeatedly
/// call [setState], the user's battery will be drained unnecessarily.
///
/// This is used by the [Navigator] and [Route] objects to ensure that routes
/// are kept around even when in the background, so that [Future]s promised
/// from subsequent routes will be handled properly when they complete.
bool get maintainState => _maintainState;
bool _maintainState;
set maintainState (bool value) {
assert(_maintainState != null);
if (_maintainState == value)
return;
_maintainState = value;
assert(_overlay != null);
_overlay._didChangeEntryOpacity();
}
OverlayState _overlay;
final GlobalKey<_OverlayEntryState> _key = new GlobalKey<_OverlayEntryState>();
@ -87,11 +129,13 @@ class OverlayEntry {
}
@override
String toString() => '$runtimeType@$hashCode(opaque: $opaque)';
String toString() => '$runtimeType@$hashCode(opaque: $opaque; maintainState: $maintainState)';
}
class _OverlayEntry extends StatefulWidget {
_OverlayEntry(OverlayEntry entry) : entry = entry, super(key: entry._key);
_OverlayEntry(OverlayEntry entry) : entry = entry, super(key: entry._key) {
assert(entry != null);
}
final OverlayEntry entry;
@ -101,7 +145,9 @@ class _OverlayEntry extends StatefulWidget {
class _OverlayEntryState extends State<_OverlayEntry> {
@override
Widget build(BuildContext context) => config.entry.builder(context);
Widget build(BuildContext context) {
return config.entry.builder(context);
}
void _markNeedsBuild() {
setState(() { /* the state that changed is in the builder */ });
@ -227,7 +273,8 @@ class OverlayState extends State<Overlay> {
setState(() { /* entry was removed */ });
}
/// (DEBUG ONLY) Check whether a given entry is visible (i.e., not behind an opaque entry).
/// (DEBUG ONLY) Check whether a given entry is visible (i.e., not behind an
/// opaque entry).
///
/// This is an O(N) algorithm, and should not be necessary except for debug
/// asserts. To avoid people depending on it, this function is implemented
@ -259,16 +306,26 @@ class OverlayState extends State<Overlay> {
@override
Widget build(BuildContext context) {
List<Widget> backwardsChildren = <Widget>[];
for (int i = _entries.length - 1; i >= 0; --i) {
// These lists are filled backwards. For the offstage children that
// does not matter since they aren't rendered, but for the onstage
// children we reverse the list below before adding it to the tree.
final List<Widget> onstageChildren = <Widget>[];
final List<Widget> offstageChildren = <Widget>[];
bool onstage = true;
for (int i = _entries.length - 1; i >= 0; i -= 1) {
OverlayEntry entry = _entries[i];
backwardsChildren.add(new _OverlayEntry(entry));
if (entry.opaque)
break;
if (onstage) {
onstageChildren.add(new _OverlayEntry(entry));
if (entry.opaque)
onstage = false;
} else if (entry.maintainState) {
offstageChildren.add(new _OverlayEntry(entry));
}
}
return new Stack(children: backwardsChildren.reversed.toList(growable: false));
return new _Theatre(
onstage: new Stack(children: onstageChildren.reversed.toList(growable: false)),
offstage: offstageChildren,
);
}
@override
@ -277,3 +334,214 @@ class OverlayState extends State<Overlay> {
description.add('entries: $_entries');
}
}
/// A widget that has one [onstage] child which is visible, and one or more
/// [offstage] widgets which are kept alive, and are built, but are not laid out
/// or painted.
///
/// The onstage widget must be a Stack.
///
/// For convenience, it is legal to use [Positioned] widgets around the offstage
/// widgets.
class _Theatre extends RenderObjectWidget {
_Theatre({
this.onstage,
this.offstage,
}) {
assert(offstage != null);
assert(!offstage.any((Widget child) => child == null));
}
final Stack onstage;
final List<Widget> offstage;
@override
_TheatreElement createElement() => new _TheatreElement(this);
@override
_RenderTheatre createRenderObject(BuildContext context) => new _RenderTheatre();
}
class _TheatreElement extends RenderObjectElement {
_TheatreElement(_Theatre widget) : super(widget) {
assert(!debugChildrenHaveDuplicateKeys(widget, widget.offstage));
}
@override
_Theatre get widget => super.widget;
@override
_RenderTheatre get renderObject => super.renderObject;
Element _onstage;
static final Object _onstageSlot = new Object();
List<Element> _offstage;
final Set<Element> _detachedOffstageChildren = new HashSet<Element>();
@override
void insertChildRenderObject(RenderBox child, dynamic slot) {
if (slot == _onstageSlot) {
assert(child is RenderStack);
renderObject.child = child;
} else {
assert(slot == null || slot is Element);
renderObject.insert(child, after: slot?.renderObject);
}
}
@override
void moveChildRenderObject(RenderBox child, dynamic slot) {
if (slot == _onstageSlot) {
renderObject.remove(child);
assert(child is RenderStack);
renderObject.child = child;
} else {
assert(slot == null || slot is Element);
if (renderObject.child == child) {
renderObject.child = null;
renderObject.insert(child, after: slot?.renderObject);
} else {
renderObject.move(child, after: slot?.renderObject);
}
}
}
@override
void removeChildRenderObject(RenderBox child) {
if (renderObject.child == child) {
renderObject.child = null;
} else {
renderObject.remove(child);
}
}
@override
void visitChildren(ElementVisitor visitor) {
if (_onstage != null)
visitor(_onstage);
for (Element child in _offstage) {
if (!_detachedOffstageChildren.contains(child))
visitor(child);
}
}
@override
void visitChildrenForSemantics(ElementVisitor visitor) {
if (_onstage != null)
visitor(_onstage);
}
@override
bool detachChild(Element child) {
if (child == _onstage) {
_onstage = null;
} else {
_detachedOffstageChildren.add(child);
}
deactivateChild(child);
return true;
}
@override
void mount(Element parent, dynamic newSlot) {
super.mount(parent, newSlot);
_onstage = updateChild(_onstage, widget.onstage, _onstageSlot);
_offstage = new List<Element>(widget.offstage.length);
Element previousChild;
for (int i = 0; i < _offstage.length; i += 1) {
final Element newChild = inflateWidget(widget.offstage[i], previousChild);
_offstage[i] = newChild;
previousChild = newChild;
}
}
@override
void update(_Theatre newWidget) {
super.update(newWidget);
assert(widget == newWidget);
_onstage = updateChild(_onstage, widget.onstage, _onstageSlot);
_offstage = updateChildren(_offstage, widget.offstage, detachedChildren: _detachedOffstageChildren);
_detachedOffstageChildren.clear();
}
}
// A render object which lays out and paints one subtree while keeping a list
// of other subtrees alive but not laid out or painted (the "zombie" children).
//
// The subtree that is laid out and painted must be a [RenderStack].
//
// This class uses [StackParentData] objects for its parent data so that the
// children of its primary subtree's stack can be moved to this object's list
// of zombie children without changing their parent data objects.
class _RenderTheatre extends RenderBox
with RenderObjectWithChildMixin<RenderStack>, RenderProxyBoxMixin,
ContainerRenderObjectMixin<RenderBox, StackParentData> {
@override
void setupParentData(RenderObject child) {
if (child.parentData is! StackParentData)
child.parentData = new StackParentData();
}
// Because both RenderObjectWithChildMixin and ContainerRenderObjectMixin
// define redepthChildren, visitChildren and debugDescribeChildren and don't
// call super, we have to define them again here to make sure the work of both
// is done.
//
// We chose to put ContainerRenderObjectMixin last in the inheritance chain so
// that we can call super to hit its more complex definitions of
// redepthChildren and visitChildren, and then duplicate the more trivial
// definition from RenderObjectWithChildMixin inline in our version here.
//
// This code duplication is suboptimal.
// TODO(ianh): Replace this with a better solution once https://github.com/dart-lang/sdk/issues/27100 is fixed
//
// For debugDescribeChildren we just roll our own because otherwise the line
// drawings won't really work as well.
@override
void redepthChildren() {
if (child != null)
redepthChild(child);
super.redepthChildren();
}
@override
void visitChildren(RenderObjectVisitor visitor) {
if (child != null)
visitor(child);
super.visitChildren(visitor);
}
@override
String debugDescribeChildren(String prefix) {
String result = '';
if (child != null)
result += '$prefix \u2502\n${child.toStringDeep('$prefix \u251C\u2500onstage: ', '$prefix \u254E')}';
if (firstChild != null) {
RenderBox child = firstChild;
int count = 1;
while (child != lastChild) {
result += '${child.toStringDeep("$prefix \u254E\u254Coffstage $count: ", "$prefix \u254E")}';
count += 1;
final StackParentData childParentData = child.parentData;
child = childParentData.nextSibling;
}
if (child != null) {
assert(child == lastChild);
result += '${child.toStringDeep("$prefix \u2514\u254Coffstage $count: ", "$prefix ")}';
}
} else {
result += '$prefix \u2514\u254Cno offstage children';
}
return result;
}
@override
void visitChildrenForSemantics(RenderObjectVisitor visitor) {
if (child != null)
visitor(child);
}
}

View File

@ -20,7 +20,7 @@ const Color _kTransparent = const Color(0x00000000);
/// A route that displays widgets in the [Navigator]'s [Overlay].
abstract class OverlayRoute<T> extends Route<T> {
/// Subclasses should override this getter to return the builders for the overlay.
List<WidgetBuilder> get builders;
Iterable<OverlayEntry> createOverlayEntries();
/// The entries this route has placed in the overlay.
@override
@ -30,8 +30,7 @@ abstract class OverlayRoute<T> extends Route<T> {
@override
void install(OverlayEntry insertionPoint) {
assert(_overlayEntries.isEmpty);
for (WidgetBuilder builder in builders)
_overlayEntries.add(new OverlayEntry(builder: builder));
_overlayEntries.addAll(createOverlayEntries());
navigator.overlay?.insertAll(_overlayEntries, above: insertionPoint);
}
@ -415,7 +414,7 @@ class _ModalScopeState extends State<_ModalScope> {
Widget build(BuildContext context) {
return new Focus(
key: config.route.focusKey,
child: new OffStage(
child: new Offstage(
offstage: config.route.offstage,
child: new IgnorePointer(
ignoring: config.route.animation?.status == AnimationStatus.reverse,
@ -577,6 +576,13 @@ abstract class ModalRoute<T> extends TransitionRoute<T> with LocalHistoryRoute<T
/// be transparent.
Color get barrierColor;
/// Whether the route should remain in memory when it is inactive. If this is
/// true, then the route is maintained, so that any futures it is holding from
/// the next route will properly resolve when the next route pops. If this is
/// not necessary, this can be set to false to allow the framework to entirely
/// discard the route's widget hierarchy when it is not visible.
bool get maintainState;
// The API for _ModalScope and HeroController
@ -654,10 +660,10 @@ abstract class ModalRoute<T> extends TransitionRoute<T> with LocalHistoryRoute<T
}
@override
List<WidgetBuilder> get builders => <WidgetBuilder>[
_buildModalBarrier,
_buildModalScope
];
Iterable<OverlayEntry> createOverlayEntries() sync* {
yield new OverlayEntry(builder: _buildModalBarrier);
yield new OverlayEntry(builder: _buildModalScope, maintainState: maintainState);
}
@override
String toString() => '$runtimeType($settings, animation: $_animation)';
@ -671,6 +677,9 @@ abstract class PopupRoute<T> extends ModalRoute<T> {
@override
bool get opaque => false;
@override
bool get maintainState => true;
@override
void didChangeNext(Route<dynamic> nextRoute) {
assert(nextRoute is! PageRoute<dynamic>);

View File

@ -6,7 +6,7 @@ import 'package:flutter_test/flutter_test.dart';
import 'package:flutter/material.dart';
void main() {
testWidgets('Navigator.push works within a PopupMenuButton ', (WidgetTester tester) async {
testWidgets('Navigator.push works within a PopupMenuButton', (WidgetTester tester) async {
await tester.pumpWidget(
new MaterialApp(
routes: <String, WidgetBuilder> {
@ -46,9 +46,9 @@ void main() {
expect(find.text('Next'), findsNothing);
await tester.tap(find.text('One'));
await tester.pump();
await tester.pump(const Duration(seconds: 1)); // finish the menu animation
await tester.pump(); // return the future
await tester.pump(); // start the navigation
await tester.pump(const Duration(seconds: 1)); // end the navigation
expect(find.text('One'), findsNothing);
expect(find.text('Next'), findsOneWidget);

View File

@ -15,7 +15,7 @@ void main() {
// viewport incoming constraints are tight 800x600
// viewport is vertical by default
RenderBox root = new RenderViewport(
child: new RenderOffStage(
child: new RenderOffstage(
child: new RenderCustomPaint(
painter: new TestCallbackPainter(
onPaint: () { painted = true; }

View File

@ -59,20 +59,20 @@ void main() {
// the initial setup.
expect(find.byKey(firstKey), isOnStage);
expect(find.byKey(firstKey), isOnstage);
expect(find.byKey(firstKey), isInCard);
expect(find.byKey(secondKey), findsNothing);
await tester.tap(find.text('two'));
await tester.pump(); // begin navigation
// at this stage, the second route is off-stage, so that we can form the
// at this stage, the second route is offstage, so that we can form the
// hero party.
expect(find.byKey(firstKey), isOnStage);
expect(find.byKey(firstKey), isOnstage);
expect(find.byKey(firstKey), isInCard);
expect(find.byKey(secondKey), isOffStage);
expect(find.byKey(secondKey), isInCard);
expect(find.byKey(secondKey, skipOffstage: false), isOffstage);
expect(find.byKey(secondKey, skipOffstage: false), isInCard);
await tester.pump();
@ -80,7 +80,7 @@ void main() {
// seeing them at t=16ms. The original page no longer contains the hero.
expect(find.byKey(firstKey), findsNothing);
expect(find.byKey(secondKey), isOnStage);
expect(find.byKey(secondKey), isOnstage);
expect(find.byKey(secondKey), isNotInCard);
await tester.pump();
@ -88,16 +88,17 @@ void main() {
// t=32ms for the journey. Surely they are still at it.
expect(find.byKey(firstKey), findsNothing);
expect(find.byKey(secondKey), isOnStage);
expect(find.byKey(secondKey), isOnstage);
expect(find.byKey(secondKey), isNotInCard);
await tester.pump(new Duration(seconds: 1));
// t=1.032s for the journey. The journey has ended (it ends this frame, in
// fact). The hero should now be in the new page, on-stage.
// fact). The hero should now be in the new page, onstage. The original
// widget will be back as well now (though not visible).
expect(find.byKey(firstKey), findsNothing);
expect(find.byKey(secondKey), isOnStage);
expect(find.byKey(secondKey), isOnstage);
expect(find.byKey(secondKey), isInCard);
await tester.pump();
@ -105,7 +106,7 @@ void main() {
// Should not change anything.
expect(find.byKey(firstKey), findsNothing);
expect(find.byKey(secondKey), isOnStage);
expect(find.byKey(secondKey), isOnstage);
expect(find.byKey(secondKey), isInCard);
// Now move on to view 3
@ -113,13 +114,13 @@ void main() {
await tester.tap(find.text('three'));
await tester.pump(); // begin navigation
// at this stage, the second route is off-stage, so that we can form the
// at this stage, the second route is offstage, so that we can form the
// hero party.
expect(find.byKey(secondKey), isOnStage);
expect(find.byKey(secondKey), isOnstage);
expect(find.byKey(secondKey), isInCard);
expect(find.byKey(thirdKey), isOffStage);
expect(find.byKey(thirdKey), isInCard);
expect(find.byKey(thirdKey, skipOffstage: false), isOffstage);
expect(find.byKey(thirdKey, skipOffstage: false), isInCard);
await tester.pump();
@ -127,7 +128,7 @@ void main() {
// seeing them at t=16ms. The original page no longer contains the hero.
expect(find.byKey(secondKey), findsNothing);
expect(find.byKey(thirdKey), isOnStage);
expect(find.byKey(thirdKey), isOnstage);
expect(find.byKey(thirdKey), isNotInCard);
await tester.pump();
@ -135,16 +136,16 @@ void main() {
// t=32ms for the journey. Surely they are still at it.
expect(find.byKey(secondKey), findsNothing);
expect(find.byKey(thirdKey), isOnStage);
expect(find.byKey(thirdKey), isOnstage);
expect(find.byKey(thirdKey), isNotInCard);
await tester.pump(new Duration(seconds: 1));
// t=1.032s for the journey. The journey has ended (it ends this frame, in
// fact). The hero should now be in the new page, on-stage.
// fact). The hero should now be in the new page, onstage.
expect(find.byKey(secondKey), findsNothing);
expect(find.byKey(thirdKey), isOnStage);
expect(find.byKey(thirdKey), isOnstage);
expect(find.byKey(thirdKey), isInCard);
await tester.pump();
@ -152,7 +153,7 @@ void main() {
// Should not change anything.
expect(find.byKey(secondKey), findsNothing);
expect(find.byKey(thirdKey), isOnStage);
expect(find.byKey(thirdKey), isOnstage);
expect(find.byKey(thirdKey), isInCard);
});
@ -187,8 +188,8 @@ void main() {
final Duration duration = const Duration(milliseconds: 300);
final Curve curve = Curves.fastOutSlowIn;
final double initialHeight = tester.getSize(find.byKey(firstKey)).height;
final double finalHeight = tester.getSize(find.byKey(secondKey)).height;
final double initialHeight = tester.getSize(find.byKey(firstKey, skipOffstage: false)).height;
final double finalHeight = tester.getSize(find.byKey(secondKey, skipOffstage: false)).height;
final double deltaHeight = finalHeight - initialHeight;
final double epsilon = 0.001;
@ -267,18 +268,18 @@ void main() {
navigator.pushNamed('/next');
expect(log, isEmpty);
await tester.tap(find.text('foo'));
await tester.tap(find.text('foo', skipOffstage: false));
expect(log, isEmpty);
await tester.pump(new Duration(milliseconds: 10));
await tester.tap(find.text('foo'));
await tester.tap(find.text('foo', skipOffstage: false));
expect(log, isEmpty);
await tester.tap(find.text('bar'));
await tester.tap(find.text('bar', skipOffstage: false));
expect(log, isEmpty);
await tester.pump(new Duration(milliseconds: 10));
expect(find.text('foo'), findsNothing);
await tester.tap(find.text('bar'));
await tester.tap(find.text('bar', skipOffstage: false));
expect(log, isEmpty);
await tester.pump(new Duration(seconds: 1));

View File

@ -69,33 +69,51 @@ class ThirdWidget extends StatelessWidget {
void main() {
testWidgets('Can navigator navigate to and from a stateful widget', (WidgetTester tester) async {
final Map<String, WidgetBuilder> routes = <String, WidgetBuilder>{
'/': (BuildContext context) => new FirstWidget(),
'/second': (BuildContext context) => new SecondWidget(),
'/': (BuildContext context) => new FirstWidget(), // X
'/second': (BuildContext context) => new SecondWidget(), // Y
};
await tester.pumpWidget(new MaterialApp(routes: routes));
expect(find.text('X'), findsOneWidget);
expect(find.text('Y'), findsNothing);
expect(find.text('Y', skipOffstage: false), findsNothing);
await tester.tap(find.text('X'));
await tester.pump(const Duration(milliseconds: 10));
await tester.pump();
expect(find.text('X'), findsOneWidget);
expect(find.text('Y', skipOffstage: false), isOffstage);
await tester.pump(const Duration(milliseconds: 10));
expect(find.text('X'), findsOneWidget);
expect(find.text('Y'), findsOneWidget);
await tester.pump(const Duration(milliseconds: 10));
expect(find.text('X'), findsOneWidget);
expect(find.text('Y'), findsOneWidget);
await tester.pump(const Duration(milliseconds: 10));
expect(find.text('X'), findsOneWidget);
expect(find.text('Y'), findsOneWidget);
await tester.pump(const Duration(seconds: 1));
expect(find.text('X'), findsNothing);
expect(find.text('X', skipOffstage: false), findsOneWidget);
expect(find.text('Y'), findsOneWidget);
await tester.tap(find.text('Y'));
await tester.pump(const Duration(milliseconds: 10));
await tester.pump(const Duration(milliseconds: 10));
await tester.pump(const Duration(milliseconds: 10));
await tester.pump(const Duration(seconds: 1));
expect(find.text('X'), findsNothing);
expect(find.text('Y'), findsOneWidget);
await tester.pump();
expect(find.text('X'), findsOneWidget);
expect(find.text('Y'), findsNothing);
expect(find.text('Y'), findsOneWidget);
await tester.pump(const Duration(milliseconds: 10));
expect(find.text('X'), findsOneWidget);
expect(find.text('Y'), findsOneWidget);
await tester.pump(const Duration(seconds: 1));
expect(find.text('X'), findsOneWidget);
expect(find.text('Y', skipOffstage: false), findsNothing);
});
testWidgets('Navigator.of fails gracefully when not found in context', (WidgetTester tester) async {

View File

@ -4,6 +4,7 @@
import 'package:flutter_test/flutter_test.dart' hide TypeMatcher;
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
class TestTransition extends AnimatedWidget {
TestTransition({
@ -26,7 +27,7 @@ class TestTransition extends AnimatedWidget {
}
class TestRoute<T> extends PageRoute<T> {
TestRoute({ this.child, RouteSettings settings}) : super(settings: settings);
TestRoute({ this.child, RouteSettings settings }) : super(settings: settings);
final Widget child;
@ -36,6 +37,9 @@ class TestRoute<T> extends PageRoute<T> {
@override
Color get barrierColor => null;
@override
bool get maintainState => false;
@override
Widget buildPage(BuildContext context, Animation<double> animation, Animation<double> forwardAnimation) {
return child;
@ -50,21 +54,21 @@ void main() {
GlobalKey insideKey = new GlobalKey();
String state() {
String state({ bool skipOffstage: true }) {
String result = '';
if (tester.any(find.text('A')))
if (tester.any(find.text('A', skipOffstage: skipOffstage)))
result += 'A';
if (tester.any(find.text('B')))
if (tester.any(find.text('B', skipOffstage: skipOffstage)))
result += 'B';
if (tester.any(find.text('C')))
if (tester.any(find.text('C', skipOffstage: skipOffstage)))
result += 'C';
if (tester.any(find.text('D')))
if (tester.any(find.text('D', skipOffstage: skipOffstage)))
result += 'D';
if (tester.any(find.text('E')))
if (tester.any(find.text('E', skipOffstage: skipOffstage)))
result += 'E';
if (tester.any(find.text('F')))
if (tester.any(find.text('F', skipOffstage: skipOffstage)))
result += 'F';
if (tester.any(find.text('G')))
if (tester.any(find.text('G', skipOffstage: skipOffstage)))
result += 'G';
return result;
}
@ -112,7 +116,8 @@ void main() {
navigator.pushNamed('/2');
expect(state(), equals('BC')); // transition 1->2 is not yet built
await tester.pump();
expect(state(), equals('BCE')); // transition 1->2 is at 0.0
expect(state(), equals('BC')); // transition 1->2 is at 0.0
expect(state(skipOffstage: false), equals('BCE')); // E is offstage
await tester.pump(kFourTenthsOfTheTransitionDuration);
expect(state(), equals('BCE')); // transition 1->2 is at 0.4
@ -122,7 +127,7 @@ void main() {
await tester.pump(kFourTenthsOfTheTransitionDuration);
expect(state(), equals('E')); // transition 1->2 is at 1.0
expect(state(skipOffstage: false), equals('E')); // B and C are gone, the route is inactive with maintainState=false
navigator.pop();
expect(state(), equals('E')); // transition 1<-2 is at 1.0, just reversed
@ -135,10 +140,12 @@ void main() {
navigator.pushNamed('/3');
expect(state(), equals('BDE')); // transition 1<-2 is at 0.6
await tester.pump();
expect(state(), equals('BDEF')); // transition 1<-2 is at 0.6, 1->3 is at 0.0
expect(state(), equals('BDE')); // transition 1<-2 is at 0.6, 1->3 is at 0.0
expect(state(skipOffstage: false), equals('BDEF')); // F is offstage since we're at 0.0
await tester.pump(kFourTenthsOfTheTransitionDuration);
expect(state(), equals('BCEF')); // transition 1<-2 is at 0.2, 1->3 is at 0.4
expect(state(skipOffstage: false), equals('BCEF')); // nothing secret going on here
await tester.pump(kFourTenthsOfTheTransitionDuration);
expect(state(), equals('BDF')); // transition 1<-2 is done, 1->3 is at 0.8
@ -157,7 +164,8 @@ void main() {
navigator.pushNamed('/4');
expect(state(), equals('BCF')); // transition 1<-3 is at 0.2, 1->4 is not yet built
await tester.pump();
expect(state(), equals('BCFG')); // transition 1<-3 is at 0.2, 1->4 is at 0.0
expect(state(), equals('BCF')); // transition 1<-3 is at 0.2, 1->4 is at 0.0
expect(state(skipOffstage: false), equals('BCFG')); // G is offstage
await tester.pump(kFourTenthsOfTheTransitionDuration);
expect(state(), equals('BCG')); // transition 1<-3 is done, 1->4 is at 0.4
@ -167,6 +175,7 @@ void main() {
await tester.pump(kFourTenthsOfTheTransitionDuration);
expect(state(), equals('G')); // transition 1->4 is done
expect(state(skipOffstage: false), equals('G')); // route 1 is not around any more
});
}

View File

@ -7,7 +7,9 @@ import 'package:flutter/material.dart';
class TestOverlayRoute extends OverlayRoute<Null> {
@override
List<WidgetBuilder> get builders => <WidgetBuilder>[ _build ];
Iterable<OverlayEntry> createOverlayEntries() sync* {
yield new OverlayEntry(builder: _build);
}
Widget _build(BuildContext context) => new Text('Overlay');
}
@ -22,7 +24,7 @@ void main() {
await tester.pumpWidget(new MaterialApp(routes: routes));
expect(find.text('Home'), isOnStage);
expect(find.text('Home'), isOnstage);
expect(find.text('Settings'), findsNothing);
expect(find.text('Overlay'), findsNothing);
@ -32,20 +34,20 @@ void main() {
await tester.pump();
expect(find.text('Home'), isOnStage);
expect(find.text('Settings'), isOffStage);
expect(find.text('Home'), isOnstage);
expect(find.text('Settings', skipOffstage: false), isOffstage);
expect(find.text('Overlay'), findsNothing);
await tester.pump(const Duration(milliseconds: 16));
expect(find.text('Home'), isOnStage);
expect(find.text('Settings'), isOnStage);
expect(find.text('Home'), isOnstage);
expect(find.text('Settings'), isOnstage);
expect(find.text('Overlay'), findsNothing);
await tester.pump(const Duration(seconds: 1));
expect(find.text('Home'), findsNothing);
expect(find.text('Settings'), isOnStage);
expect(find.text('Settings'), isOnstage);
expect(find.text('Overlay'), findsNothing);
Navigator.push(containerKey2.currentContext, new TestOverlayRoute());
@ -53,40 +55,40 @@ void main() {
await tester.pump();
expect(find.text('Home'), findsNothing);
expect(find.text('Settings'), isOnStage);
expect(find.text('Overlay'), isOnStage);
expect(find.text('Settings'), isOnstage);
expect(find.text('Overlay'), isOnstage);
await tester.pump(const Duration(seconds: 1));
expect(find.text('Home'), findsNothing);
expect(find.text('Settings'), isOnStage);
expect(find.text('Overlay'), isOnStage);
expect(find.text('Settings'), isOnstage);
expect(find.text('Overlay'), isOnstage);
expect(Navigator.canPop(containerKey2.currentContext), isTrue);
Navigator.pop(containerKey2.currentContext);
await tester.pump();
expect(find.text('Home'), findsNothing);
expect(find.text('Settings'), isOnStage);
expect(find.text('Settings'), isOnstage);
expect(find.text('Overlay'), findsNothing);
await tester.pump(const Duration(seconds: 1));
expect(find.text('Home'), findsNothing);
expect(find.text('Settings'), isOnStage);
expect(find.text('Settings'), isOnstage);
expect(find.text('Overlay'), findsNothing);
expect(Navigator.canPop(containerKey2.currentContext), isTrue);
Navigator.pop(containerKey2.currentContext);
await tester.pump();
expect(find.text('Home'), isOnStage);
expect(find.text('Settings'), isOnStage);
expect(find.text('Home'), isOnstage);
expect(find.text('Settings'), isOnstage);
expect(find.text('Overlay'), findsNothing);
await tester.pump(const Duration(seconds: 1));
expect(find.text('Home'), isOnStage);
expect(find.text('Home'), isOnstage);
expect(find.text('Settings'), findsNothing);
expect(find.text('Overlay'), findsNothing);

View File

@ -2,10 +2,13 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:meta/meta.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:flutter/material.dart';
class ThePositiveNumbers extends StatelessWidget {
ThePositiveNumbers({ @required this.from });
final int from;
@override
Widget build(BuildContext context) {
return new ScrollableLazyList(
@ -13,104 +16,110 @@ class ThePositiveNumbers extends StatelessWidget {
itemBuilder: (BuildContext context, int start, int count) {
List<Widget> result = new List<Widget>();
for (int index = start; index < start + count; index += 1)
result.add(new Text('$index', key: new ValueKey<int>(index)));
result.add(new Text('${index + from}', key: new ValueKey<int>(index)));
return result;
}
);
}
}
Future<Null> performTest(WidgetTester tester, bool maintainState) async {
GlobalKey<NavigatorState> navigatorKey = new GlobalKey<NavigatorState>();
await tester.pumpWidget(new Navigator(
key: navigatorKey,
onGenerateRoute: (RouteSettings settings) {
if (settings.name == '/') {
return new MaterialPageRoute<Null>(
settings: settings,
builder: (_) => new Container(child: new ThePositiveNumbers(from: 0)),
maintainState: maintainState,
);
} else if (settings.name == '/second') {
return new MaterialPageRoute<Null>(
settings: settings,
builder: (_) => new Container(child: new ThePositiveNumbers(from: 10000)),
maintainState: maintainState,
);
}
return null;
}
));
// we're 600 pixels high, each item is 100 pixels high, scroll position is
// zero, so we should have exactly 6 items, 0..5.
expect(find.text('0'), findsOneWidget);
expect(find.text('1'), findsOneWidget);
expect(find.text('2'), findsOneWidget);
expect(find.text('3'), findsOneWidget);
expect(find.text('4'), findsOneWidget);
expect(find.text('5'), findsOneWidget);
expect(find.text('6'), findsNothing);
expect(find.text('10'), findsNothing);
expect(find.text('100'), findsNothing);
ScrollableState targetState = tester.state(find.byType(Scrollable));
targetState.scrollTo(1000.0);
await tester.pump(new Duration(seconds: 1));
// we're 600 pixels high, each item is 100 pixels high, scroll position is
// 1000, so we should have exactly 6 items, 10..15.
expect(find.text('0'), findsNothing);
expect(find.text('8'), findsNothing);
expect(find.text('9'), findsNothing);
expect(find.text('10'), findsOneWidget);
expect(find.text('11'), findsOneWidget);
expect(find.text('12'), findsOneWidget);
expect(find.text('13'), findsOneWidget);
expect(find.text('14'), findsOneWidget);
expect(find.text('15'), findsOneWidget);
expect(find.text('16'), findsNothing);
expect(find.text('100'), findsNothing);
navigatorKey.currentState.pushNamed('/second');
await tester.pump(); // navigating always takes two frames, one to start...
await tester.pump(new Duration(seconds: 1)); // ...and one to end the transition
// the second list is now visible, starting at 10000
expect(find.text('10000'), findsOneWidget);
expect(find.text('10001'), findsOneWidget);
expect(find.text('10002'), findsOneWidget);
expect(find.text('10003'), findsOneWidget);
expect(find.text('10004'), findsOneWidget);
expect(find.text('10005'), findsOneWidget);
expect(find.text('10006'), findsNothing);
expect(find.text('10010'), findsNothing);
expect(find.text('10100'), findsNothing);
navigatorKey.currentState.pop();
await tester.pump(); // again, navigating always takes two frames
// Ensure we don't clamp the scroll offset even during the navigation.
// https://github.com/flutter/flutter/issues/4883
LazyListViewport viewport = tester.firstWidget(find.byType(LazyListViewport));
expect(viewport.scrollOffset, equals(1000.0));
await tester.pump(new Duration(seconds: 1));
// we're 600 pixels high, each item is 100 pixels high, scroll position is
// 1000, so we should have exactly 6 items, 10..15.
expect(find.text('0'), findsNothing);
expect(find.text('8'), findsNothing);
expect(find.text('9'), findsNothing);
expect(find.text('10'), findsOneWidget);
expect(find.text('11'), findsOneWidget);
expect(find.text('12'), findsOneWidget);
expect(find.text('13'), findsOneWidget);
expect(find.text('14'), findsOneWidget);
expect(find.text('15'), findsOneWidget);
expect(find.text('16'), findsNothing);
expect(find.text('100'), findsNothing);
}
void main() {
testWidgets('whether we remember our scroll position', (WidgetTester tester) async {
GlobalKey<NavigatorState> navigatorKey = new GlobalKey<NavigatorState>();
await tester.pumpWidget(new Navigator(
key: navigatorKey,
onGenerateRoute: (RouteSettings settings) {
if (settings.name == '/') {
return new MaterialPageRoute<Null>(
settings: settings,
builder: (_) => new Container(child: new ThePositiveNumbers())
);
} else if (settings.name == '/second') {
return new MaterialPageRoute<Null>(
settings: settings,
builder: (_) => new Container(child: new ThePositiveNumbers())
);
}
return null;
}
));
// we're 600 pixels high, each item is 100 pixels high, scroll position is
// zero, so we should have exactly 6 items, 0..5.
expect(find.text('0'), findsOneWidget);
expect(find.text('1'), findsOneWidget);
expect(find.text('2'), findsOneWidget);
expect(find.text('3'), findsOneWidget);
expect(find.text('4'), findsOneWidget);
expect(find.text('5'), findsOneWidget);
expect(find.text('6'), findsNothing);
expect(find.text('10'), findsNothing);
expect(find.text('100'), findsNothing);
ScrollableState targetState = tester.state(find.byType(Scrollable));
targetState.scrollTo(1000.0);
await tester.pump(new Duration(seconds: 1));
// we're 600 pixels high, each item is 100 pixels high, scroll position is
// 1000, so we should have exactly 6 items, 10..15.
expect(find.text('0'), findsNothing);
expect(find.text('8'), findsNothing);
expect(find.text('9'), findsNothing);
expect(find.text('10'), findsOneWidget);
expect(find.text('11'), findsOneWidget);
expect(find.text('12'), findsOneWidget);
expect(find.text('13'), findsOneWidget);
expect(find.text('14'), findsOneWidget);
expect(find.text('15'), findsOneWidget);
expect(find.text('16'), findsNothing);
expect(find.text('100'), findsNothing);
navigatorKey.currentState.pushNamed('/second');
await tester.pump(); // navigating always takes two frames
await tester.pump(new Duration(seconds: 1));
// same as the first list again
expect(find.text('0'), findsOneWidget);
expect(find.text('1'), findsOneWidget);
expect(find.text('2'), findsOneWidget);
expect(find.text('3'), findsOneWidget);
expect(find.text('4'), findsOneWidget);
expect(find.text('5'), findsOneWidget);
expect(find.text('6'), findsNothing);
expect(find.text('10'), findsNothing);
expect(find.text('100'), findsNothing);
navigatorKey.currentState.pop();
await tester.pump(); // navigating always takes two frames
// Ensure we don't clamp the scroll offset even during the navigation.
// https://github.com/flutter/flutter/issues/4883
LazyListViewport viewport = tester.firstWidget(find.byType(LazyListViewport));
expect(viewport.scrollOffset, equals(1000.0));
await tester.pump(new Duration(seconds: 1));
// we're 600 pixels high, each item is 100 pixels high, scroll position is
// 1000, so we should have exactly 6 items, 10..15.
expect(find.text('0'), findsNothing);
expect(find.text('8'), findsNothing);
expect(find.text('9'), findsNothing);
expect(find.text('10'), findsOneWidget);
expect(find.text('11'), findsOneWidget);
expect(find.text('12'), findsOneWidget);
expect(find.text('13'), findsOneWidget);
expect(find.text('14'), findsOneWidget);
expect(find.text('15'), findsOneWidget);
expect(find.text('16'), findsNothing);
expect(find.text('100'), findsNothing);
await performTest(tester, true);
await performTest(tester, false);
});
}

View File

@ -327,4 +327,45 @@ void main() {
await tester.pump();
expect(log, isEmpty);
});
testWidgets('Reparenting with multiple moves', (WidgetTester tester) async {
final GlobalKey key1 = new GlobalKey();
final GlobalKey key2 = new GlobalKey();
final GlobalKey key3 = new GlobalKey();
await tester.pumpWidget(
new Row(
children: <Widget>[
new StateMarker(
key: key1,
child: new StateMarker(
key: key2,
child: new StateMarker(
key: key3,
child: new StateMarker(child: new Container(width: 100.0))
)
)
)
]
)
);
await tester.pumpWidget(
new Row(
children: <Widget>[
new StateMarker(
key: key2,
child: new StateMarker(child: new Container(width: 100.0))
),
new StateMarker(
key: key1,
child: new StateMarker(
key: key3,
child: new StateMarker(child: new Container(width: 100.0))
)
),
]
)
);
});
}

View File

@ -2,6 +2,8 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:meta/meta.dart';
import 'package:flutter/widgets.dart';
/// Provides an iterable that efficiently returns all the elements
@ -15,13 +17,18 @@ import 'package:flutter/widgets.dart';
/// The same applies to any iterable obtained indirectly through this
/// one, for example the results of calling `where` on this iterable
/// are also cached.
Iterable<Element> collectAllElementsFrom(Element rootElement) {
return new CachingIterable<Element>(new _DepthFirstChildIterator(rootElement));
Iterable<Element> collectAllElementsFrom(Element rootElement, {
@required bool skipOffstage
}) {
return new CachingIterable<Element>(new _DepthFirstChildIterator(rootElement, skipOffstage));
}
class _DepthFirstChildIterator implements Iterator<Element> {
_DepthFirstChildIterator(Element rootElement)
: _stack = _reverseChildrenOf(rootElement).toList();
_DepthFirstChildIterator(Element rootElement, bool skipOffstage)
: skipOffstage = skipOffstage,
_stack = _reverseChildrenOf(rootElement, skipOffstage).toList();
final bool skipOffstage;
Element _current;
@ -37,14 +44,18 @@ class _DepthFirstChildIterator implements Iterator<Element> {
_current = _stack.removeLast();
// Stack children in reverse order to traverse first branch first
_stack.addAll(_reverseChildrenOf(_current));
_stack.addAll(_reverseChildrenOf(_current, skipOffstage));
return true;
}
static Iterable<Element> _reverseChildrenOf(Element element) {
static Iterable<Element> _reverseChildrenOf(Element element, bool skipOffstage) {
final List<Element> children = <Element>[];
element.visitChildren(children.add);
if (skipOffstage) {
element.visitChildrenForSemantics(children.add);
} else {
element.visitChildren(children.add);
}
return children.reversed;
}
}

View File

@ -85,7 +85,7 @@ class WidgetController {
/// using [Iterator.moveNext].
Iterable<Element> get allElements {
TestAsyncUtils.guardSync();
return collectAllElementsFrom(binding.renderViewElement);
return collectAllElementsFrom(binding.renderViewElement, skipOffstage: false);
}
/// The matching element in the widget tree.

View File

@ -27,7 +27,10 @@ class CommonFinders {
/// Example:
///
/// expect(tester, hasWidget(find.text('Back')));
Finder text(String text) => new _TextFinder(text);
///
/// If the `skipOffstage` argument is true (the default), then this skips
/// nodes that are [Offstage] or that are from inactive [Route]s.
Finder text(String text, { bool skipOffstage: true }) => new _TextFinder(text, skipOffstage: skipOffstage);
/// Looks for widgets that contain a [Text] descendant with `text`
/// in it.
@ -41,8 +44,11 @@ class CommonFinders {
///
/// // You can find and tap on it like this:
/// tester.tap(find.widgetWithText(Button, 'Update'));
Finder widgetWithText(Type widgetType, String text) {
return new _WidgetWithTextFinder(widgetType, text);
///
/// If the `skipOffstage` argument is true (the default), then this skips
/// nodes that are [Offstage] or that are from inactive [Route]s.
Finder widgetWithText(Type widgetType, String text, { bool skipOffstage: true }) {
return new _WidgetWithTextFinder(widgetType, text, skipOffstage: skipOffstage);
}
/// Finds widgets by searching for one with a particular [Key].
@ -50,7 +56,10 @@ class CommonFinders {
/// Example:
///
/// expect(tester, hasWidget(find.byKey(backKey)));
Finder byKey(Key key) => new _KeyFinder(key);
///
/// If the `skipOffstage` argument is true (the default), then this skips
/// nodes that are [Offstage] or that are from inactive [Route]s.
Finder byKey(Key key, { bool skipOffstage: true }) => new _KeyFinder(key, skipOffstage: skipOffstage);
/// Finds widgets by searching for widgets with a particular type.
///
@ -63,7 +72,10 @@ class CommonFinders {
/// Example:
///
/// expect(tester, hasWidget(find.byType(IconButton)));
Finder byType(Type type) => new _WidgetTypeFinder(type);
///
/// If the `skipOffstage` argument is true (the default), then this skips
/// nodes that are [Offstage] or that are from inactive [Route]s.
Finder byType(Type type, { bool skipOffstage: true }) => new _WidgetTypeFinder(type, skipOffstage: skipOffstage);
/// Finds widgets by searching for elements with a particular type.
///
@ -76,7 +88,10 @@ class CommonFinders {
/// Example:
///
/// expect(tester, hasWidget(find.byElementType(SingleChildRenderObjectElement)));
Finder byElementType(Type type) => new _ElementTypeFinder(type);
///
/// If the `skipOffstage` argument is true (the default), then this skips
/// nodes that are [Offstage] or that are from inactive [Route]s.
Finder byElementType(Type type, { bool skipOffstage: true }) => new _ElementTypeFinder(type, skipOffstage: skipOffstage);
/// Finds widgets whose current widget is the instance given by the
/// argument.
@ -90,7 +105,10 @@ class CommonFinders {
///
/// // You can find and tap on it like this:
/// tester.tap(find.byConfig(myButton));
Finder byConfig(Widget config) => new _ConfigFinder(config);
///
/// If the `skipOffstage` argument is true (the default), then this skips
/// nodes that are [Offstage] or that are from inactive [Route]s.
Finder byConfig(Widget config, { bool skipOffstage: true }) => new _ConfigFinder(config, skipOffstage: skipOffstage);
/// Finds widgets using a widget predicate.
///
@ -99,8 +117,11 @@ class CommonFinders {
/// expect(tester, hasWidget(find.byWidgetPredicate(
/// (Widget widget) => widget is Tooltip && widget.message == 'Back'
/// )));
Finder byWidgetPredicate(WidgetPredicate predicate) {
return new _WidgetPredicateFinder(predicate);
///
/// If the `skipOffstage` argument is true (the default), then this skips
/// nodes that are [Offstage] or that are from inactive [Route]s.
Finder byWidgetPredicate(WidgetPredicate predicate, { bool skipOffstage: true }) {
return new _WidgetPredicateFinder(predicate, skipOffstage: skipOffstage);
}
/// Finds widgets using an element predicate.
@ -113,14 +134,21 @@ class CommonFinders {
/// // (contrast with byElementType, which only returns exact matches)
/// (Element element) => element is SingleChildRenderObjectElement
/// )));
Finder byElementPredicate(ElementPredicate predicate) {
return new _ElementPredicateFinder(predicate);
///
/// If the `skipOffstage` argument is true (the default), then this skips
/// nodes that are [Offstage] or that are from inactive [Route]s.
Finder byElementPredicate(ElementPredicate predicate, { bool skipOffstage: true }) {
return new _ElementPredicateFinder(predicate, skipOffstage: skipOffstage);
}
}
/// Searches a widget tree and returns nodes that match a particular
/// pattern.
abstract class Finder {
/// Initialises a Finder. Used by subclasses to initialize the [skipOffstage]
/// property.
Finder({ this.skipOffstage: true });
/// Describes what the finder is looking for. The description should be
/// a brief English noun phrase describing the finder's pattern.
String get description;
@ -134,11 +162,19 @@ abstract class Finder {
/// function, consider extending [MatchFinder] instead.
Iterable<Element> apply(Iterable<Element> candidates);
// Right now this is hard-coded to just grab the elements from the binding.
//
// One could imagine a world where CommonFinders and Finder can be configured
// to work from a specific subtree, but we'll implement that when it's needed.
static Iterable<Element> get _allElements => collectAllElementsFrom(WidgetsBinding.instance.renderViewElement);
/// Whether this finder skips nodes that are offstage.
///
/// If this is true, then the elements are walked using
/// [Element.visitChildrenForSemantics]. This skips offstage children of
/// [Offstage] widgets, as well as children of inactive [Route]s.
final bool skipOffstage;
Iterable<Element> get _allElements {
return collectAllElementsFrom(
WidgetsBinding.instance.renderViewElement,
skipOffstage: skipOffstage
);
}
Iterable<Element> _cachedResult;
@ -171,21 +207,26 @@ abstract class Finder {
@override
String toString() {
final String additional = skipOffstage ? ' (ignoring offstage widgets)' : '';
final List<Element> widgets = evaluate().toList();
final int count = widgets.length;
if (count == 0)
return 'zero widgets with $description';
return 'zero widgets with $description$additional';
if (count == 1)
return 'exactly one widget with $description: ${widgets.single}';
return 'exactly one widget with $description$additional: ${widgets.single}';
if (count < 4)
return '$count widgets with $description: $widgets';
return '$count widgets with $description: ${widgets[0]}, ${widgets[1]}, ${widgets[2]}, ...';
return '$count widgets with $description$additional: $widgets';
return '$count widgets with $description$additional: ${widgets[0]}, ${widgets[1]}, ${widgets[2]}, ...';
}
}
/// Searches a widget tree and returns nodes that match a particular
/// pattern.
abstract class MatchFinder extends Finder {
/// Initialises a predicate-based Finder. Used by subclasses to initialize the
/// [skipOffstage] property.
MatchFinder({ bool skipOffstage: true }) : super(skipOffstage: skipOffstage);
/// Returns true if the given element matches the pattern.
///
/// When implementing your own MatchFinder, this is the main method to override.
@ -198,7 +239,7 @@ abstract class MatchFinder extends Finder {
}
class _TextFinder extends MatchFinder {
_TextFinder(this.text);
_TextFinder(this.text, { bool skipOffstage: true }) : super(skipOffstage: skipOffstage);
final String text;
@ -215,7 +256,7 @@ class _TextFinder extends MatchFinder {
}
class _WidgetWithTextFinder extends Finder {
_WidgetWithTextFinder(this.widgetType, this.text);
_WidgetWithTextFinder(this.widgetType, this.text, { bool skipOffstage: true }) : super(skipOffstage: skipOffstage);
final Type widgetType;
final String text;
@ -249,7 +290,7 @@ class _WidgetWithTextFinder extends Finder {
}
class _KeyFinder extends MatchFinder {
_KeyFinder(this.key);
_KeyFinder(this.key, { bool skipOffstage: true }) : super(skipOffstage: skipOffstage);
final Key key;
@ -263,7 +304,7 @@ class _KeyFinder extends MatchFinder {
}
class _WidgetTypeFinder extends MatchFinder {
_WidgetTypeFinder(this.widgetType);
_WidgetTypeFinder(this.widgetType, { bool skipOffstage: true }) : super(skipOffstage: skipOffstage);
final Type widgetType;
@ -277,7 +318,7 @@ class _WidgetTypeFinder extends MatchFinder {
}
class _ElementTypeFinder extends MatchFinder {
_ElementTypeFinder(this.elementType);
_ElementTypeFinder(this.elementType, { bool skipOffstage: true }) : super(skipOffstage: skipOffstage);
final Type elementType;
@ -291,7 +332,7 @@ class _ElementTypeFinder extends MatchFinder {
}
class _ConfigFinder extends MatchFinder {
_ConfigFinder(this.config);
_ConfigFinder(this.config, { bool skipOffstage: true }) : super(skipOffstage: skipOffstage);
final Widget config;
@ -305,7 +346,7 @@ class _ConfigFinder extends MatchFinder {
}
class _WidgetPredicateFinder extends MatchFinder {
_WidgetPredicateFinder(this.predicate);
_WidgetPredicateFinder(this.predicate, { bool skipOffstage: true }) : super(skipOffstage: skipOffstage);
final WidgetPredicate predicate;
@ -319,7 +360,7 @@ class _WidgetPredicateFinder extends MatchFinder {
}
class _ElementPredicateFinder extends MatchFinder {
_ElementPredicateFinder(this.predicate);
_ElementPredicateFinder(this.predicate, { bool skipOffstage: true }) : super(skipOffstage: skipOffstage);
final ElementPredicate predicate;

View File

@ -36,12 +36,19 @@ const Matcher findsOneWidget = const _FindsWidgetMatcher(1, 1);
Matcher findsNWidgets(int n) => new _FindsWidgetMatcher(n, n);
/// Asserts that the [Finder] locates the a single widget that has at
/// least one [OffStage] widget ancestor.
const Matcher isOffStage = const _IsOffStage();
/// least one [Offstage] widget ancestor.
///
/// It's important to use a full finder, since by default finders exclude
/// offstage widgets.
///
/// Example:
///
/// expect(find.text('Save', skipOffstage: false), isOffstage);
const Matcher isOffstage = const _IsOffstage();
/// Asserts that the [Finder] locates the a single widget that has no
/// [OffStage] widget ancestors.
const Matcher isOnStage = const _IsOnStage();
/// [Offstage] widget ancestors.
const Matcher isOnstage = const _IsOnstage();
/// Asserts that the [Finder] locates the a single widget that has at
/// least one [Card] widget ancestor.
@ -54,7 +61,8 @@ const Matcher isNotInCard = const _IsNotInCard();
/// Asserts that an object's toString() is a plausible one-line description.
///
/// Specifically, this matcher checks that the string does not contains newline
/// characters and does not have leading or trailing whitespace.
/// characters, and does not have leading or trailing whitespace, and is not
/// empty.
const Matcher hasOneLineDescription = const _HasOneLineDescription();
class _FindsWidgetMatcher extends Matcher {
@ -66,18 +74,17 @@ class _FindsWidgetMatcher extends Matcher {
@override
bool matches(Finder finder, Map<dynamic, dynamic> matchState) {
assert(min != null || max != null);
assert(min == null || max == null || min <= max);
matchState[Finder] = finder;
int count = 0;
Iterator<Element> iterator = finder.evaluate().iterator;
if (min != null) {
int count = 0;
Iterator<Element> iterator = finder.evaluate().iterator;
while (count < min && iterator.moveNext())
count += 1;
if (count < min)
return false;
}
if (max != null) {
int count = 0;
Iterator<Element> iterator = finder.evaluate().iterator;
while (count <= max && iterator.moveNext())
count += 1;
if (count > max)
@ -137,9 +144,11 @@ class _FindsWidgetMatcher extends Matcher {
}
bool _hasAncestorMatching(Finder finder, bool predicate(Widget widget)) {
expect(finder, findsOneWidget);
Iterable<Element> nodes = finder.evaluate();
if (nodes.length != 1)
return false;
bool result = false;
finder.evaluate().single.visitAncestorElements((Element ancestor) {
nodes.single.visitAncestorElements((Element ancestor) {
if (predicate(ancestor.widget)) {
result = true;
return false;
@ -153,16 +162,15 @@ bool _hasAncestorOfType(Finder finder, Type targetType) {
return _hasAncestorMatching(finder, (Widget widget) => widget.runtimeType == targetType);
}
class _IsOffStage extends Matcher {
const _IsOffStage();
class _IsOffstage extends Matcher {
const _IsOffstage();
@override
bool matches(Finder finder, Map<dynamic, dynamic> matchState) {
return _hasAncestorMatching(finder, (Widget widget) {
if (widget.runtimeType != OffStage)
return false;
OffStage offstage = widget;
return offstage.offstage;
if (widget is Offstage)
return widget.offstage;
return false;
});
}
@ -170,18 +178,19 @@ class _IsOffStage extends Matcher {
Description describe(Description description) => description.add('offstage');
}
class _IsOnStage extends Matcher {
const _IsOnStage();
class _IsOnstage extends Matcher {
const _IsOnstage();
@override
bool matches(Finder finder, Map<dynamic, dynamic> matchState) {
expect(finder, findsOneWidget);
Iterable<Element> nodes = finder.evaluate();
if (nodes.length != 1)
return false;
bool result = true;
finder.evaluate().single.visitAncestorElements((Element ancestor) {
nodes.single.visitAncestorElements((Element ancestor) {
Widget widget = ancestor.widget;
if (widget.runtimeType == OffStage) {
OffStage offstage = widget;
result = !offstage.offstage;
if (widget is Offstage) {
result = !widget.offstage;
return false;
}
return true;
@ -219,9 +228,9 @@ class _HasOneLineDescription extends Matcher {
@override
bool matches(Object object, Map<dynamic, dynamic> matchState) {
String description = object.toString();
return description.isNotEmpty &&
!description.contains('\n') &&
description.trim() == description;
return description.isNotEmpty
&& !description.contains('\n')
&& description.trim() == description;
}
@override

View File

@ -204,7 +204,7 @@ class WidgetTester extends WidgetController implements HitTestDispatcher {
)?.target;
if (innerTarget == null)
return null;
final Element innerTargetElement = collectAllElementsFrom(binding.renderViewElement)
final Element innerTargetElement = collectAllElementsFrom(binding.renderViewElement, skipOffstage: true)
.lastWhere((Element element) => element.renderObject == innerTarget);
final List<Element> candidates = <Element>[];
innerTargetElement.visitAncestorElements((Element element) {

View File

@ -15,7 +15,7 @@ void main() {
testWidgets('fails with a descriptive message', (WidgetTester tester) async {
TestFailure failure;
try {
expect(find.text('foo'), findsOneWidget);
expect(find.text('foo', skipOffstage: false), findsOneWidget);
} catch(e) {
failure = e;
}
@ -38,7 +38,7 @@ void main() {
TestFailure failure;
try {
expect(find.text('foo'), findsNothing);
expect(find.text('foo', skipOffstage: false), findsNothing);
} catch(e) {
failure = e;
}
@ -50,6 +50,24 @@ void main() {
expect(message, contains('Actual: ?:<exactly one widget with text "foo": Text("foo")>\n'));
expect(message, contains('Which: means one was found but none were expected\n'));
});
testWidgets('fails with a descriptive message when skipping', (WidgetTester tester) async {
await tester.pumpWidget(new Text('foo'));
TestFailure failure;
try {
expect(find.text('foo'), findsNothing);
} catch(e) {
failure = e;
}
expect(failure, isNotNull);
String message = failure.message;
expect(message, contains('Expected: no matching nodes in the widget tree\n'));
expect(message, contains('Actual: ?:<exactly one widget with text "foo" (ignoring offstage widgets): Text("foo")>\n'));
expect(message, contains('Which: means one was found but none were expected\n'));
});
});
}