blob: 313dadd337964076b2a6aab46b1c97244a51f51d [file] [log] [blame]
// 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/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.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
State<ShowABCField> 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
});
}