Unverified Commit 343b5703 authored by Hans Muller's avatar Hans Muller Committed by GitHub

InheritedModel - an InheritedWidget for data models (#19739)

Added InheritedModel:  An InheritedWidget that's intended to be used as the base class for models whose dependents may only depend on one part or "aspect" of the overall model.
parent a74f591d
......@@ -1870,6 +1870,19 @@ abstract class BuildContext {
/// render object is usually short.
Size get size;
/// Registers this build context with [ancestor] such that when
/// [ancestor]'s widget changes this build context is rebuilt.
///
/// Returns `ancestor.widget`.
///
/// This method is rarely called directly. Most applications should use
/// [inheritFromWidgetOfExactType], which calls this method after finding
/// the appropriate [InheritedElement] ancestor.
///
/// All of the qualifications about when [inheritFromWidgetOfExactType] can
/// be called apply to this method as well.
InheritedWidget inheritFromElement(InheritedElement ancestor, { Object aspect });
/// 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
......@@ -1879,7 +1892,7 @@ abstract class BuildContext {
/// This is typically called implicitly from `of()` static methods, e.g.
/// [Theme.of].
///
/// This should not be called from widget constructors or from
/// This method 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
......@@ -1892,9 +1905,9 @@ abstract class BuildContext {
/// It is safe to use this method from [State.deactivate], which is called
/// whenever the widget is removed from the tree.
///
/// 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.
/// It is also possible to call this method 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.
......@@ -1904,7 +1917,12 @@ abstract class BuildContext {
/// called, whenever changes occur relating to that widget until the next time
/// the widget or one of its ancestors is moved (for example, because an
/// ancestor is added or removed).
InheritedWidget inheritFromWidgetOfExactType(Type targetType);
///
/// The [aspect] parameter is only used when [targetType] is an
/// [InheritedWidget] subclasses that supports partial updates, like
/// [InheritedModel]. It specifies what "aspect" of the inherited
/// widget this context depends on.
InheritedWidget inheritFromWidgetOfExactType(Type targetType, { Object aspect });
/// Obtains the element corresponding to the nearest widget of the given type,
/// which must be the type of a concrete [InheritedWidget] subclass.
......@@ -3226,15 +3244,31 @@ abstract class Element extends DiagnosticableTree implements BuildContext {
}
@override
InheritedWidget inheritFromWidgetOfExactType(Type targetType) {
InheritedWidget inheritFromElement(InheritedElement ancestor, { Object aspect }) {
assert(ancestor != null);
assert(() {
if (_parent == null) {
// We're being deactivated, see deactivateChild()
return true;
}
Element element = _parent;
while (ancestor != element && element != null)
element = element._parent;
return ancestor == element;
}());
_dependencies ??= new HashSet<InheritedElement>();
_dependencies.add(ancestor);
ancestor.updateDependencies(this, aspect);
return ancestor.widget;
}
@override
InheritedWidget inheritFromWidgetOfExactType(Type targetType, { Object aspect }) {
assert(_debugCheckStateIsActiveForAncestorLookup());
final InheritedElement ancestor = _inheritedWidgets == null ? null : _inheritedWidgets[targetType];
if (ancestor != null) {
assert(ancestor is InheritedElement);
_dependencies ??= new HashSet<InheritedElement>();
_dependencies.add(ancestor);
ancestor._dependents.add(this);
return ancestor.widget;
return inheritFromElement(ancestor, aspect: aspect);
}
_hadUnsatisfiedDependencies = true;
return null;
......@@ -3845,11 +3879,13 @@ class StatefulElement extends ComponentElement {
}
@override
InheritedWidget inheritFromWidgetOfExactType(Type targetType) {
InheritedWidget inheritFromElement(Element ancestor, { Object aspect }) {
assert(ancestor != null);
assert(() {
final Type targetType = ancestor.widget.runtimeType;
if (state._debugLifecycleState == _StateLifecycle.created) {
throw new FlutterError(
'inheritFromWidgetOfExactType($targetType) was called before ${_state.runtimeType}.initState() completed.\n'
'inheritFromWidgetOfExactType($targetType) or inheritFromElement() was called before ${_state.runtimeType}.initState() completed.\n'
'When an inherited widget changes, for example if the value of Theme.of() changes, '
'its dependent widgets are rebuilt. If the dependent widget\'s reference to '
'the inherited widget is in a constructor or an initState() method, '
......@@ -3862,7 +3898,7 @@ class StatefulElement extends ComponentElement {
}
if (state._debugLifecycleState == _StateLifecycle.defunct) {
throw new FlutterError(
'inheritFromWidgetOfExactType($targetType) called after dispose(): $this\n'
'inheritFromWidgetOfExactType($targetType) or inheritFromElement() was called after dispose(): $this\n'
'This error happens if you call inheritFromWidgetOfExactType() on the '
'BuildContext for a widget that no longer appears in the widget tree '
'(e.g., whose parent widget no longer includes the widget in its '
......@@ -3882,7 +3918,7 @@ class StatefulElement extends ComponentElement {
}
return true;
}());
return super.inheritFromWidgetOfExactType(targetType);
return super.inheritFromElement(ancestor, aspect: aspect);
}
@override
......@@ -4032,7 +4068,7 @@ class InheritedElement extends ProxyElement {
@override
InheritedWidget get widget => super.widget;
final Set<Element> _dependents = new HashSet<Element>();
final Map<Element, Object> _dependents = new HashMap<Element, Object>();
@override
void _updateInheritance() {
......@@ -4054,6 +4090,110 @@ class InheritedElement extends ProxyElement {
super.debugDeactivated();
}
/// Returns the dependencies value recorded for [dependent]
/// with [setDependencies].
///
/// Each dependent element is mapped to a single object value
/// which represents how the element depends on this
/// [InheritedElement]. This value is null by default and by default
/// dependent elements are rebuilt unconditionally.
///
/// Subclasses can manage these values with [updateDependencies]
/// so that they can selectively rebuild dependents in
/// [notifyDependents].
///
/// This method is typically only called in overrides of [updateDependencies].
///
/// See also:
///
/// * [updateDependencies], which is called each time a dependency is
/// created with [inheritFromWidgetOfExactType].
/// * [setDependencies], which sets dependencies value for a dependent
/// element.
/// * [notifyDependent], which can be overridden to use a dependent's
/// dependencies value to decide if the dependent needs to be rebuilt.
/// * [InheritedModel], which is an example of a class that uses this method
/// to manage dependency values.
@protected
Object getDependencies(Element dependent) {
return _dependents[dependent];
}
/// Sets the value returned by [getDependencies] value for [dependent].
///
/// Each dependent element is mapped to a single object value
/// which represents how the element depends on this
/// [InheritedElement]. The [updateDependencies] method sets this value to
/// null by default so that dependent elements are rebuilt unconditionally.
///
/// Subclasses can manage these values with [updateDependencies]
/// so that they can selectively rebuild dependents in [notifyDependents].
///
/// This method is typically only called in overrides of [updateDependencies].
///
/// See also:
///
/// * [updateDependencies], which is called each time a dependency is
/// created with [inheritFromWidgetOfExactType].
/// * [getDependencies], which returns the current value for a dependent
/// element.
/// * [notifyDependent], which can be overridden to use a dependent's
/// [getDependencies] value to decide if the dependent needs to be rebuilt.
/// * [InheritedModel], which is an example of a class that uses this method
/// to manage dependency values.
@protected
void setDependencies(Element dependent, Object value) {
_dependents[dependent] = value;
}
/// Called by [inheritFromWidgetOfExactType] when a new [dependent] is added.
///
/// Each dependent element can be mapped to a single object value with
/// [setDependencies]. This method can lookup the existing dependencies with
/// [getDependencies].
///
/// By default this method sets the inherited dependencies for [dependent]
/// to null. This only serves to record an unconditional dependency on
/// [dependent].
///
/// Subclasses can manage their own dependencies values so that they
/// can selectively rebuild dependents in [notifyDependents].
///
/// See also:
///
/// * [getDependencies], which returns the current value for a dependent
/// element.
/// * [setDependencies], which sets the value for a dependent element.
/// * [notifyDependent], which can be overridden to use a dependent's
/// dependencies value to decide if the dependent needs to be rebuilt.
/// * [InheritedModel], which is an example of a class that uses this method
/// to manage dependency values.
@protected
void updateDependencies(Element dependent, Object aspect) {
setDependencies(dependent, null);
}
/// Called by [notifyClients] for each dependent.
///
/// Calls `dependent.didChangeDependencies()` by default.
///
/// Subclasses can override this method to selectively call
/// [didChangeDependencies] based on the value of [getDependencies].
///
/// See also:
///
/// * [updateDependencies], which is called each time a dependency is
/// created with [inheritFromWidgetOfExactType].
/// * [getDependencies], which returns the current value for a dependent
/// element.
/// * [setDependencies], which sets the value for a dependent element.
/// * [InheritedModel], which is an example of a class that uses this method
/// to manage dependency values.
@protected
void notifyDependent(covariant InheritedWidget oldWidget, Element dependent) {
dependent.didChangeDependencies();
}
/// Calls [Element.didChangeDependencies] of all dependent elements, if
/// [InheritedWidget.updateShouldNotify] returns true.
///
......@@ -4070,7 +4210,7 @@ class InheritedElement extends ProxyElement {
if (!widget.updateShouldNotify(oldWidget))
return;
assert(_debugCheckOwnerBuildTargetExists('notifyClients'));
for (Element dependent in _dependents) {
for (Element dependent in _dependents.keys) {
assert(() {
// check that it really is our descendant
Element ancestor = dependent._parent;
......@@ -4080,7 +4220,7 @@ class InheritedElement extends ProxyElement {
}());
// check that it really depends on us
assert(dependent._dependencies.contains(this));
dependent.didChangeDependencies();
notifyDependent(oldWidget, dependent);
}
}
}
......
// Copyright 2018 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.
import 'dart:collection';
import 'package:flutter/foundation.dart';
import 'framework.dart';
/// An [InheritedWidget] that's intended to be used as the base class for
/// models whose dependents may only depend on one part or "aspect" of the
/// overall model.
///
/// An inherited widget's dependents are unconditionally rebuilt when the
/// inherited widget changes per [InheritedWidget.updateShouldNotify].
/// This widget is similar except that dependents aren't rebuilt
/// unconditionally.
///
/// Widgets that depend on an [InheritedModel] qualify their dependence
/// with a value that indicates what "aspect" of the model they depend
/// on. When the model is rebuilt, dependents will also be rebuilt, but
/// only if there was a change in the model that corresponds to the aspect
/// they provided.
///
/// The type parameter `T` is the type of the model aspect objects.
///
/// Widgets create a dependency on an [InheritedModel] with a static method:
/// [InheritedModel.inheritFrom]. This method's `context` parameter
/// defines the subtree that will be rebuilt when the model changes.
/// Typically the `inheritFrom` method is called from a model-specific
/// static `of` method. For example:
///
/// ```dart
/// class MyModel extends InheritedModel<String> {
/// // ...
/// static MyModel of(BuildContext context, String aspect) {
/// return InheritedModel.inheritFrom<MyModel>(context, aspect: aspect);
/// }
/// }
/// ```
///
/// Calling `MyModel.of(context, 'foo')` means that `context` should only
/// be rebuilt when the `foo` aspect of `MyModel` changes. If the aspect
/// is null, then the model supports all aspects.
///
/// When the inherited model is rebuilt the [updateShouldNotify] and
/// [updateShouldNotifyDependent] methods are used to decide what
/// should be rebuilt. If [updateShouldNotify] returns true, then the
/// inherited model's [updateShouldNotifyDependent] method is tested for
/// each dependent and the set of aspect objects it depends on.
/// The [updateShouldNotifyDependent] method must compare the set of aspect
/// dependencies with the changes in the model itself.
///
/// For example:
///
/// ```dart
/// class ABModel extends InheritedModel<String> {
/// ABModel({ this.a, this.b, Widget child }) : super(child: child);
///
/// final int a;
/// final int b;
///
/// @override
/// bool updateShouldNotify(ABCModel old) {
/// return return super.updateShouldNotify(old) || a != old.a || b != old.b;
/// }
///
/// @override
/// bool updateShouldNotifyDependent(ABCModel old, Set<String> dependencies) {
/// return (a != old.a && dependencies.contains('a'))
/// || (b != old.b && dependencies.contains('b'))
/// }
///
/// // ...
/// }
/// ```
///
/// In the previous example the dependencies checked by
/// [updateShouldNotifyDependent] are just the aspect strings passed to
/// `inheritFromWidgetOfExactType`. They're represented as a [Set] because
/// one Widget can depend on more than one aspect of the model.
/// If a widget depends on the model but doesn't specify an aspect,
/// then changes in the model will cause the widget to be rebuilt
/// unconditionally.
abstract class InheritedModel<T> extends InheritedWidget {
/// Creates an inherited widget that supports dependencies qualified by
/// "aspects", i.e. a descendant widget can indicate that it should
/// only be rebuilt if a specific aspect of the model changes.
const InheritedModel({ Key key, Widget child }) : super(key: key, child: child);
@override
InheritedModelElement<T> createElement() => new InheritedModelElement<T>(this);
/// Return true if the changes between this model and [oldWidget] match any
/// of the [dependencies].
@protected
bool updateShouldNotifyDependent(covariant InheritedModel<T> oldWidget, Set<T> dependencies);
/// Returns true if this model supports the given [aspect].
///
/// Returns true by default: this model supports all aspects.
///
/// Subclasses may override this method to indicate that they do not support
/// all model aspects. This is typically done when a model can be used
/// to "shadow" some aspects of an ancestor.
@protected
bool isSupportedAspect(Object aspect) => true;
// The [result] will be a list of all of context's type T ancestors concluding
// with the one that supports the specified model [aspect].
static Iterable<InheritedElement> _findModels<T extends InheritedModel<Object>>(BuildContext context, Object aspect) sync* {
final InheritedElement model = context.ancestorInheritedElementForWidgetOfExactType(T);
if (model == null)
return;
yield model;
assert(model.widget is T);
final T modelWidget = model.widget;
if (modelWidget.isSupportedAspect(aspect))
return;
Element modelParent;
model.visitAncestorElements((Element ancestor) {
modelParent = ancestor;
return false;
});
if (modelParent == null)
return;
yield* _findModels<T>(modelParent, aspect);
}
/// Makes [context] dependent on the specified [aspect] of an [InheritedModel]
/// of type T.
///
/// When the given [aspect] of the model changes, the [context] will be
/// rebuilt. The [updateShouldNotifyDependent] method must determine if a
/// change in the model widget corresponds to an [aspect] value.
///
/// The dependencies created by this method target all [InheritedModel] ancestors
/// of type T up to and including the first one for which [isSupportedAspect]
/// returns true.
///
/// If [aspect] is null this method is the same as
/// `context.inheritFromWidgetOfExactType(T)`.
static T inheritFrom<T extends InheritedModel<Object>>(BuildContext context, { Object aspect }) {
if (aspect == null)
return context.inheritFromWidgetOfExactType(T);
// Create a dependency on all of the type T ancestor models up until
// a model is found for which isSupportedAspect(aspect) is true.
final List<InheritedElement> models = _findModels<T>(context, aspect).toList();
final InheritedElement lastModel = models.last;
for (InheritedElement model in models) {
final T value = context.inheritFromElement(model, aspect: aspect);
if (model == lastModel)
return value;
}
assert(false);
return null;
}
}
/// An [Element] that uses a [InheritedModel] as its configuration.
class InheritedModelElement<T> extends InheritedElement {
/// Creates an element that uses the given widget as its configuration.
InheritedModelElement(InheritedModel<T> widget) : super(widget);
@override
InheritedModel<T> get widget => super.widget;
@override
void updateDependencies(Element dependent, Object aspect) {
final Set<T> dependencies = getDependencies(dependent);
if (dependencies != null && dependencies.isEmpty)
return;
if (aspect == null) {
setDependencies(dependent, new HashSet<T>());
} else {
assert(aspect is T);
setDependencies(dependent, (dependencies ?? new HashSet<T>())..add(aspect));
}
}
@override
void notifyDependent(InheritedModel<T> oldWidget, Element dependent) {
final Set<T> dependencies = getDependencies(dependent);
if (dependencies == null)
return;
if (dependencies.isEmpty || widget.updateShouldNotifyDependent(oldWidget, dependencies))
dependent.didChangeDependencies();
}
}
......@@ -46,6 +46,7 @@ export 'src/widgets/icon_theme_data.dart';
export 'src/widgets/image.dart';
export 'src/widgets/image_icon.dart';
export 'src/widgets/implicit_animations.dart';
export 'src/widgets/inherited_model.dart';
export 'src/widgets/layout_builder.dart';
export 'src/widgets/list_wheel_scroll_view.dart';
export 'src/widgets/localizations.dart';
......
// Copyright 2018 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.
import 'package:flutter_test/flutter_test.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
// A simple "flat" InheritedModel: the data model is just 3 integer
// valued fields: a, b, c.
class ABCModel extends InheritedModel<String> {
const ABCModel({
Key key,
this.a,
this.b,
this.c,
this.aspects,
Widget child,
}) : super(key: key, child: child);
final int a;
final int b;
final int c;
// The aspects (fields) of this model that widgets can depend on with
// inheritFrom.
//
// This property is null by default, which means that the model supports
// all 3 fields.
final Set<String> aspects;
@override
bool isSupportedAspect(Object aspect) {
return aspect == null || aspects == null || aspects.contains(aspect);
}
@override
bool updateShouldNotify(ABCModel old) {
return !setEquals<String>(aspects, old.aspects) || a != old.a || b != old.b || c != old.c;
}
@override
bool updateShouldNotifyDependent(ABCModel old, Set<String> dependencies) {
return !setEquals<String>(aspects, old.aspects)
|| (a != old.a && dependencies.contains('a'))
|| (b != old.b && dependencies.contains('b'))
|| (c != old.c && dependencies.contains('c'));
}
static ABCModel of(BuildContext context, { String fieldName }) {
return InheritedModel.inheritFrom<ABCModel>(context, aspect: fieldName);
}
}
class ShowABCField extends StatefulWidget {
const ShowABCField({ Key key, this.fieldName }) : super(key: key);
final String fieldName;
@override
_ShowABCFieldState createState() => new _ShowABCFieldState();
}
class _ShowABCFieldState extends State<ShowABCField> {
int _buildCount = 0;
@override
Widget build(BuildContext context) {
final ABCModel abc = ABCModel.of(context, fieldName: widget.fieldName);
final int value = widget.fieldName == 'a' ? abc.a : (widget.fieldName == 'b' ? abc.b : abc.c);
return new Text('${widget.fieldName}: $value [${_buildCount++}]');
}
}
void main() {
testWidgets('InheritedModel basics', (WidgetTester tester) async {
int _a = 0;
int _b = 1;
int _c = 2;
final Widget abcPage = new StatefulBuilder(
builder: (BuildContext context, StateSetter setState) {
const Widget showA = ShowABCField(fieldName: 'a');
const Widget showB = ShowABCField(fieldName: 'b');
const Widget showC = ShowABCField(fieldName: 'c');
// Unconditionally depends on the ABCModel: rebuilt when any
// aspect of the model changes.
final Widget showABC = new Builder(
builder: (BuildContext context) {
final ABCModel abc = ABCModel.of(context);
return new Text('a: ${abc.a} b: ${abc.b} c: ${abc.c}');
}
);
return new Scaffold(
body: new StatefulBuilder(
builder: (BuildContext context, StateSetter setState) {
return new ABCModel(
a: _a,
b: _b,
c: _c,
child: new Center(
child: new Column(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
showA,
showB,
showC,
showABC,
new RaisedButton(
child: const Text('Increment a'),
onPressed: () {
// Rebuilds the ABCModel which triggers a rebuild
// of showA because showA depends on the 'a' aspect
// of the ABCModel.
setState(() { _a += 1; });
},
),
new RaisedButton(
child: const Text('Increment b'),
onPressed: () {
// Rebuilds the ABCModel which triggers a rebuild
// of showB because showB depends on the 'b' aspect
// of the ABCModel.
setState(() { _b += 1; });
},
),
new RaisedButton(
child: const Text('Increment c'),
onPressed: () {
// Rebuilds the ABCModel which triggers a rebuild
// of showC because showC depends on the 'c' aspect
// of the ABCModel.
setState(() { _c += 1; });
},
),
],
),
),
);
},
),
);
},
);
await tester.pumpWidget(new MaterialApp(home: abcPage));
expect(find.text('a: 0 [0]'), findsOneWidget);
expect(find.text('b: 1 [0]'), findsOneWidget);
expect(find.text('c: 2 [0]'), findsOneWidget);
expect(find.text('a: 0 b: 1 c: 2'), findsOneWidget);
await tester.tap(find.text('Increment a'));
await tester.pumpAndSettle();
// Verify that field 'a' was incremented, but only the showA
// and showABC widgets were rebuilt.
expect(find.text('a: 1 [1]'), findsOneWidget);
expect(find.text('b: 1 [0]'), findsOneWidget);
expect(find.text('c: 2 [0]'), findsOneWidget);
expect(find.text('a: 1 b: 1 c: 2'), findsOneWidget);
// Verify that field 'a' was incremented, but only the showA
// and showABC widgets were rebuilt.
await tester.tap(find.text('Increment a'));
await tester.pumpAndSettle();
expect(find.text('a: 2 [2]'), findsOneWidget);
expect(find.text('b: 1 [0]'), findsOneWidget);
expect(find.text('c: 2 [0]'), findsOneWidget);
expect(find.text('a: 2 b: 1 c: 2'), findsOneWidget);
// Verify that field 'b' was incremented, but only the showB
// and showABC widgets were rebuilt.
await tester.tap(find.text('Increment b'));
await tester.pumpAndSettle();
expect(find.text('a: 2 [2]'), findsOneWidget);
expect(find.text('b: 2 [1]'), findsOneWidget);
expect(find.text('c: 2 [0]'), findsOneWidget);
expect(find.text('a: 2 b: 2 c: 2'), findsOneWidget);
// Verify that field 'c' was incremented, but only the showC
// and showABC widgets were rebuilt.
await tester.tap(find.text('Increment c'));
await tester.pumpAndSettle();
expect(find.text('a: 2 [2]'), findsOneWidget);
expect(find.text('b: 2 [1]'), findsOneWidget);
expect(find.text('c: 3 [1]'), findsOneWidget);
expect(find.text('a: 2 b: 2 c: 3'), findsOneWidget);
});
testWidgets('Inner InheritedModel shadows the outer one', (WidgetTester tester) async {
int _a = 0;
int _b = 1;
int _c = 2;
// Same as in abcPage in the "InheritedModel basics" test except:
// there are two ABCModels and the inner model's "a" and "b"
// properties shadow (override) the outer model. Further complicating
// matters: the inner model only supports the model's "a" aspect,
// so showB and showC will depend on the outer model.
final Widget abcPage = new StatefulBuilder(
builder: (BuildContext context, StateSetter setState) {
const Widget showA = ShowABCField(fieldName: 'a');
const Widget showB = ShowABCField(fieldName: 'b');
const Widget showC = ShowABCField(fieldName: 'c');
// Unconditionally depends on the closest ABCModel ancestor.
// Which is the inner model, for which b,c are null.
final Widget showABC = new Builder(
builder: (BuildContext context) {
final ABCModel abc = ABCModel.of(context);
return new Text('a: ${abc.a} b: ${abc.b} c: ${abc.c}', style: Theme.of(context).textTheme.title);
}
);
return new Scaffold(
body: new StatefulBuilder(
builder: (BuildContext context, StateSetter setState) {
return new ABCModel( // The "outer" model
a: _a,
b: _b,
c: _c,
child: new ABCModel( // The "inner" model
a: 100 + _a,
b: 100 + _b,
aspects: new Set<String>.of(<String>['a']),
child: new Center(
child: new Column(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
showA,
showB,
showC,
const SizedBox(height: 24.0),
showABC,
const SizedBox(height: 24.0),
new RaisedButton(
child: const Text('Increment a'),
onPressed: () {
setState(() { _a += 1; });
},
),
new RaisedButton(
child: const Text('Increment b'),
onPressed: () {
setState(() { _b += 1; });
},
),
new RaisedButton(
child: const Text('Increment c'),
onPressed: () {
setState(() { _c += 1; });
},
),
],
),
),
),
);
},
),
);
},
);
await tester.pumpWidget(new MaterialApp(home: abcPage));
expect(find.text('a: 100 [0]'), findsOneWidget);
expect(find.text('b: 1 [0]'), findsOneWidget);
expect(find.text('c: 2 [0]'), findsOneWidget);
expect(find.text('a: 100 b: 101 c: null'), findsOneWidget);
await tester.tap(find.text('Increment a'));
await tester.pumpAndSettle();
// Verify that field 'a' was incremented, but only the showA
// and showABC widgets were rebuilt.
expect(find.text('a: 101 [1]'), findsOneWidget);
expect(find.text('b: 1 [0]'), findsOneWidget);
expect(find.text('c: 2 [0]'), findsOneWidget);
expect(find.text('a: 101 b: 101 c: null'), findsOneWidget);
await tester.tap(find.text('Increment a'));
await tester.pumpAndSettle();
// Verify that field 'a' was incremented, but only the showA
// and showABC widgets were rebuilt.
expect(find.text('a: 102 [2]'), findsOneWidget);
expect(find.text('b: 1 [0]'), findsOneWidget);
expect(find.text('c: 2 [0]'), findsOneWidget);
expect(find.text('a: 102 b: 101 c: null'), findsOneWidget);
// Verify that field 'b' was incremented, but only the showB
// and showABC widgets were rebuilt.
await tester.tap(find.text('Increment b'));
await tester.pumpAndSettle();
expect(find.text('a: 102 [2]'), findsOneWidget);
expect(find.text('b: 2 [1]'), findsOneWidget);
expect(find.text('c: 2 [0]'), findsOneWidget);
expect(find.text('a: 102 b: 102 c: null'), findsOneWidget);
// Verify that field 'c' was incremented, but only the showC
// and showABC widgets were rebuilt.
await tester.tap(find.text('Increment c'));
await tester.pumpAndSettle();
expect(find.text('a: 102 [2]'), findsOneWidget);
expect(find.text('b: 2 [1]'), findsOneWidget);
expect(find.text('c: 3 [1]'), findsOneWidget);
expect(find.text('a: 102 b: 102 c: null'), findsOneWidget);
});
testWidgets('InheritedModel inner models supported aspect change', (WidgetTester tester) async {
int _a = 0;
int _b = 1;
int _c = 2;
Set<String> _innerModelAspects = new Set<String>.of(<String>['a']);
// Same as in abcPage in the "Inner InheritedModel shadows the outer one"
// test except: the "Add b aspect" changes adds 'b' to the set of
// aspects supported by the inner model.
final Widget abcPage = new StatefulBuilder(
builder: (BuildContext context, StateSetter setState) {
const Widget showA = ShowABCField(fieldName: 'a');
const Widget showB = ShowABCField(fieldName: 'b');
const Widget showC = ShowABCField(fieldName: 'c');
// Unconditionally depends on the closest ABCModel ancestor.
// Which is the inner model, for which b,c are null.
final Widget showABC = new Builder(
builder: (BuildContext context) {
final ABCModel abc = ABCModel.of(context);
return new Text('a: ${abc.a} b: ${abc.b} c: ${abc.c}', style: Theme.of(context).textTheme.title);
}
);
return new Scaffold(
body: new StatefulBuilder(
builder: (BuildContext context, StateSetter setState) {
return new ABCModel( // The "outer" model
a: _a,
b: _b,
c: _c,
child: new ABCModel( // The "inner" model
a: 100 + _a,
b: 100 + _b,
aspects: _innerModelAspects,
child: new Center(
child: new Column(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
showA,
showB,
showC,
const SizedBox(height: 24.0),
showABC,
const SizedBox(height: 24.0),
new RaisedButton(
child: const Text('Increment a'),
onPressed: () {
setState(() { _a += 1; });
},
),
new RaisedButton(
child: const Text('Increment b'),
onPressed: () {
setState(() { _b += 1; });
},
),
new RaisedButton(
child: const Text('Increment c'),
onPressed: () {
setState(() { _c += 1; });
},
),
new RaisedButton(
child: const Text('rebuild'),
onPressed: () {
setState(() {
// Rebuild both models
});
},
),
],
),
),
),
);
},
),
);
},
);
_innerModelAspects = new Set<String>.of(<String>['a']);
await tester.pumpWidget(new MaterialApp(home: abcPage));
expect(find.text('a: 100 [0]'), findsOneWidget); // showA depends on the inner model
expect(find.text('b: 1 [0]'), findsOneWidget); // showB depends on the outer model
expect(find.text('c: 2 [0]'), findsOneWidget);
expect(find.text('a: 100 b: 101 c: null'), findsOneWidget); // inner model's a, b, c
_innerModelAspects = new Set<String>.of(<String>['a', 'b']);
await tester.tap(find.text('rebuild'));
await tester.pumpAndSettle();
expect(find.text('a: 100 [1]'), findsOneWidget); // rebuilt showA still depend on the inner model
expect(find.text('b: 101 [1]'), findsOneWidget); // rebuilt showB now depends on the inner model
expect(find.text('c: 2 [1]'), findsOneWidget); // rebuilt showC still depends on the outer model
expect(find.text('a: 100 b: 101 c: null'), findsOneWidget); // inner model's a, b, c
// Verify that field 'a' was incremented, but only the showA
// and showABC widgets were rebuilt.
await tester.tap(find.text('Increment a'));
await tester.pumpAndSettle();
expect(find.text('a: 101 [2]'), findsOneWidget); // rebuilt showA still depends on the inner model
expect(find.text('b: 101 [1]'), findsOneWidget);
expect(find.text('c: 2 [1]'), findsOneWidget);
expect(find.text('a: 101 b: 101 c: null'), findsOneWidget);
// Verify that field 'b' was incremented, but only the showB
// and showABC widgets were rebuilt.
await tester.tap(find.text('Increment b'));
await tester.pumpAndSettle();
expect(find.text('a: 101 [2]'), findsOneWidget); // rebuilt showB still depends on the inner model
expect(find.text('b: 102 [2]'), findsOneWidget);
expect(find.text('c: 2 [1]'), findsOneWidget);
expect(find.text('a: 101 b: 102 c: null'), findsOneWidget);
// Verify that field 'c' was incremented, but only the showC
// and showABC widgets were rebuilt.
await tester.tap(find.text('Increment c'));
await tester.pumpAndSettle();
expect(find.text('a: 101 [2]'), findsOneWidget);
expect(find.text('b: 102 [2]'), findsOneWidget);
expect(find.text('c: 3 [2]'), findsOneWidget); // rebuilt showC still depends on the outer model
expect(find.text('a: 101 b: 102 c: null'), findsOneWidget);
_innerModelAspects = new Set<String>.of(<String>['a', 'b', 'c']);
await tester.tap(find.text('rebuild'));
await tester.pumpAndSettle();
expect(find.text('a: 101 [3]'), findsOneWidget); // rebuilt showA still depend on the inner model
expect(find.text('b: 102 [3]'), findsOneWidget); // rebuilt showB still depends on the inner model
expect(find.text('c: null [3]'), findsOneWidget); // rebuilt showC now depends on the inner model
expect(find.text('a: 101 b: 102 c: null'), findsOneWidget); // inner model's a, b, c
// Now the inner model supports no aspects
_innerModelAspects = new Set<String>.of(<String>[]);
await tester.tap(find.text('rebuild'));
await tester.pumpAndSettle();
expect(find.text('a: 1 [4]'), findsOneWidget); // rebuilt showA now depends on the outer model
expect(find.text('b: 2 [4]'), findsOneWidget); // rebuilt showB now depends on the outer model
expect(find.text('c: 3 [4]'), findsOneWidget); // rebuilt showC now depends on the outer model
expect(find.text('a: 101 b: 102 c: null'), findsOneWidget); // inner model's a, b, c
// Now the inner model supports all aspects
_innerModelAspects = null;
await tester.tap(find.text('rebuild'));
await tester.pumpAndSettle();
expect(find.text('a: 101 [5]'), findsOneWidget); // rebuilt showA now depends on the inner model
expect(find.text('b: 102 [5]'), findsOneWidget); // rebuilt showB now depends on the inner model
expect(find.text('c: null [5]'), findsOneWidget); // rebuilt showC now depends on the inner model
expect(find.text('a: 101 b: 102 c: null'), findsOneWidget); // inner model's a, b, c
});
}
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