// 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({ super.key, this.a, this.b, this.c, this.aspects, required super.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({ super.key, required this.fieldName }); 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 }); }