| // 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 |
| }); |
| } |