Commit 8f56f6fd authored by Ian Hickson's avatar Ian Hickson Committed by GitHub

Add documentation and clean up code. (#11330)

Mainly, this adds documentation to members that were previously
lacking documentation.

It also adds a big block of documentation about improving performance
of widgets.

This also removes some references to package:collection and adds
global setEquals and listEquals methods in foundation that we can use.
(setEquals in particular should be much faster than the
package:collection equivalent, though both should be faster as they
avoid allocating new objects.) All remaining references now qualify
the import so we know what our remaining dependencies are.

Also lots of code reordering in Flutter driver to make the code
consistent and apply the style guide more thoroughly.
parent e0f3001f
......@@ -5,7 +5,7 @@
import 'dart:async';
import 'package:test/test.dart';
import 'package:collection/collection.dart';
import 'package:collection/collection.dart' show ListEquality, MapEquality;
import 'package:flutter_devicelab/framework/adb.dart';
......
......@@ -33,6 +33,7 @@ export 'src/foundation/assertions.dart';
export 'src/foundation/basic_types.dart';
export 'src/foundation/binding.dart';
export 'src/foundation/change_notifier.dart';
export 'src/foundation/collections.dart';
export 'src/foundation/debug.dart';
export 'src/foundation/licenses.dart';
export 'src/foundation/observer_list.dart';
......
......@@ -34,6 +34,7 @@
/// * [Summary], which is used to provide a one-line description of a
/// class that overrides the inline documentations' own description.
class Category {
/// Create an annotation to provide a categorization of a class.
const Category(this.sections) : assert(sections != null);
/// The strings the correspond to the section and subsection of the
......@@ -67,6 +68,7 @@ class Category {
/// * [Summary], which is used to provide a one-line description of a
/// class that overrides the inline documentations' own description.
class DocumentationIcon {
/// Create an annotation to provide a URL to an image describing a class.
const DocumentationIcon(this.url) : assert(url != null);
/// The URL to an image that represents the annotated class.
......@@ -102,6 +104,7 @@ class DocumentationIcon {
/// * [DocumentationIcon], which is used to give the URL to an image that
/// represents the class.
class Summary {
/// Create an annotation to provide a short description of a class.
const Summary(this.text) : assert(text != null);
/// The text of the summary of the annotated class.
......
// Copyright 2017 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
// TODO(ianh): These should be on the Set and List classes themselves.
/// Compares two sets for deep equality.
///
/// Returns true if the sets are both null, or if they are both non-null, have
/// the same length, and contain the same members. Returns false otherwise.
/// Order is not compared.
///
/// See also:
///
/// * [listEquals], which does something similar for lists.
bool setEquals<T>(Set<T> a, Set<T> b) {
if (a == null)
return b == null;
if (b == null || a.length != b.length)
return false;
for (T value in a) {
if (!b.contains(value))
return false;
}
return true;
}
/// Compares two lists for deep equality.
///
/// Returns true if the lists are both null, or if they are both non-null, have
/// the same length, and contain the same members in the same order. Returns
/// false otherwise.
///
/// See also:
///
/// * [setEquals], which does something similar for sets.
bool listEquals<T>(List<T> a, List<T> b) {
if (a == null)
return b == null;
if (b == null || a.length != b.length)
return false;
for (int index = 0; index < a.length; index += 1) {
if (a[index] != b[index])
return false;
}
return true;
}
......@@ -59,7 +59,8 @@ class BackButtonIcon extends StatelessWidget {
/// See also:
///
/// * [AppBar], which automatically uses a [BackButton] in its
/// [AppBar.leading] slot when appropriate.
/// [AppBar.leading] slot when the [Scaffold] has no [Drawer] and the
/// current [Route] is not the [Navigator]'s first route.
/// * [BackButtonIcon], which is useful if you need to create a back button
/// that responds differently to being pressed.
/// * [IconButton], which is a more general widget for creating buttons with
......
......@@ -268,13 +268,19 @@ class DayPicker extends StatelessWidget {
}
// Do not use this directly - call getDaysInMonth instead.
static const List<int> _kDaysInMonth = const <int>[31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
static const List<int> _kDaysInMonth = const <int>[31, -1, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
/// Returns the number of days in a month, according to the proleptic
/// Gregorian calendar.
///
/// This applies the leap year logic introduced by the Gregorian reforms of
/// 1582. It will not give valid results for dates prior to that time.
static int getDaysInMonth(int year, int month) {
if (month == DateTime.FEBRUARY) {
final bool isLeapYear = (year % 4 == 0) && (year % 100 != 0) || (year % 400 == 0);
if (isLeapYear)
return 29;
return 28;
}
return _kDaysInMonth[month - 1];
}
......
......@@ -19,12 +19,18 @@ class _InputDecoratorChildGlobalKey extends GlobalObjectKey {
/// Text and styles used to label an input field.
///
/// The [TextField] and [InputDecorator] classes use [InputDecoration] objects
/// to describe their decoration. (In fact, this class is merely the
/// configuration of an [InputDecorator], which does all the heavy lifting.)
///
/// See also:
///
/// * [TextField], which is a text input widget that uses an
/// [InputDecoration].
/// * [InputDecorator], which is a widget that draws an [InputDecoration]
/// around an arbitrary child widget.
/// * [Decoration] and [DecoratedBox], for drawing arbitrary decorations
/// around other widgets.
@immutable
class InputDecoration {
/// Creates a bundle of text and styles used to label an input field.
......@@ -307,15 +313,22 @@ class InputDecoration {
/// Use [InputDecorator] to create widgets that look and behave like a
/// [TextField] but can be used to input information other than text.
///
/// The configuration of this widget is primarily provided in the form of an
/// [InputDecoration] object.
///
/// Requires one of its ancestors to be a [Material] widget.
///
/// See also:
///
/// * [TextField], which uses an [InputDecorator] to draw labels and other
/// * [TextField], which uses an [InputDecorator] to draw labels and other
/// visual elements around a text entry widget.
/// * [Decoration] and [DecoratedBox], for drawing arbitrary decorations
/// around other widgets.
class InputDecorator extends StatelessWidget {
/// Creates a widget that displayes labels and other visual elements similar
/// to a [TextField].
///
/// The [isFocused] and [isEmpty] arguments must not be null.
const InputDecorator({
Key key,
@required this.decoration,
......@@ -324,7 +337,9 @@ class InputDecorator extends StatelessWidget {
this.isFocused: false,
this.isEmpty: false,
this.child,
}) : super(key: key);
}) : assert(isFocused != null),
assert(isEmpty != null),
super(key: key);
/// The text and styles to use when decorating the child.
final InputDecoration decoration;
......
......@@ -63,7 +63,7 @@ export 'dart:ui' show
/// For example, [layout] (index 3) implies [paint] (2).
enum RenderComparison {
/// The two objects are identical (meaning deeply equal, not necessarily
/// [identical]).
/// [dart:core.identical]).
identical,
/// The two objects are identical for the purpose of layout, but may be different
......
......@@ -11,19 +11,6 @@ import 'package:flutter/services.dart';
import 'basic_types.dart';
import 'text_style.dart';
// TODO(ianh): This should be on List itself.
bool _deepEquals(List<Object> a, List<Object> b) {
if (a == null)
return b == null;
if (b == null || a.length != b.length)
return false;
for (int i = 0; i < a.length; i += 1) {
if (a[i] != b[i])
return false;
}
return true;
}
/// An immutable span of text.
///
/// A [TextSpan] object can be styled using its [style] property.
......@@ -360,7 +347,7 @@ class TextSpan {
return typedOther.text == text
&& typedOther.style == style
&& typedOther.recognizer == recognizer
&& _deepEquals(typedOther.children, children);
&& listEquals<TextSpan>(typedOther.children, children);
}
@override
......
......@@ -147,8 +147,9 @@ bool debugCheckIntrinsicSizes = false;
/// * [debugPrintLayouts], which does something similar for layout but using
/// console output.
///
/// * [debugPrintRebuildDirtyWidgets], which does something similar for widgets
/// being rebuilt.
/// * [debugProfileBuildsEnabled], which does something similar for widgets
/// being rebuilt, and [debugPrintRebuildDirtyWidgets], its console
/// equivalent.
///
/// * The discussion at [RendererBinding.drawFrame].
bool debugProfilePaintsEnabled = false;
......
......@@ -118,6 +118,7 @@ abstract class Layer extends AbstractNode with TreeDiagnosticsMixin {
///
/// Picture layers are always leaves in the layer tree.
class PictureLayer extends Layer {
/// Creates a leaf layer for the layer tree.
PictureLayer(this.canvasBounds);
/// The bounds that were used for the canvas that drew this layer's [picture].
......
......@@ -8,7 +8,6 @@ import 'package:flutter/foundation.dart';
import 'package:flutter/gestures.dart';
import 'package:flutter/painting.dart';
import 'package:collection/collection.dart';
import 'package:vector_math/vector_math_64.dart';
import 'box.dart';
......@@ -2736,10 +2735,23 @@ class RenderSemanticsGestureHandler extends RenderProxyBox implements SemanticsA
_onVerticalDragUpdate = onVerticalDragUpdate,
super(child);
/// If non-null, the set of actions to allow. Other actions will be omitted,
/// even if their callback is provided.
///
/// For example, if [onTap] is non-null but [validActions] does not contain
/// [SemanticsAction.tap], then the semantic description of this node will
/// not claim to support taps.
///
/// This is normally used to filter the actions made available by
/// [onHorizontalDragUpdate] and [onVerticalDragUpdate]. Normally, these make
/// both the right and left, or up and down, actions available. For example,
/// if [onHorizontalDragUpdate] is set but [validActions] only contains
/// [SemanticsAction.scrollLeft], then the [SemanticsAction.scrollRight]
/// action will be omitted.
Set<SemanticsAction> get validActions => _validActions;
Set<SemanticsAction> _validActions;
set validActions(Set<SemanticsAction> value) {
if (const SetEquality<SemanticsAction>().equals(value, _validActions))
if (setEquals<SemanticsAction>(value, _validActions))
return;
_validActions = value;
markNeedsSemanticsUpdate(onlyChanges: true);
......
......@@ -30,8 +30,13 @@ import 'viewport_offset.dart';
///
/// * hit testing, painting, and other details of the sliver protocol.
///
/// Subclasses must implement [performLayout], [minExtent], and [maxExtent].
/// Subclasses must implement [performLayout], [minExtent], and [maxExtent], and
/// typically also will implement [updateChild].
abstract class RenderSliverPersistentHeader extends RenderSliver with RenderObjectWithChildMixin<RenderBox>, RenderSliverHelpers {
/// Creates a sliver that changes its size when scrolled to the start of the
/// viewport.
///
/// This is an abstract class; this constructor only initializes the [child].
RenderSliverPersistentHeader({ RenderBox child }) {
this.child = child;
}
......@@ -101,6 +106,15 @@ abstract class RenderSliverPersistentHeader extends RenderSliver with RenderObje
super.markNeedsLayout();
}
/// Lays out the [child].
///
/// This is called by [performLayout]. It applies the given `scrollOffset`
/// (which need not match the offset given by the [constraints]) and the
/// `maxExtent` (which need not match the value returned by the [maxExtent]
/// getter).
///
/// The `overlapsContent` argument is passed to [updateChild].
@protected
void layoutChild(double scrollOffset, double maxExtent, { bool overlapsContent: false }) {
assert(maxExtent != null);
final double shrinkOffset = math.min(scrollOffset, maxExtent);
......@@ -211,6 +225,8 @@ abstract class RenderSliverPersistentHeader extends RenderSliver with RenderObje
///
/// This sliver makes no effort to avoid overlapping other content.
abstract class RenderSliverScrollingPersistentHeader extends RenderSliverPersistentHeader {
/// Creates a sliver that shrinks when it hits the start of the viewport, then
/// scrolls off.
RenderSliverScrollingPersistentHeader({
RenderBox child,
}) : super(child: child);
......@@ -247,6 +263,8 @@ abstract class RenderSliverScrollingPersistentHeader extends RenderSliverPersist
///
/// This sliver avoids overlapping other earlier slivers where possible.
abstract class RenderSliverPinnedPersistentHeader extends RenderSliverPersistentHeader {
/// Creates a sliver that shrinks when it hits the start of the viewport, then
/// stays pinned there.
RenderSliverPinnedPersistentHeader({
RenderBox child,
}) : super(child: child);
......@@ -304,7 +322,15 @@ class FloatingHeaderSnapConfiguration {
/// A sliver with a [RenderBox] child which shrinks and scrolls like a
/// [RenderSliverScrollingPersistentHeader], but immediately comes back when the
/// user scrolls in the reverse direction.
///
/// See also:
///
/// * [RenderSliverFloatingPinnedPersistentHeader], which is similar but sticks
/// to the start of the viewport rather than scrolling off.
abstract class RenderSliverFloatingPersistentHeader extends RenderSliverPersistentHeader {
/// Creates a sliver that shrinks when it hits the start of the viewport, then
/// scrolls off, and comes back immediately when the user reverses the scroll
/// direction.
RenderSliverFloatingPersistentHeader({
RenderBox child,
FloatingHeaderSnapConfiguration snapConfiguration,
......@@ -352,7 +378,9 @@ abstract class RenderSliverFloatingPersistentHeader extends RenderSliverPersiste
_snapConfiguration = value;
}
// Update [geometry] and return the new value for [childMainAxisPosition].
/// Updates [geometry], and returns the new value for [childMainAxisPosition].
///
/// This is used by [performLayout].
@protected
double updateGeometry() {
final double maxExtent = this.maxExtent;
......@@ -443,7 +471,18 @@ abstract class RenderSliverFloatingPersistentHeader extends RenderSliverPersiste
}
}
/// A sliver with a [RenderBox] child which shrinks and then remains pinned to
/// the start of the viewport like a [RenderSliverPinnedPersistentHeader], but
/// immediately grows when the user scrolls in the reverse direction.
///
/// See also:
///
/// * [RenderSliverFloatingPersistentHeader], which is similar but scrolls off
/// the top rather than sticking to it.
abstract class RenderSliverFloatingPinnedPersistentHeader extends RenderSliverFloatingPersistentHeader {
/// Creates a sliver that shrinks when it hits the start of the viewport, then
/// stays pinned there, and grows immediately when the user reverses the
/// scroll direction.
RenderSliverFloatingPinnedPersistentHeader({
RenderBox child,
FloatingHeaderSnapConfiguration snapConfiguration,
......
......@@ -8,7 +8,7 @@ import 'dart:developer';
import 'dart:ui' as ui show window;
import 'dart:ui' show VoidCallback;
import 'package:collection/collection.dart';
import 'package:collection/collection.dart' show PriorityQueue, HeapPriorityQueue;
import 'package:flutter/foundation.dart';
import 'debug.dart';
......
......@@ -105,6 +105,14 @@ class AnimatedList extends StatefulWidget {
/// view is scrolled.
///
/// Must be null if [primary] is true.
///
/// A [ScrollController] serves several purposes. It can be used to control
/// the initial scroll position (see [ScrollController.initialScrollOffset]).
/// It can be used to control whether the scroll view should automatically
/// save and restore its scroll position in the [PageStorage] (see
/// [ScrollController.keepScrollOffset]). It can be used to read the current
/// scroll position (see [ScrollController.offset]), or change it (see
/// [ScrollController.animateTo]).
final ScrollController controller;
/// Whether this is the primary scroll view associated with the parent
......
......@@ -24,6 +24,10 @@ import 'table.dart';
/// Combined with [debugPrintScheduleBuildForStacks], this lets you watch a
/// widget's dirty/clean lifecycle.
///
/// To get similar information but showing it on the timeline available from the
/// Observatory rather than getting it in the console (where it can be
/// overwhelming), consider [debugProfileBuildsEnabled].
///
/// See also the discussion at [WidgetsBinding.drawFrame].
bool debugPrintRebuildDirtyWidgets = false;
......@@ -63,6 +67,10 @@ bool debugPrintGlobalKeyedWidgetLifecycle = false;
///
/// For details on how to use [Timeline] events in the Dart Observatory to
/// optimize your app, see https://fuchsia.googlesource.com/sysui/+/master/docs/performance.md
///
/// See also [debugProfilePaintsEnabled], which does something similar but for
/// painting, and [debugPrintRebuildDirtyWidgets], which does something similar
/// but reporting the builds to the console.
bool debugProfileBuildsEnabled = false;
/// Show banners for deprecated widgets.
......
......@@ -523,6 +523,46 @@ abstract class Widget {
/// having an internal clock-driven state, or depending on some system state,
/// consider using [StatefulWidget].
///
/// ## Performance considerations
///
/// The [build] method of a stateless widget is typically only called in three
/// situations: the first time the widget is inserted in the tree, when the
/// widget's parent changes its configuration, and when an [InheritedWidget] it
/// depends on changes.
///
/// If a widget's parent will regularly change the widget's configuration, or if
/// it depends on inherited widgets that frequently change, then it is important
/// to optimize the performance of the [build] method to maintain a fluid
/// rendering performance.
///
/// There are several techniques one can use to minimize the impact of
/// rebuilding a stateless widget:
///
/// * Minimize the number of nodes transitively created by the build method and
/// any widgets it creates. For example, instead of an elaborate arrangement
/// of [Row]s, [Column]s, [Padding]s, and [SizedBox]es to position a single
/// child in a particularly fancy manner, consider using just an [Align] or a
/// [CustomSingleChildLayout]. Instead of an intricate layering of multiple
/// [Container]s and with [Decoration]s to draw just the right graphical
/// effect, consider a single [CustomPaint] widget.
///
/// * Use `const` widgets where possible, and provide a `const` constructor for
/// the widget so that users of the widget can also do so.
///
/// * Consider refactoring the stateless widget into a stateful widget so that
/// it can use some of the techniques described at [StatefulWidget], such as
/// caching common parts of subtrees and using [GlobalKey]s when changing the
/// tree structure.
///
/// * If the widget is likely to get rebuilt frequently due to the use of
/// [InheritedWidget]s, consider refactoring the stateless widget into
/// multiple widgets, with the parts of the tree that change being pushed to
/// the leaves. For example instead of building a tree with four widgets, the
/// inner-most widget depending on the [Theme], consider factoring out the
/// part of the build function that builds the inner-most widget into its own
/// widget, so that only the inner-most widget needs to be rebuilt when the
/// theme changes.
///
/// ## Sample code
///
/// The following is a skeleton of a stateless widget subclass called `GreenFrog`:
......@@ -614,6 +654,10 @@ abstract class StatelessWidget extends Widget {
///
/// If a widget's [build] method is to depend on anything else, use a
/// [StatefulWidget] instead.
///
/// See also:
///
/// * The discussion on performance considerations at [StatelessWidget].
@protected
Widget build(BuildContext context);
}
......@@ -666,6 +710,66 @@ abstract class StatelessWidget extends Widget {
/// eligible for grafting, the widget might be inserted into the new location in
/// the same animation frame in which it was removed from the old location.
///
/// ## Performance considerations
///
/// There are two primary categories of [StatefulWidget]s.
///
/// The first is one which allocates resources in [State.initState] and disposes
/// of them in [State.dispose], but which does not depend on [InheritedWidget]s
/// or call [State.setState]. Such widgets are commonly used at the root of an
/// application or page, and communicate with subwidgets via [ChangeNotifier]s,
/// [Stream]s, or other such objects. Stateful widgets following such a pattern
/// are relatively cheap (in terms of CPU and GPU cycles), because they are
/// built once then never update. They can, therefore, have somewhat complicated
/// and deep build methods.
///
/// The second category is widgets that use [State.setState] or depend on
/// [InheritedWidget]s. These will typically rebuild many times during the
/// application's lifetime, and it is therefore important to minimise the impact
/// of rebuilding such a widget. (They may also use [State.initState] or
/// [State.didChangeDependencies] and allocate resources, but the important part
/// is that they rebuild.)
///
/// There are several techniques one can use to minimize the impact of
/// rebuilding a stateful widget:
///
/// * Push the state to the leaves. For example, if your page has a ticking
/// clock, rather than putting the state at the top of the page and
/// rebuilding the entire page each time the clock ticks, create a dedicated
/// clock widget that only updates itself.
///
/// * Minimize the number of nodes transitively created by the build method and
/// any widgets it creates. Ideally, a stateful widget would only create a
/// single widget, and that widget would be a [RenderObjectWidget].
/// (Obviously this isn't always practical, but the closer a widget gets to
/// this ideal, the more efficient it will be.)
///
/// * If a subtree does not change, cache the widget that represents that
/// subtree and re-use it each time it can be used. It is massively more
/// efficient for a widget to be re-used than for a new (but
/// identically-configured) widget to be created. Factoring out the stateful
/// part into a widget that takes a child argument is a common way of doing
/// this.
///
/// * Use `const` widgets where possible. (This is equivalent to caching a
/// widget and re-using it.)
///
/// * Avoid changing the depth of any created subtrees or changing the type of
/// any widgets in the subtree. For example, rather than returning either the
/// child or the child wrapped in an [IgnorePointer], always wrap the child
/// widget in an [IgnorePointer] and control the [IgnorePointer.ignoring]
/// property. This is because changing the depth of the subtree requires
/// rebuilding, laying out, and painting the entire subtree, whereas just
/// changing the property will require the least possible change to the
/// render tree (in the case of [IgnorePointer], for example, no layout or
/// repaint is necessary at all).
///
/// * If the depth must be changed for some reason, consider wrapping the
/// common parts of the subtrees in widgets that have a [GlobalKey] that
/// remains consistent for the life of the stateful widget. (The
/// [KeyedSubtree] widget may be useful for this purpose if no other widget
/// can conveniently be assigned the key.)
///
/// ## Sample code
///
/// The following is a skeleton of a stateful widget subclass called `YellowBird`:
......@@ -1238,6 +1342,10 @@ abstract class State<T extends StatefulWidget> {
/// rebuilds, but the framework has updated that [State] object's [widget]
/// property to refer to the new `MyButton` instance and `${widget.color}`
/// prints green, as expected.
///
/// See also:
///
/// * The discussion on performance considerations at [StatefulWidget].
@protected
Widget build(BuildContext context);
......
......@@ -535,6 +535,16 @@ class RawGestureDetectorState extends State<RawGestureDetector> {
}
}
/// This method can be called after the build phase, during the layout of the
/// nearest descendant [RenderObjectWidget] of the gesture detector, to filter
/// the list of available semantic actions.
///
/// This is used by [Scrollable] to configure system accessibility tools so
/// that they know in which direction a particular list can be scrolled.
///
/// If this is never called, then the actions are not filtered. If the list of
/// actions to filter changes, it must be called again (during the layout of
/// the nearest descendant [RenderObjectWidget] of the gesture detector).
void replaceSemanticsActions(Set<SemanticsAction> actions) {
assert(() {
if (!context.findRenderObject().owner.debugDoingLayout) {
......
......@@ -33,6 +33,9 @@ import 'ticker_provider.dart';
typedef List<Widget> NestedScrollViewHeaderSliversBuilder(BuildContext context, bool innerBoxIsScrolled);
class NestedScrollView extends StatefulWidget {
/// Creates a nested scroll view.
///
/// The [reverse], [headerSliverBuilder], and [body] arguments must not be null.
const NestedScrollView({
Key key,
this.controller,
......@@ -73,9 +76,18 @@ class NestedScrollView extends StatefulWidget {
/// How the scroll view should respond to user input.
///
/// For example, determines how the scroll view continues to animate after the
/// user stops dragging the scroll view.
/// user stops dragging the scroll view (providing a custom implementation of
/// [ScrollPhysics.createBallisticSimulation] allows this particular aspect of
/// the physics to be overridden).
///
/// Defaults to matching platform conventions.
///
/// The [ScrollPhysics.applyBoundaryConditions] implementation of the provided
/// object should not allow scrolling outside the scroll extent range
/// described by the [ScrollMetrics.minScrollExtent] and
/// [ScrollMetrics.maxScrollExtent] properties passed to that method. If that
/// invariant is not maintained, the nested scroll view may respond to user
/// scrolling erratically.
final ScrollPhysics physics;
/// A builder for any widgets that are to precede the inner scroll views (as
......
......@@ -26,6 +26,8 @@ typedef bool NotificationListenerCallback<T extends Notification>(T notification
/// widgets with the appropriate type parameters that are ancestors of the given
/// [BuildContext].
abstract class Notification {
/// Abstract const constructor. This constructor enables subclasses to provide
/// const constructors so that they can be used in const expressions.
const Notification();
/// Applied to each ancestor of the [dispatch] target.
......@@ -107,6 +109,15 @@ class NotificationListener<T extends Notification> extends StatelessWidget {
///
/// The notification's [Notification.visitAncestor] method is called for each
/// ancestor, and invokes this callback as appropriate.
///
/// Notifications vary in terms of when they are dispatched. There are two
/// main possibilities: dispatch between frames, and dispatch during layout.
///
/// For notifications that dispatch during layout, such as those that inherit
/// from [LayoutChangedNotification], it is too late to call [State.setState]
/// in response to the notification (as layout is currently happening in a
/// descendant, by definition, since notifications bubble up the tree). For
/// widgets that depend on layout, consider a [LayoutBuilder] instead.
final NotificationListenerCallback<T> onNotification;
bool _dispatch(Notification notification, Element element) {
......
......@@ -34,7 +34,7 @@ import 'viewport.dart';
///
/// See also:
///
/// - [PageView], which is the widget this object controls.
/// * [PageView], which is the widget this object controls.
class PageController extends ScrollController {
/// Creates a page controller.
///
......@@ -64,7 +64,7 @@ class PageController extends ScrollController {
/// See also:
///
/// * [PageStorageKey], which should be used when more than one
//// scrollable appears in the same route, to distinguish the [PageStorage]
/// scrollable appears in the same route, to distinguish the [PageStorage]
/// locations used to save scroll offsets.
final bool keepPage;
......@@ -251,6 +251,12 @@ class _PagePosition extends ScrollPositionWithSingleContext {
/// Scroll physics used by a [PageView].
///
/// These physics cause the page view to snap to page boundaries.
///
/// See also:
///
/// * [ScrollPhysics], the base class which defines the API for scrolling
/// physics.
/// * [PageView.physics], which can override the physics used by a page view.
class PageScrollPhysics extends ScrollPhysics {
/// Creates physics for a [PageView].
const PageScrollPhysics({ ScrollPhysics parent }) : super(parent: parent);
......@@ -323,6 +329,8 @@ const PageScrollPhysics _kPagePhysics = const PageScrollPhysics();
/// * [SingleChildScrollView], when you need to make a single child scrollable.
/// * [ListView], for a scrollable list of boxes.
/// * [GridView], for a scrollable grid of boxes.
/// * [ScollNotification] and [NotificationListener], which can be used to watch
/// the scroll position without using a [ScrollController].
class PageView extends StatefulWidget {
/// Creates a scrollable list that works page by page from an explicit [List]
/// of widgets.
......
......@@ -33,6 +33,11 @@ class PrimaryScrollController extends InheritedWidget {
super(key: key, child: child);
/// The [ScrollController] associated with the subtree.
///
/// See also:
///
/// * [ScrollView.controller], which discusses the purpose of specifying a
/// scroll controller.
final ScrollController controller;
/// Returns the [ScrollController] most closely associated with the given
......
......@@ -40,6 +40,8 @@ import 'scroll_position_with_single_context.dart';
/// [PageView].
/// * [ScrollPosition], which manages the scroll offset for an individual
/// scrolling widget.
/// * [ScollNotification] and [NotificationListener], which can be used to watch
/// the scroll position without using a [ScrollController].
class ScrollController extends ChangeNotifier {
/// Creates a controller for a scrollable widget.
///
......@@ -59,8 +61,8 @@ class ScrollController extends ChangeNotifier {
/// if [keepScrollOffset] is false or a scroll offset hasn't been saved yet.
///
/// Defaults to 0.0.
final double _initialScrollOffset;
double get initialScrollOffset => _initialScrollOffset;
final double _initialScrollOffset;
/// Each time a scroll completes, save the current scroll [offset] with
/// [PageStorage] and restore it if this controller's scrollable is recreated.
......@@ -272,7 +274,7 @@ class ScrollController extends ChangeNotifier {
// Examples can assume:
// TrackingScrollController _trackingScrollController;
/// A [ScrollController] whose `initialScrollOffset` tracks its most recently
/// A [ScrollController] whose [initialScrollOffset] tracks its most recently
/// updated [ScrollPosition].
///
/// This class can be used to synchronize the scroll offset of two or more
......@@ -309,6 +311,8 @@ class ScrollController extends ChangeNotifier {
/// In this example the `_trackingController` would have been created by the
/// stateful widget that built the widget tree.
class TrackingScrollController extends ScrollController {
/// Creates a scroll controller that continually updates its
/// [initialScrollOffset] to match the last scroll notification it received.
TrackingScrollController({
double initialScrollOffset: 0.0,
bool keepScrollOffset: true,
......@@ -317,14 +321,20 @@ class TrackingScrollController extends ScrollController {
keepScrollOffset: keepScrollOffset,
debugLabel: debugLabel);
Map<ScrollPosition, VoidCallback> _positionToListener = <ScrollPosition, VoidCallback>{};
final Map<ScrollPosition, VoidCallback> _positionToListener = <ScrollPosition, VoidCallback>{};
ScrollPosition _lastUpdated;
/// The last [ScrollPosition] to change. Returns null if there aren't any
/// attached scroll positions or there hasn't been any scrolling yet.
/// attached scroll positions, or there hasn't been any scrolling yet, or the
/// last [ScrollPosition] to change has since been removed.
ScrollPosition get mostRecentlyUpdatedPosition => _lastUpdated;
/// Returns the scroll offset of the [mostRecentlyUpdatedPosition] or 0.0.
/// Returns the scroll offset of the [mostRecentlyUpdatedPosition] or, if that
/// is null, the initial scroll offset provided to the constructor.
///
/// See also:
///
/// * [ScrollController.initialScrollOffset], which this overrides.
@override
double get initialScrollOffset => _lastUpdated?.pixels ?? super.initialScrollOffset;
......@@ -342,6 +352,8 @@ class TrackingScrollController extends ScrollController {
assert(_positionToListener.containsKey(position));
position.removeListener(_positionToListener[position]);
_positionToListener.remove(position);
if (_lastUpdated == position)
_lastUpdated = null;
}
@override
......@@ -350,7 +362,6 @@ class TrackingScrollController extends ScrollController {
assert(_positionToListener.containsKey(position));
position.removeListener(_positionToListener[position]);
}
_positionToListener.clear();
super.dispose();
}
}
......@@ -69,6 +69,18 @@ abstract class ViewportNotificationMixin extends Notification {
/// [Scrollable] widgets. To focus on notifications from the nearest
/// [Scrollable] descendant, check that the [depth] property of the notification
/// is zero.
///
/// When a scroll notification is received by a [NotificationListener], the
/// listener will have already completed build and layout, and it is therefore
/// too late for that widget to call [State.setState]. Any attempt to adjust the
/// build or layout based on a scroll notification would result in a layout that
/// lagged one frame behind, which is a poor user experience. Scroll
/// notifications are therefore primarily useful for paint effects (since paint
/// happens after layout). The [GlowingOverscrollIndicator] and [Scrollbar]
/// widgets are examples of paint effects that use scroll notifications.
///
/// To drive layout based on the scroll position, consider listening to the
/// [ScrollPosition] directly (or indirectly via a [ScrollController]).
abstract class ScrollNotification extends LayoutChangedNotification with ViewportNotificationMixin {
/// Initializes fields for subclasses.
ScrollNotification({
......
......@@ -4,7 +4,6 @@
import 'dart:async';
import 'package:collection/collection.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/gestures.dart';
import 'package:flutter/rendering.dart';
......@@ -59,6 +58,8 @@ export 'scroll_activity.dart' show ScrollHoldController;
/// other scrollable widgets to control a [ScrollPosition].
/// * [ScrollPositionWithSingleContext], which is the most commonly used
/// concrete subclass of [ScrollPosition].
/// * [ScollNotification] and [NotificationListener], which can be used to watch
/// the scroll position without using a [ScrollController].
abstract class ScrollPosition extends ViewportOffset with ScrollMetrics {
/// Creates an object that determines which portion of the content is visible
/// in a scroll view.
......@@ -390,7 +391,7 @@ abstract class ScrollPosition extends ViewportOffset with ScrollMetrics {
if (pixels < maxScrollExtent)
actions.add(forward);
if (const SetEquality<SemanticsAction>().equals(actions, _semanticActions))
if (setEquals<SemanticsAction>(actions, _semanticActions))
return;
_semanticActions = actions;
......
......@@ -29,6 +29,9 @@ import 'viewport.dart';
/// [ScrollView] helps orchestrate these pieces by creating the [Scrollable] and
/// the viewport and defering to its subclass to create the slivers.
///
/// To control the initial scroll offset of the scroll view, provide a
/// [controller] with its [ScrollController.initialScrollOffset] property set.
///
/// See also:
///
/// * [ListView], which is a commonly used [ScrollView] that displays a
......@@ -39,6 +42,8 @@ import 'viewport.dart';
/// of child widgets.
/// * [CustomScrollView], which is a [ScrollView] that creates custom scroll
/// effects using slivers.
/// * [ScollNotification] and [NotificationListener], which can be used to watch
/// the scroll position without using a [ScrollController].
abstract class ScrollView extends StatelessWidget {
/// Creates a widget that scrolls.
///
......@@ -84,6 +89,14 @@ abstract class ScrollView extends StatelessWidget {
/// view is scrolled.
///
/// Must be null if [primary] is true.
///
/// A [ScrollController] serves several purposes. It can be used to control
/// the initial scroll position (see [ScrollController.initialScrollOffset]).
/// It can be used to control whether the scroll view should automatically
/// save and restore its scroll position in the [PageStorage] (see
/// [ScrollController.keepScrollOffset]). It can be used to read the current
/// scroll position (see [ScrollController.offset]), or change it (see
/// [ScrollController.animateTo]).
final ScrollController controller;
/// Whether this is the primary scroll view associated with the parent
......@@ -233,6 +246,9 @@ abstract class ScrollView extends StatelessWidget {
/// list and a grid, use a list of three slivers: [SliverAppBar], [SliverList],
/// and [SliverGrid].
///
/// To control the initial scroll offset of the scroll view, provide a
/// [controller] with its [ScrollController.initialScrollOffset] property set.
///
/// ## Sample code
///
/// This sample code shows a scroll view that contains a flexible pinned app
......@@ -292,6 +308,8 @@ abstract class ScrollView extends StatelessWidget {
/// sliver.
/// * [SliverAppBar], which is a sliver that displays a header that can expand
/// and float as the scroll view scrolls.
/// * [ScollNotification] and [NotificationListener], which can be used to watch
/// the scroll position without using a [ScrollController].
class CustomScrollView extends ScrollView {
/// Creates a [ScrollView] that creates custom scroll effects using slivers.
///
......@@ -406,6 +424,9 @@ abstract class BoxScrollView extends ScrollView {
/// a [SliverChildDelegate] can control the algorithm used to estimate the
/// size of children that are not actually visible.
///
/// To control the initial scroll offset of the scroll view, provide a
/// [controller] with its [ScrollController.initialScrollOffset] property set.
///
/// ## Sample code
///
/// An infinite list of children:
......@@ -504,6 +525,8 @@ abstract class BoxScrollView extends ScrollView {
/// scroll effects using slivers.
/// * [ListBody], which arranges its children in a similar manner, but without
/// scrolling.
/// * [ScollNotification] and [NotificationListener], which can be used to watch
/// the scroll position without using a [ScrollController].
class ListView extends BoxScrollView {
/// Creates a scrollable, linear array of widgets from an explicit [List].
///
......@@ -686,6 +709,9 @@ class ListView extends BoxScrollView {
///
/// To create a linear array of children, use a [ListView].
///
/// To control the initial scroll offset of the scroll view, provide a
/// [controller] with its [ScrollController.initialScrollOffset] property set.
///
/// ## Transitioning to [CustomScrollView]
///
/// A [GridView] is basically a [CustomScrollView] with a single [SliverGrid] in
......@@ -785,6 +811,8 @@ class ListView extends BoxScrollView {
/// a fixed number of tiles in the cross axis.
/// * [SliverGridDelegateWithMaxCrossAxisExtent], which creates a layout with
/// tiles that have a maximum cross-axis extent.
/// * [ScollNotification] and [NotificationListener], which can be used to watch
/// the scroll position without using a [ScrollController].
class GridView extends BoxScrollView {
/// Creates a scrollable, 2D array of widgets with a custom
/// [SliverGridDelegate].
......
......@@ -67,6 +67,8 @@ typedef Widget ViewportBuilder(BuildContext context, ViewportOffset position);
/// effects using slivers.
/// * [SingleChildScrollView], which is a scrollable widget that has a single
/// child.
/// * [ScollNotification] and [NotificationListener], which can be used to watch
/// the scroll position without using a [ScrollController].
class Scrollable extends StatefulWidget {
/// Creates a widget that scrolls.
///
......@@ -96,6 +98,14 @@ class Scrollable extends StatefulWidget {
/// An object that can be used to control the position to which this widget is
/// scrolled.
///
/// A [ScrollController] serves several purposes. It can be used to control
/// the initial scroll position (see [ScrollController.initialScrollOffset]).
/// It can be used to control whether the scroll view should automatically
/// save and restore its scroll position in the [PageStorage] (see
/// [ScrollController.keepScrollOffset]). It can be used to read the current
/// scroll position (see [ScrollController.offset]), or change it (see
/// [ScrollController.animateTo]).
///
/// See also:
///
/// * [ensureVisible], which animates the scroll position to reveal a given
......
......@@ -83,6 +83,14 @@ class SingleChildScrollView extends StatelessWidget {
/// view is scrolled.
///
/// Must be null if [primary] is true.
///
/// A [ScrollController] serves several purposes. It can be used to control
/// the initial scroll position (see [ScrollController.initialScrollOffset]).
/// It can be used to control whether the scroll view should automatically
/// save and restore its scroll position in the [PageStorage] (see
/// [ScrollController.keepScrollOffset]). It can be used to read the current
/// scroll position (see [ScrollController.offset]), or change it (see
/// [ScrollController.animateTo]).
final ScrollController controller;
/// Whether this is the primary scroll view associated with the parent
......
......@@ -7,28 +7,89 @@ import 'package:flutter/rendering.dart';
import 'framework.dart';
/// Delegate for configuring a [SliverPersistentHeader].
abstract class SliverPersistentHeaderDelegate {
/// Abstract const constructor. This constructor enables subclasses to provide
/// const constructors so that they can be used in const expressions.
const SliverPersistentHeaderDelegate();
/// The widget to place inside the [SliverPersistentHeader].
///
/// The `context` is the [BuildContext] of the sliver.
///
/// The `shrinkOffset` is a distance from [maxExtent] towards [minExtent]
/// representing the current amount by which the sliver has been shrunk. When
/// the `shrinkOffset` is zero, the contents will be rendered with a dimension
/// of [maxExtent] in the main axis. When `shrinkOffset` equals the difference
/// between [maxExtent] and [minExtent] (a positive number), the contents will
/// be rendered with a dimension of [minExtent] in the main axis. The
/// `shrinkOffset` will always be a positive number in that range.
///
/// The `overlapsContent` argument is true if subsequent slivers (if any) will
/// be rendered beneath this one, and false if the sliver will not have any
/// contents below it. Typically this is used to decide whether to draw a
/// shadow to simulate the sliver being above the contents below it. Typically
/// this is true when `shrinkOffset` is at its greatest value and false
/// otherwise, but that is not guaranteed. See [NestedScrollView] for an
/// example of a case where `overlapsContent`'s value can be unrelated to
/// `shrinkOffset`.
Widget build(BuildContext context, double shrinkOffset, bool overlapsContent);
/// The smallest size to allow the header to reach, when it shrinks at the
/// start of the viewport.
///
/// This must return a value equal to or less than [maxExtent].
///
/// This value should not change over the lifetime of the delegate. It should
/// be based entirely on the constructor arguments passed to the delegate. See
/// [shouldRebuild], which must return true if a new delegate would return a
/// different value.
double get minExtent;
/// The size of the header when it is not shrinking at the top of the
/// viewport.
///
/// This must return a value equal to or greater than [minExtent].
///
/// This value should not change over the lifetime of the delegate. It should
/// be based entirely on the constructor arguments passed to the delegate. See
/// [shouldRebuild], which must return true if a new delegate would return a
/// different value.
double get maxExtent;
bool shouldRebuild(covariant SliverPersistentHeaderDelegate oldDelegate);
/// Specifies how floating headers should animate in and out of view.
///
/// If the value of this property is null, then floating headers will
/// not animate into place.
@protected
///
/// This is only used for floating headers (those with
/// [SliverPersistentHeader.floating] set to true).
///
/// Defaults to null.
FloatingHeaderSnapConfiguration get snapConfiguration => null;
/// Whether this delegate is meaningfully different from the old delegate.
///
/// If this returns false, then the header might not be rebuilt, even though
/// the instance of the delegate changed.
///
/// This must return true if `oldDelegate` and this object would return
/// different values for [minExtent], [maxExtent], [snapConfiguration], or
/// would return a meaningfully different widget tree from [build] for the
/// same arguments.
bool shouldRebuild(covariant SliverPersistentHeaderDelegate oldDelegate);
}
/// A sliver whose size varies when the sliver is scrolled to the leading edge
/// of the viewport.
///
/// This is the layout primitive that [SliverAppBar] uses for its
/// shrinking/growing effect.
class SliverPersistentHeader extends StatelessWidget {
/// Creates a sliver that varies its size when it is scrolled to the start of
/// a viewport.
///
/// The [delegate], [pinned], and [floating] arguments must not be null.
const SliverPersistentHeader({
Key key,
@required this.delegate,
......@@ -39,10 +100,32 @@ class SliverPersistentHeader extends StatelessWidget {
assert(floating != null),
super(key: key);
/// Configuration for the sliver's layout.
///
/// The delegate provides the following information:
///
/// * The minimum and maximum dimensions of the sliver.
///
/// * The builder for generating the widgets of the sliver.
///
/// * The instructions for snapping the scroll offset, if [floating] is true.
final SliverPersistentHeaderDelegate delegate;
/// Whether to stick the header to the start of the viewport once it has
/// reached its minimum size.
///
/// If this is false, the header will continue scrolling off the screen after
/// it has shrunk to its minimum extent.
final bool pinned;
/// Whether the header should immediately grow again if the user reverses
/// scroll direction.
///
/// If this is false, the header only grows again once the user reaches the
/// part of the viewport that contains the sliver.
///
/// The [delegate]'s [SliverPersistentHeaderDelegate.snapConfiguration] is
/// ignored unless [floating] is true.
final bool floating;
@override
......
......@@ -113,11 +113,15 @@ class FlutterDriver {
/// Creates a driver that uses a connection provided by the given
/// [_serviceClient], [_peer] and [_appIsolate].
@visibleForTesting
FlutterDriver.connectedTo(this._serviceClient, this._peer, this._appIsolate,
{ bool printCommunication: false, bool logCommunicationToFile: true })
: _printCommunication = printCommunication,
_logCommunicationToFile = logCommunicationToFile,
_driverId = _nextDriverId++;
FlutterDriver.connectedTo(
this._serviceClient,
this._peer,
this._appIsolate, {
bool printCommunication: false,
bool logCommunicationToFile: true,
}) : _printCommunication = printCommunication,
_logCommunicationToFile = logCommunicationToFile,
_driverId = _nextDriverId++;
static const String _kFlutterExtensionMethod = 'ext.flutter.driver';
static const String _kSetVMTimelineFlagsMethod = '_setVMTimelineFlags';
......
......@@ -71,7 +71,7 @@ enum LogLevel {
critical,
}
/// A log entry.
/// A log entry, as emitted on [flutterDriverLog].
class LogRecord {
const LogRecord._(this.level, this.loggerName, this.message);
......
......@@ -28,6 +28,10 @@ import 'semantics.dart';
const String _extensionMethodName = 'driver';
const String _extensionMethod = 'ext.flutter.$_extensionMethodName';
/// Signature for the handler passed to [enableFlutterDriverExtension].
///
/// Messages are described in string form and should return a [Future] which
/// eventually completes to a string response.
typedef Future<String> DataHandler(String message);
class _DriverBinding extends BindingBase with SchedulerBinding, GestureBinding, ServicesBinding, RendererBinding, WidgetsBinding {
......@@ -72,8 +76,14 @@ typedef Command CommandDeserializerCallback(Map<String, String> params);
/// found, if any, or null otherwise.
typedef Finder FinderConstructor(SerializableFinder finder);
/// The class that manages communication between a Flutter Driver test and the
/// application being remote-controlled, on the application side.
///
/// This is not normally used directly. It is instantiated automatically when
/// calling [enableFlutterDriverExtension].
@visibleForTesting
class FlutterDriverExtension {
/// Creates an object to manage a Flutter Driver connection.
FlutterDriverExtension(this._requestDataHandler) {
_commandHandlers.addAll(<String, CommandHandlerCallback>{
'get_health': _getHealth,
......
......@@ -13,7 +13,7 @@ DriverError _createInvalidKeyValueTypeError(String invalidType) {
return new DriverError('Unsupported key value type $invalidType. Flutter Driver only supports ${_supportedKeyValueTypes.join(", ")}');
}
/// A command aimed at an object to be located by [finder].
/// A Flutter Driver command aimed at an object to be located by [finder].
///
/// Implementations must provide a concrete [kind]. If additional data is
/// required beyond the [finder] the implementation may override [serialize]
......@@ -25,7 +25,7 @@ abstract class CommandWithTarget extends Command {
throw new DriverError('$runtimeType target cannot be null');
}
/// Deserializes the command from JSON generated by [serialize].
/// Deserializes this command from the value generated by [serialize].
CommandWithTarget.deserialize(Map<String, String> json)
: finder = SerializableFinder.deserialize(json),
super.deserialize(json);
......@@ -46,11 +46,8 @@ abstract class CommandWithTarget extends Command {
super.serialize()..addAll(finder.serialize());
}
/// Waits until [finder] can locate the target.
/// A Flutter Driver command that waits until [finder] can locate the target.
class WaitFor extends CommandWithTarget {
@override
final String kind = 'waitFor';
/// Creates a command that waits for the widget identified by [finder] to
/// appear within the [timeout] amount of time.
///
......@@ -58,15 +55,26 @@ class WaitFor extends CommandWithTarget {
WaitFor(SerializableFinder finder, {Duration timeout})
: super(finder, timeout: timeout);
/// Deserializes the command from JSON generated by [serialize].
/// Deserializes this command from the value generated by [serialize].
WaitFor.deserialize(Map<String, String> json) : super.deserialize(json);
@override
final String kind = 'waitFor';
}
/// Waits until [finder] can no longer locate the target.
class WaitForAbsent extends CommandWithTarget {
/// The result of a [WaitFor] command.
class WaitForResult extends Result {
/// Deserializes the result from JSON.
static WaitForResult fromJson(Map<String, dynamic> json) {
return new WaitForResult();
}
@override
final String kind = 'waitForAbsent';
Map<String, dynamic> toJson() => <String, dynamic>{};
}
/// A Flutter Driver command that waits until [finder] can no longer locate the target.
class WaitForAbsent extends CommandWithTarget {
/// Creates a command that waits for the widget identified by [finder] to
/// disappear within the [timeout] amount of time.
///
......@@ -74,31 +82,11 @@ class WaitForAbsent extends CommandWithTarget {
WaitForAbsent(SerializableFinder finder, {Duration timeout})
: super(finder, timeout: timeout);
/// Deserializes the command from JSON generated by [serialize].
/// Deserializes this command from the value generated by [serialize].
WaitForAbsent.deserialize(Map<String, String> json) : super.deserialize(json);
}
/// Waits until there are no more transient callbacks in the queue.
class WaitUntilNoTransientCallbacks extends Command {
@override
final String kind = 'waitUntilNoTransientCallbacks';
WaitUntilNoTransientCallbacks({Duration timeout}) : super(timeout: timeout);
/// Deserializes the command from JSON generated by [serialize].
WaitUntilNoTransientCallbacks.deserialize(Map<String, String> json)
: super.deserialize(json);
}
/// The result of a [WaitFor] command.
class WaitForResult extends Result {
/// Deserializes the result from JSON.
static WaitForResult fromJson(Map<String, dynamic> json) {
return new WaitForResult();
}
@override
Map<String, dynamic> toJson() => <String, dynamic>{};
final String kind = 'waitForAbsent';
}
/// The result of a [WaitForAbsent] command.
......@@ -112,11 +100,34 @@ class WaitForAbsentResult extends Result {
Map<String, dynamic> toJson() => <String, dynamic>{};
}
/// Describes how to the driver should search for elements.
/// A Flutter Driver command that waits until there are no more transient callbacks in the queue.
class WaitUntilNoTransientCallbacks extends Command {
/// Creates a command that waits for there to be no transient callbacks.
WaitUntilNoTransientCallbacks({ Duration timeout }) : super(timeout: timeout);
/// Deserializes this command from the value generated by [serialize].
WaitUntilNoTransientCallbacks.deserialize(Map<String, String> json)
: super.deserialize(json);
@override
final String kind = 'waitUntilNoTransientCallbacks';
}
/// Base class for Flutter Driver finders, objects that describe how the driver
/// should search for elements.
abstract class SerializableFinder {
/// Identifies the type of finder to be used by the driver extension.
String get finderType;
/// Serializes common fields to JSON.
///
/// Methods that override [serialize] are expected to call `super.serialize`
/// and add more fields to the returned [Map].
@mustCallSuper
Map<String, String> serialize() => <String, String>{
'finderType': finderType,
};
/// Deserializes a finder from JSON generated by [serialize].
static SerializableFinder deserialize(Map<String, String> json) {
final String finderType = json['finderType'];
......@@ -128,28 +139,19 @@ abstract class SerializableFinder {
}
throw new DriverError('Unsupported search specification type $finderType');
}
/// Serializes common fields to JSON.
///
/// Methods that override [serialize] are expected to call `super.serialize`
/// and add more fields to the returned [Map].
@mustCallSuper
Map<String, String> serialize() => <String, String>{
'finderType': finderType,
};
}
/// Finds widgets by tooltip text.
/// A Flutter Driver finder that finds widgets by tooltip text.
class ByTooltipMessage extends SerializableFinder {
@override
final String finderType = 'ByTooltipMessage';
/// Creates a tooltip finder given the tooltip's message [text].
ByTooltipMessage(this.text);
/// Tooltip message text.
final String text;
@override
final String finderType = 'ByTooltipMessage';
@override
Map<String, String> serialize() => super.serialize()..addAll(<String, String>{
'text': text,
......@@ -161,17 +163,17 @@ class ByTooltipMessage extends SerializableFinder {
}
}
/// Finds widgets by [text] inside a `Text` widget.
/// A Flutter Driver finder that finds widgets by [text] inside a `Text` widget.
class ByText extends SerializableFinder {
@override
final String finderType = 'ByText';
/// Creates a text finder given the text.
ByText(this.text);
/// The text that appears inside the `Text` widget.
final String text;
@override
final String finderType = 'ByText';
@override
Map<String, String> serialize() => super.serialize()..addAll(<String, String>{
'text': text,
......@@ -183,11 +185,8 @@ class ByText extends SerializableFinder {
}
}
/// Finds widgets by `ValueKey`.
/// A Flutter Driver finder that finds widgets by `ValueKey`.
class ByValueKey extends SerializableFinder {
@override
final String finderType = 'ByValueKey';
/// Creates a finder given the key value.
ByValueKey(this.keyValue)
: this.keyValueString = '$keyValue',
......@@ -207,6 +206,9 @@ class ByValueKey extends SerializableFinder {
/// May be one of "String", "int". The list of supported types may change.
final String keyValueType;
@override
final String finderType = 'ByValueKey';
@override
Map<String, String> serialize() => super.serialize()..addAll(<String, String>{
'keyValueString': keyValueString,
......@@ -228,17 +230,17 @@ class ByValueKey extends SerializableFinder {
}
}
/// Finds widgets by their [runtimeType].
/// A Flutter Driver finder that finds widgets by their [runtimeType].
class ByType extends SerializableFinder {
@override
final String finderType = 'ByType';
/// Creates a finder that given the runtime type in string form.
ByType(this.type);
/// The widget's [runtimeType], in string form.
final String type;
@override
final String finderType = 'ByType';
@override
Map<String, String> serialize() => super.serialize()..addAll(<String, String>{
'type': type,
......@@ -250,16 +252,16 @@ class ByType extends SerializableFinder {
}
}
/// Command to read the text from a given element.
/// A Flutter Driver command that reads the text from a given element.
class GetText extends CommandWithTarget {
@override
final String kind = 'get_text';
/// [finder] looks for an element that contains a piece of text.
GetText(SerializableFinder finder, { Duration timeout }) : super(finder, timeout: timeout);
/// Deserializes the command from JSON generated by [serialize].
/// Deserializes this command from the value generated by [serialize].
GetText.deserialize(Map<String, dynamic> json) : super.deserialize(json);
@override
final String kind = 'get_text';
}
/// The result of the [GetText] command.
......
......@@ -4,21 +4,22 @@
import 'message.dart';
/// Enables or disables the FrameSync mechanism.
/// A Flutter Driver command that enables or disables the FrameSync mechanism.
class SetFrameSync extends Command {
@override
final String kind = 'set_frame_sync';
/// Creates a command to toggle the FrameSync mechanism.
SetFrameSync(this.enabled, { Duration timeout }) : super(timeout: timeout);
/// Whether frameSync should be enabled or disabled.
final bool enabled;
/// Deserializes this command from the value generated by [serialize].
SetFrameSync.deserialize(Map<String, String> params)
: this.enabled = params['enabled'].toLowerCase() == 'true',
super.deserialize(params);
/// Whether frameSync should be enabled or disabled.
final bool enabled;
@override
final String kind = 'set_frame_sync';
@override
Map<String, String> serialize() => super.serialize()..addAll(<String, String>{
'enabled': '$enabled',
......
......@@ -5,16 +5,16 @@
import 'find.dart';
import 'message.dart';
/// Taps on a target widget located by [finder].
/// A Flutter Driver command that taps on a target widget located by [finder].
class Tap extends CommandWithTarget {
@override
final String kind = 'tap';
/// Creates a tap command to tap on a widget located by [finder].
Tap(SerializableFinder finder, {Duration timeout}) : super(finder, timeout: timeout);
Tap(SerializableFinder finder, { Duration timeout }) : super(finder, timeout: timeout);
/// Deserializes this command from JSON generated by [serialize].
/// Deserializes this command from the value generated by [serialize].
Tap.deserialize(Map<String, String> json) : super.deserialize(json);
@override
final String kind = 'tap';
}
/// The result of a [Tap] command.
......@@ -29,11 +29,8 @@ class TapResult extends Result {
}
/// Command the driver to perform a scrolling action.
/// A Flutter Driver command that commands the driver to perform a scrolling action.
class Scroll extends CommandWithTarget {
@override
final String kind = 'scroll';
/// Creates a scroll command that will attempt to scroll a scrollable view by
/// dragging a widget located by the given [finder].
Scroll(
......@@ -41,11 +38,11 @@ class Scroll extends CommandWithTarget {
this.dx,
this.dy,
this.duration,
this.frequency,
{Duration timeout}
) : super(finder, timeout: timeout);
this.frequency, {
Duration timeout,
}) : super(finder, timeout: timeout);
/// Deserializes this command from JSON generated by [serialize].
/// Deserializes this command from the value generated by [serialize].
Scroll.deserialize(Map<String, String> json)
: this.dx = double.parse(json['dx']),
this.dy = double.parse(json['dy']),
......@@ -65,6 +62,9 @@ class Scroll extends CommandWithTarget {
/// The frequency in Hz of the generated move events.
final int frequency;
@override
final String kind = 'scroll';
@override
Map<String, String> serialize() => super.serialize()..addAll(<String, String>{
'dx': '$dx',
......@@ -85,23 +85,29 @@ class ScrollResult extends Result {
Map<String, dynamic> toJson() => <String, dynamic>{};
}
/// Command the driver to ensure that the element represented by [finder]
/// has been scrolled completely into view.
/// A Flutter Driver command that commands the driver to ensure that the element
/// represented by [finder] has been scrolled completely into view.
class ScrollIntoView extends CommandWithTarget {
@override
final String kind = 'scrollIntoView';
/// Creates this command given a [finder] used to locate the widget to be
/// scrolled into view.
ScrollIntoView(SerializableFinder finder, { this.alignment: 0.0, Duration timeout }) : super(finder, timeout: timeout);
/// Deserializes this command from JSON generated by [serialize].
/// Deserializes this command from the value generated by [serialize].
ScrollIntoView.deserialize(Map<String, String> json)
: this.alignment = double.parse(json['alignment']),
super.deserialize(json);
/// How the widget should be aligned.
///
/// This value is passed to [Scrollable.ensureVisible] as the value of its
/// argument of the same name.
///
/// Defaults to 0.0.
final double alignment;
@override
final String kind = 'scrollIntoView';
@override
Map<String, String> serialize() => super.serialize()..addAll(<String, String>{
'alignment': '$alignment',
......
......@@ -5,16 +5,16 @@
import 'enum_util.dart';
import 'message.dart';
/// Requests an application health check.
/// A Flutter Driver command that requests an application health check.
class GetHealth extends Command {
@override
final String kind = 'get_health';
/// Create a health check command.
GetHealth({Duration timeout}) : super(timeout: timeout);
GetHealth({ Duration timeout }) : super(timeout: timeout);
/// Deserializes the command from JSON generated by [serialize].
/// Deserializes this command from the value generated by [serialize].
GetHealth.deserialize(Map<String, String> json) : super.deserialize(json);
@override
final String kind = 'get_health';
}
/// A description of application state.
......@@ -37,16 +37,16 @@ class Health extends Result {
assert(status != null);
}
/// Deserializes the result from JSON.
static Health fromJson(Map<String, dynamic> json) {
return new Health(_healthStatusIndex.lookupBySimpleName(json['status']));
}
/// The status represented by this object.
///
/// If the application responded, this will be [HealthStatus.ok].
final HealthStatus status;
/// Deserializes the result from JSON.
static Health fromJson(Map<String, dynamic> json) {
return new Health(_healthStatusIndex.lookupBySimpleName(json['status']));
}
@override
Map<String, dynamic> toJson() => <String, dynamic>{
'status': _healthStatusIndex.toSimpleName(status),
......
......@@ -10,8 +10,9 @@ MatchResult match(dynamic value, Matcher matcher) {
if (matcher.matches(value, matchState)) {
return new MatchResult._matched();
} else {
final Description description =
matcher.describeMismatch(value, new _TextDescription(), matchState, false);
final Description description = matcher.describeMismatch(
value, new _TextDescription(), matchState, false,
);
return new MatchResult._mismatched(description.toString());
}
}
......
......@@ -7,9 +7,12 @@ import 'package:meta/meta.dart';
/// An object sent from the Flutter Driver to a Flutter application to instruct
/// the application to perform a task.
abstract class Command {
Command({Duration timeout})
/// Abstract const constructor. This constructor enables subclasses to provide
/// const constructors so that they can be used in const expressions.
const Command({ Duration timeout })
: this.timeout = timeout ?? const Duration(seconds: 5);
/// Deserializes this command from the value generated by [serialize].
Command.deserialize(Map<String, String> json)
: timeout = new Duration(milliseconds: int.parse(json['timeout']));
......
......@@ -4,30 +4,31 @@
import 'message.dart';
/// A request for a string representation of the render tree.
/// A Flutter Driver command that requests a string representation of the render tree.
class GetRenderTree extends Command {
@override
final String kind = 'get_render_tree';
GetRenderTree({Duration timeout}) : super(timeout: timeout);
/// Create a command to request a string representation of the render tree.
GetRenderTree({ Duration timeout }) : super(timeout: timeout);
/// Deserializes the command from JSON generated by [serialize].
/// Deserializes this command from the value generated by [serialize].
GetRenderTree.deserialize(Map<String, String> json) : super.deserialize(json);
@override
final String kind = 'get_render_tree';
}
/// A string representation of the render tree.
/// A string representation of the render tree, the result of a [GetRenderTree] command.
class RenderTree extends Result {
/// Creates a [RenderTree] object with the given string representation.
RenderTree(this.tree);
/// String representation of the render tree.
final String tree;
/// Deserializes the result from JSON.
static RenderTree fromJson(Map<String, dynamic> json) {
return new RenderTree(json['tree']);
}
/// String representation of the render tree.
final String tree;
@override
Map<String, dynamic> toJson() => <String, dynamic>{
'tree': tree
......
......@@ -4,22 +4,23 @@
import 'message.dart';
/// Send a string and get a string response.
/// A Flutter Driver command that sends a string to the application and expects a
/// string response.
class RequestData extends Command {
@override
final String kind = 'request_data';
/// Create a command that sends a message.
RequestData(this.message, { Duration timeout }) : super(timeout: timeout);
/// The message being sent from the test to the application.
final String message;
/// Deserializes this command from the value generated by [serialize].
RequestData.deserialize(Map<String, String> params)
: this.message = params['message'],
super.deserialize(params);
/// The message being sent from the test to the application.
final String message;
@override
final String kind = 'request_data';
@override
Map<String, String> serialize() => super.serialize()..addAll(<String, String>{
'message': message,
......
......@@ -17,8 +17,12 @@ typedef bool Predicate(dynamic value);
///
/// When the retry time out, the last seen error and stack trace are returned in
/// an error [Future].
Future<dynamic> retry(Action action, Duration timeout,
Duration pauseBetweenRetries, { Predicate predicate }) async {
Future<dynamic> retry(
Action action,
Duration timeout,
Duration pauseBetweenRetries, {
Predicate predicate,
}) async {
assert(action != null);
assert(timeout != null);
assert(pauseBetweenRetries != null);
......@@ -29,7 +33,7 @@ Future<dynamic> retry(Action action, Duration timeout,
dynamic lastStackTrace;
bool success = false;
while(!success && sw.elapsed < timeout) {
while (!success && sw.elapsed < timeout) {
try {
result = await action();
if (predicate == null || predicate(result))
......
......@@ -4,21 +4,22 @@
import 'message.dart';
/// Enables or disables semantics.
/// A Flutter Driver command that enables or disables semantics.
class SetSemantics extends Command {
@override
final String kind = 'set_semantics';
/// Creates a command that enables or disables semantics.
SetSemantics(this.enabled, { Duration timeout }) : super(timeout: timeout);
/// Whether semantics should be enabled or disabled.
final bool enabled;
/// Deserializes this command from the value generated by [serialize].
SetSemantics.deserialize(Map<String, String> params)
: this.enabled = params['enabled'].toLowerCase() == 'true',
super.deserialize(params);
/// Whether semantics should be enabled (true) or disabled (false).
final bool enabled;
@override
final String kind = 'set_semantics';
@override
Map<String, String> serialize() => super.serialize()..addAll(<String, String>{
'enabled': '$enabled',
......@@ -27,8 +28,11 @@ class SetSemantics extends Command {
/// The result of a [SetSemantics] command.
class SetSemanticsResult extends Result {
/// Create a result with the given [changedState].
SetSemanticsResult(this.changedState);
/// Whether the [SetSemantics] command actually changed the state that the
/// application was in.
final bool changedState;
/// Deserializes this result from JSON.
......
......@@ -89,8 +89,11 @@ class TimelineSummary {
}
/// Writes all of the recorded timeline data to a file.
Future<Null> writeTimelineToFile(String traceName,
{String destinationDirectory, bool pretty: false}) async {
Future<Null> writeTimelineToFile(
String traceName, {
String destinationDirectory,
bool pretty: false,
}) async {
destinationDirectory ??= testOutputsDirectory;
await fs.directory(destinationDirectory).create(recursive: true);
final File file = fs.file(path.join(destinationDirectory, '$traceName.timeline.json'));
......@@ -98,8 +101,11 @@ class TimelineSummary {
}
/// Writes [summaryJson] to a file.
Future<Null> writeSummaryToFile(String traceName,
{String destinationDirectory, bool pretty: false}) async {
Future<Null> writeSummaryToFile(
String traceName, {
String destinationDirectory,
bool pretty: false,
}) async {
destinationDirectory ??= testOutputsDirectory;
await fs.directory(destinationDirectory).create(recursive: true);
final File file = fs.file(path.join(destinationDirectory, '$traceName.timeline_summary.json'));
......@@ -174,6 +180,10 @@ class TimelineSummary {
/// Timing information about an event that happened in the event loop.
class TimedEvent {
/// Creates a timed event given begin and end timestamps in microseconds.
TimedEvent(this.beginTimeMicros, this.endTimeMicros)
: this.duration = new Duration(microseconds: endTimeMicros - beginTimeMicros);
/// The timestamp when the event began.
final int beginTimeMicros;
......@@ -182,8 +192,4 @@ class TimedEvent {
/// The duration of the event.
final Duration duration;
/// Creates a timed event given begin and end timestamps in microseconds.
TimedEvent(this.beginTimeMicros, this.endTimeMicros)
: this.duration = new Duration(microseconds: endTimeMicros - beginTimeMicros);
}
......@@ -98,9 +98,20 @@ abstract class TestWidgetsFlutterBinding extends BindingBase
debugCheckIntrinsicSizes = checkIntrinsicSizes;
}
/// The value to set [debugPrint] to while tests are running.
///
/// This can be used to redirect console output from the framework, or to
/// change the behavior of [debugPrint]. For example,
/// [AutomatedTestWidgetsFlutterBinding] uses it to make [debugPrint]
/// synchronous, disabling its normal throttling behaviour.
@protected
DebugPrintCallback get debugPrintOverride => debugPrint;
/// The value to set [debugCheckIntrinsicSizes] to while tests are running.
///
/// This can be used to enable additional checks. For example,
/// [AutomatedTestWidgetsFlutterBinding] sets this to true, so that all tests
/// always run with aggressive intrinsic sizing tests enabled.
@protected
bool get checkIntrinsicSizes => false;
......
......@@ -27,6 +27,13 @@ class TestTextInput {
}
int _client = 0;
/// The last set of arguments that [TextInputConnection.setEditingState] sent
/// to the embedder.
///
/// This is a map representation of a [TextEditingValue] object. For example,
/// it will have a `text` entry whose value matches the most recent
/// [TextEditingValue.text] that was sent to the embedder.
Map<String, dynamic> editingState;
Future<dynamic> _handleTextInputCall(MethodCall methodCall) async {
......
......@@ -4,7 +4,7 @@
import 'dart:convert';
import 'package:collection/collection.dart';
import 'package:collection/collection.dart' show ListEquality;
import 'package:mockito/mockito.dart';
import 'package:process/process.dart';
import 'package:quiver/time.dart';
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment