478 lines
19 KiB
Dart
478 lines
19 KiB
Dart
// Copyright 2014 The Flutter 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,
|
|
required 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, required this.fieldName }) : super(key: key);
|
|
|
|
final String fieldName;
|
|
|
|
@override
|
|
_ShowABCFieldState createState() => _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 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 = 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 = Builder(
|
|
builder: (BuildContext context) {
|
|
final ABCModel abc = ABCModel.of(context)!;
|
|
return Text('a: ${abc.a} b: ${abc.b} c: ${abc.c}');
|
|
}
|
|
);
|
|
|
|
return Scaffold(
|
|
body: StatefulBuilder(
|
|
builder: (BuildContext context, StateSetter setState) {
|
|
return ABCModel(
|
|
a: _a,
|
|
b: _b,
|
|
c: _c,
|
|
child: Center(
|
|
child: Column(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: <Widget>[
|
|
showA,
|
|
showB,
|
|
showC,
|
|
showABC,
|
|
ElevatedButton(
|
|
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; });
|
|
},
|
|
),
|
|
ElevatedButton(
|
|
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; });
|
|
},
|
|
),
|
|
ElevatedButton(
|
|
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(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('Looking up an non existent InheritedModel ancestor returns null', (WidgetTester tester) async {
|
|
ABCModel? inheritedModel;
|
|
|
|
await tester.pumpWidget(
|
|
Builder(
|
|
builder: (BuildContext context) {
|
|
inheritedModel = InheritedModel.inheritFrom(context);
|
|
return Container();
|
|
},
|
|
),
|
|
);
|
|
// Shouldn't crash first of all.
|
|
|
|
expect(inheritedModel, null);
|
|
});
|
|
|
|
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 = 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 = Builder(
|
|
builder: (BuildContext context) {
|
|
final ABCModel abc = ABCModel.of(context)!;
|
|
return Text('a: ${abc.a} b: ${abc.b} c: ${abc.c}', style: Theme.of(context).textTheme.headline6);
|
|
}
|
|
);
|
|
|
|
return Scaffold(
|
|
body: StatefulBuilder(
|
|
builder: (BuildContext context, StateSetter setState) {
|
|
return ABCModel( // The "outer" model
|
|
a: _a,
|
|
b: _b,
|
|
c: _c,
|
|
child: ABCModel( // The "inner" model
|
|
a: 100 + _a,
|
|
b: 100 + _b,
|
|
aspects: const <String>{'a'},
|
|
child: Center(
|
|
child: Column(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: <Widget>[
|
|
showA,
|
|
showB,
|
|
showC,
|
|
const SizedBox(height: 24.0),
|
|
showABC,
|
|
const SizedBox(height: 24.0),
|
|
ElevatedButton(
|
|
child: const Text('Increment a'),
|
|
onPressed: () {
|
|
setState(() { _a += 1; });
|
|
},
|
|
),
|
|
ElevatedButton(
|
|
child: const Text('Increment b'),
|
|
onPressed: () {
|
|
setState(() { _b += 1; });
|
|
},
|
|
),
|
|
ElevatedButton(
|
|
child: const Text('Increment c'),
|
|
onPressed: () {
|
|
setState(() { _c += 1; });
|
|
},
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
);
|
|
},
|
|
),
|
|
);
|
|
},
|
|
);
|
|
|
|
await tester.pumpWidget(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 = <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 = 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 = Builder(
|
|
builder: (BuildContext context) {
|
|
final ABCModel abc = ABCModel.of(context)!;
|
|
return Text('a: ${abc.a} b: ${abc.b} c: ${abc.c}', style: Theme.of(context).textTheme.headline6);
|
|
}
|
|
);
|
|
|
|
return Scaffold(
|
|
body: StatefulBuilder(
|
|
builder: (BuildContext context, StateSetter setState) {
|
|
return ABCModel( // The "outer" model
|
|
a: _a,
|
|
b: _b,
|
|
c: _c,
|
|
child: ABCModel( // The "inner" model
|
|
a: 100 + _a,
|
|
b: 100 + _b,
|
|
aspects: _innerModelAspects,
|
|
child: Center(
|
|
child: Column(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: <Widget>[
|
|
showA,
|
|
showB,
|
|
showC,
|
|
const SizedBox(height: 24.0),
|
|
showABC,
|
|
const SizedBox(height: 24.0),
|
|
ElevatedButton(
|
|
child: const Text('Increment a'),
|
|
onPressed: () {
|
|
setState(() { _a += 1; });
|
|
},
|
|
),
|
|
ElevatedButton(
|
|
child: const Text('Increment b'),
|
|
onPressed: () {
|
|
setState(() { _b += 1; });
|
|
},
|
|
),
|
|
ElevatedButton(
|
|
child: const Text('Increment c'),
|
|
onPressed: () {
|
|
setState(() { _c += 1; });
|
|
},
|
|
),
|
|
ElevatedButton(
|
|
child: const Text('rebuild'),
|
|
onPressed: () {
|
|
setState(() {
|
|
// Rebuild both models
|
|
});
|
|
},
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
);
|
|
},
|
|
),
|
|
);
|
|
},
|
|
);
|
|
|
|
_innerModelAspects = <String>{'a'};
|
|
await tester.pumpWidget(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 = <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 = <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 = <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
|
|
});
|
|
}
|