├── .github └── workflows │ └── ci.yml ├── .gitignore ├── CHANGELOG.md ├── LICENSE ├── README.md ├── analysis_options.yaml ├── example └── example.dart ├── lib └── nested.dart ├── pubspec.yaml ├── scripts └── flutter_test.sh └── test ├── common.dart └── nested_test.dart /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: ci 2 | on: 3 | push: 4 | branches: 5 | - master 6 | pull_request: 7 | schedule: 8 | - cron: "0 0 * * 0" 9 | jobs: 10 | test: 11 | name: Test on ${{ matrix.os }} 12 | runs-on: ${{ matrix.os }} 13 | strategy: 14 | matrix: 15 | os: [ubuntu-latest] 16 | steps: 17 | # Set up Flutter and add it to the path. 18 | - name: Clone Flutter repository 19 | uses: actions/checkout@v2 20 | with: 21 | repository: 'flutter/flutter' 22 | ref: 'beta' 23 | path: 'flutter' 24 | fetch-depth: 0 25 | - name: Add Flutter to the PATH for Unix 26 | if: startsWith(matrix.os, 'macOS') || startsWith(matrix.os, 'ubuntu') 27 | run: echo "$GITHUB_WORKSPACE/flutter/bin" >> $GITHUB_PATH 28 | - name: Add Flutter to the PATH for Windows 29 | if: startsWith(matrix.os, 'windows') 30 | run: echo "${env:GITHUB_WORKSPACE}\flutter\bin" | Out-File -FilePath $env:GITHUB_PATH -Encoding utf8 -Append 31 | - name: Run Flutter doctor 32 | run: flutter doctor -v 33 | 34 | # Clone the google_fonts repository under `code`, to avoid conflicts with `flutter`. 35 | - uses: actions/checkout@v2 36 | with: 37 | path: 'code' 38 | 39 | # Analyze, check format, and run tests for the repository. 40 | - run: flutter pub get 41 | working-directory: code 42 | - run: flutter analyze --fatal-infos 43 | working-directory: code 44 | - run: flutter format --set-exit-if-changed --dry-run . 45 | working-directory: code 46 | - run: flutter test 47 | working-directory: code -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Files and directories created by pub 2 | .dart_tool/ 3 | android/ 4 | ios/ 5 | .packages 6 | # Remove the following pattern if you wish to check in your lock file 7 | pubspec.lock 8 | 9 | # Conventional directory for build outputs 10 | build/ 11 | coverage/ 12 | 13 | # Directory created by dartdoc 14 | doc/api/ 15 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## 1.0.0 2 | 3 | First stable release of Nested (with null safety) 4 | 5 | ## 0.0.5-nullsafety.0 6 | 7 | - Support null-safety (Thanks to [@vishna](https://github.com/vishna)!) 8 | 9 | ## 0.0.4 10 | 11 | - Handle reorder through `GlobalKey`s 12 | 13 | ## 0.0.3 14 | 15 | - Made the element of Stateless/Stateful variants public 16 | 17 | ## 0.0.2 18 | 19 | - Added the ability to implement `SingleChildWidget` using mixins 20 | - Added support for `InheritedWidget` (only using mixins). 21 | 22 | ## 0.0.1 23 | 24 | Initial release 25 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Remi Rousselet 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![pub package](https://img.shields.io/pub/v/nested.svg)](https://pub.dartlang.org/packages/nested) ![ci](https://github.com/rrousselGit/nested/workflows/ci/badge.svg) 2 | 3 | A widget that simplifies the syntax for deeply nested widget trees. 4 | 5 | ## Motivation 6 | 7 | Widgets tend to get pretty nested rapidly. 8 | It's not rare to see: 9 | 10 | ```dart 11 | MyWidget( 12 | child: AnotherWidget( 13 | child: Again( 14 | child: AndAgain( 15 | child: Leaf(), 16 | ) 17 | ) 18 | ) 19 | ) 20 | ``` 21 | 22 | That's not very ideal. 23 | 24 | There's where `nested` propose a solution. 25 | Using `nested`, it is possible to flatten the previous tree into: 26 | 27 | ```dart 28 | Nested( 29 | children: [ 30 | MyWidget(), 31 | AnotherWidget(), 32 | Again(), 33 | AndAgain(), 34 | ], 35 | child: Leaf(), 36 | ), 37 | ``` 38 | 39 | That's a lot more readable! 40 | 41 | ## Usage 42 | 43 | `Nested` relies on a new kind of widget: [SingleChildWidget], which has two 44 | concrete implementation: 45 | 46 | - [SingleChildStatelessWidget] 47 | - [SingleChildStatefulWidget] 48 | 49 | These are [SingleChildWidget] variants of the original `Stateless`/`StatefulWidget`. 50 | 51 | The difference between a widget and its single-child variant is that they have 52 | a custom `build` method that takes an extra parameter. 53 | 54 | As such, a `StatelessWidget` would be: 55 | 56 | ```dart 57 | class MyWidget extends StatelessWidget { 58 | MyWidget({Key key, this.child}): super(key: key); 59 | 60 | final Widget child; 61 | 62 | @override 63 | Widget build(BuildContext context) { 64 | return SomethingWidget(child: child); 65 | } 66 | } 67 | ``` 68 | 69 | Whereas a [SingleChildStatelessWidget] would be: 70 | 71 | ```dart 72 | class MyWidget extends SingleChildStatelessWidget { 73 | MyWidget({Key key, Widget child}): super(key: key, child: child); 74 | 75 | @override 76 | Widget buildWithChild(BuildContext context, Widget child) { 77 | return SomethingWidget(child: child); 78 | } 79 | } 80 | ``` 81 | 82 | This allows our new `MyWidget` to be used both with: 83 | 84 | ```dart 85 | MyWidget( 86 | child: AnotherWidget(), 87 | ) 88 | ``` 89 | 90 | and to be placed inside `children` of [Nested] like so: 91 | 92 | ```dart 93 | Nested( 94 | children: [ 95 | MyWidget(), 96 | ... 97 | ], 98 | child: AnotherWidget(), 99 | ) 100 | ``` 101 | 102 | [singlechildwidget]: https://pub.dartlang.org/documentation/nested/latest/nested/SingleChildWidget-class.html 103 | [singlechildstatelesswidget]: https://pub.dartlang.org/documentation/nested/latest/nested/SingleChildStatelessWidget-class.html 104 | [singlechildstatefulwidget]: https://pub.dartlang.org/documentation/nested/latest/nested/SingleChildStatefulWidget-class.html 105 | -------------------------------------------------------------------------------- /analysis_options.yaml: -------------------------------------------------------------------------------- 1 | analyzer: 2 | strong-mode: 3 | implicit-casts: false 4 | implicit-dynamic: false 5 | linter: 6 | rules: 7 | - annotate_overrides 8 | - avoid_empty_else 9 | - avoid_function_literals_in_foreach_calls 10 | - avoid_init_to_null 11 | - avoid_null_checks_in_equality_operators 12 | - avoid_relative_lib_imports 13 | - avoid_renaming_method_parameters 14 | - avoid_return_types_on_setters 15 | - avoid_returning_null 16 | - avoid_types_as_parameter_names 17 | - avoid_unused_constructor_parameters 18 | - await_only_futures 19 | - camel_case_types 20 | - cancel_subscriptions 21 | - cascade_invocations 22 | - comment_references 23 | - constant_identifier_names 24 | - control_flow_in_finally 25 | - directives_ordering 26 | - empty_catches 27 | - empty_constructor_bodies 28 | - empty_statements 29 | - hash_and_equals 30 | - implementation_imports 31 | - invariant_booleans 32 | - iterable_contains_unrelated_type 33 | - library_names 34 | - library_prefixes 35 | - list_remove_unrelated_type 36 | - no_adjacent_strings_in_list 37 | - no_duplicate_case_values 38 | - non_constant_identifier_names 39 | - null_closures 40 | - omit_local_variable_types 41 | - only_throw_errors 42 | - overridden_fields 43 | - package_api_docs 44 | - package_names 45 | - package_prefixed_library_names 46 | - prefer_adjacent_string_concatenation 47 | - prefer_collection_literals 48 | - prefer_conditional_assignment 49 | - prefer_const_constructors 50 | - prefer_contains 51 | - prefer_equal_for_default_values 52 | - prefer_final_fields 53 | - prefer_initializing_formals 54 | - prefer_interpolation_to_compose_strings 55 | - prefer_is_empty 56 | - prefer_is_not_empty 57 | - prefer_single_quotes 58 | - prefer_typing_uninitialized_variables 59 | - recursive_getters 60 | - slash_for_doc_comments 61 | - test_types_in_equals 62 | - throw_in_finally 63 | - type_init_formals 64 | - unawaited_futures 65 | - unnecessary_brace_in_string_interps 66 | - unnecessary_const 67 | - unnecessary_getters_setters 68 | - unnecessary_lambdas 69 | - unnecessary_new 70 | - unnecessary_null_aware_assignments 71 | - unnecessary_statements 72 | - unnecessary_this 73 | - unrelated_type_equality_checks 74 | - use_rethrow_when_possible 75 | - valid_regexps 76 | -------------------------------------------------------------------------------- /example/example.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:nested/nested.dart'; 3 | 4 | void main() { 5 | runApp( 6 | Nested( 7 | children: [ 8 | const SingleChildContainer(color: Colors.red), 9 | SingleChildBuilder( 10 | builder: (context, child) => Center(child: child), 11 | ), 12 | ], 13 | child: const Text('Hello world', textDirection: TextDirection.ltr), 14 | ), 15 | ); 16 | } 17 | 18 | class SingleChildContainer extends SingleChildStatelessWidget { 19 | const SingleChildContainer({Key? key, required this.color, Widget? child}) 20 | : super(key: key, child: child); 21 | 22 | final Color color; 23 | 24 | @override 25 | Widget buildWithChild(BuildContext context, Widget? child) { 26 | return Container( 27 | color: color, 28 | child: child, 29 | ); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /lib/nested.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/foundation.dart'; 2 | import 'package:flutter/material.dart'; 3 | import 'package:flutter/widgets.dart'; 4 | 5 | /// A widget that simplify the writing of deeply nested widget trees. 6 | /// 7 | /// It relies on the new kind of widget [SingleChildWidget], which has two 8 | /// concrete implementations: 9 | /// - [SingleChildStatelessWidget] 10 | /// - [SingleChildStatefulWidget] 11 | /// 12 | /// They are both respectively a [SingleChildWidget] variant of [StatelessWidget] 13 | /// and [StatefulWidget]. 14 | /// 15 | /// The difference between a widget and its single-child variant is that they have 16 | /// a custom `build` method that takes an extra parameter. 17 | /// 18 | /// As such, a `StatelessWidget` would be: 19 | /// 20 | /// ```dart 21 | /// class MyWidget extends StatelessWidget { 22 | /// MyWidget({Key key, this.child}): super(key: key); 23 | /// 24 | /// final Widget child; 25 | /// 26 | /// @override 27 | /// Widget build(BuildContext context) { 28 | /// return SomethingWidget(child: child); 29 | /// } 30 | /// } 31 | /// ``` 32 | /// 33 | /// Whereas a [SingleChildStatelessWidget] would be: 34 | /// 35 | /// ```dart 36 | /// class MyWidget extends SingleChildStatelessWidget { 37 | /// MyWidget({Key key, Widget child}): super(key: key, child: child); 38 | /// 39 | /// @override 40 | /// Widget buildWithChild(BuildContext context, Widget child) { 41 | /// return SomethingWidget(child: child); 42 | /// } 43 | /// } 44 | /// ``` 45 | /// 46 | /// This allows our new `MyWidget` to be used both with: 47 | /// 48 | /// ```dart 49 | /// MyWidget( 50 | /// child: AnotherWidget(), 51 | /// ) 52 | /// ``` 53 | /// 54 | /// and to be placed inside `children` of [Nested] like so: 55 | /// 56 | /// ```dart 57 | /// Nested( 58 | /// children: [ 59 | /// MyWidget(), 60 | /// ... 61 | /// ], 62 | /// child: AnotherWidget(), 63 | /// ) 64 | /// ``` 65 | class Nested extends StatelessWidget implements SingleChildWidget { 66 | /// Allows configuring key, children and child 67 | Nested({ 68 | Key? key, 69 | required List children, 70 | Widget? child, 71 | }) : assert(children.isNotEmpty), 72 | _children = children, 73 | _child = child, 74 | super(key: key); 75 | 76 | final List _children; 77 | final Widget? _child; 78 | 79 | @override 80 | Widget build(BuildContext context) { 81 | throw StateError('implemented internally'); 82 | } 83 | 84 | @override 85 | _NestedElement createElement() => _NestedElement(this); 86 | } 87 | 88 | class _NestedElement extends StatelessElement 89 | with SingleChildWidgetElementMixin { 90 | _NestedElement(Nested widget) : super(widget); 91 | 92 | @override 93 | Nested get widget => super.widget as Nested; 94 | 95 | final nodes = <_NestedHookElement>{}; 96 | 97 | @override 98 | Widget build() { 99 | _NestedHook? nestedHook; 100 | var nextNode = _parent?.injectedChild ?? widget._child; 101 | 102 | for (final child in widget._children.reversed) { 103 | nextNode = nestedHook = _NestedHook( 104 | owner: this, 105 | wrappedWidget: child, 106 | injectedChild: nextNode, 107 | ); 108 | } 109 | 110 | if (nestedHook != null) { 111 | // We manually update _NestedHookElement instead of letter widgets do their thing 112 | // because an item N may be constant but N+1 not. So, if we used widgets 113 | // then N+1 wouldn't rebuild because N didn't change 114 | for (final node in nodes) { 115 | node 116 | ..wrappedChild = nestedHook!.wrappedWidget 117 | ..injectedChild = nestedHook.injectedChild; 118 | 119 | final next = nestedHook.injectedChild; 120 | if (next is _NestedHook) { 121 | nestedHook = next; 122 | } else { 123 | break; 124 | } 125 | } 126 | } 127 | 128 | return nextNode!; 129 | } 130 | } 131 | 132 | class _NestedHook extends StatelessWidget { 133 | _NestedHook({ 134 | this.injectedChild, 135 | required this.wrappedWidget, 136 | required this.owner, 137 | }); 138 | 139 | final SingleChildWidget wrappedWidget; 140 | final Widget? injectedChild; 141 | final _NestedElement owner; 142 | 143 | @override 144 | _NestedHookElement createElement() => _NestedHookElement(this); 145 | 146 | @override 147 | Widget build(BuildContext context) => throw StateError('handled internally'); 148 | } 149 | 150 | class _NestedHookElement extends StatelessElement { 151 | _NestedHookElement(_NestedHook widget) : super(widget); 152 | 153 | @override 154 | _NestedHook get widget => super.widget as _NestedHook; 155 | 156 | Widget? _injectedChild; 157 | Widget? get injectedChild => _injectedChild; 158 | set injectedChild(Widget? value) { 159 | final previous = _injectedChild; 160 | if (value is _NestedHook && 161 | previous is _NestedHook && 162 | Widget.canUpdate(value.wrappedWidget, previous.wrappedWidget)) { 163 | // no need to rebuild the wrapped widget just for a _NestedHook. 164 | // The widget doesn't matter here, only its Element. 165 | return; 166 | } 167 | if (previous != value) { 168 | _injectedChild = value; 169 | visitChildren((e) => e.markNeedsBuild()); 170 | } 171 | } 172 | 173 | SingleChildWidget? _wrappedChild; 174 | SingleChildWidget? get wrappedChild => _wrappedChild; 175 | set wrappedChild(SingleChildWidget? value) { 176 | if (_wrappedChild != value) { 177 | _wrappedChild = value; 178 | markNeedsBuild(); 179 | } 180 | } 181 | 182 | @override 183 | void mount(Element? parent, dynamic newSlot) { 184 | widget.owner.nodes.add(this); 185 | _wrappedChild = widget.wrappedWidget; 186 | _injectedChild = widget.injectedChild; 187 | super.mount(parent, newSlot); 188 | } 189 | 190 | @override 191 | void unmount() { 192 | widget.owner.nodes.remove(this); 193 | super.unmount(); 194 | } 195 | 196 | @override 197 | Widget build() { 198 | return wrappedChild!; 199 | } 200 | } 201 | 202 | /// A [Widget] that takes a single descendant. 203 | /// 204 | /// As opposed to [ProxyWidget], it may have a "build" method. 205 | /// 206 | /// See also: 207 | /// - [SingleChildStatelessWidget] 208 | /// - [SingleChildStatefulWidget] 209 | abstract class SingleChildWidget implements Widget { 210 | @override 211 | SingleChildWidgetElementMixin createElement(); 212 | } 213 | 214 | mixin SingleChildWidgetElementMixin on Element { 215 | _NestedHookElement? _parent; 216 | 217 | @override 218 | void mount(Element? parent, dynamic newSlot) { 219 | if (parent is _NestedHookElement?) { 220 | _parent = parent; 221 | } 222 | super.mount(parent, newSlot); 223 | } 224 | 225 | @override 226 | void activate() { 227 | super.activate(); 228 | visitAncestorElements((parent) { 229 | if (parent is _NestedHookElement) { 230 | _parent = parent; 231 | } 232 | return false; 233 | }); 234 | } 235 | } 236 | 237 | /// A [StatelessWidget] that implements [SingleChildWidget] and is therefore 238 | /// compatible with [Nested]. 239 | /// 240 | /// Its [build] method must **not** be overriden. Instead use [buildWithChild]. 241 | abstract class SingleChildStatelessWidget extends StatelessWidget 242 | implements SingleChildWidget { 243 | /// Creates a widget that has exactly one child widget. 244 | const SingleChildStatelessWidget({Key? key, Widget? child}) 245 | : _child = child, 246 | super(key: key); 247 | 248 | final Widget? _child; 249 | 250 | /// A [build] method that receives an extra `child` parameter. 251 | /// 252 | /// This method may be called with a `child` different from the parameter 253 | /// passed to the constructor of [SingleChildStatelessWidget]. 254 | /// It may also be called again with a different `child`, without this widget 255 | /// being recreated. 256 | Widget buildWithChild(BuildContext context, Widget? child); 257 | 258 | @override 259 | Widget build(BuildContext context) => buildWithChild(context, _child); 260 | 261 | @override 262 | SingleChildStatelessElement createElement() { 263 | return SingleChildStatelessElement(this); 264 | } 265 | } 266 | 267 | /// An [Element] that uses a [SingleChildStatelessWidget] as its configuration. 268 | class SingleChildStatelessElement extends StatelessElement 269 | with SingleChildWidgetElementMixin { 270 | /// Creates an element that uses the given widget as its configuration. 271 | SingleChildStatelessElement(SingleChildStatelessWidget widget) 272 | : super(widget); 273 | 274 | @override 275 | Widget build() { 276 | if (_parent != null) { 277 | return widget.buildWithChild(this, _parent!.injectedChild); 278 | } 279 | return super.build(); 280 | } 281 | 282 | @override 283 | SingleChildStatelessWidget get widget => 284 | super.widget as SingleChildStatelessWidget; 285 | } 286 | 287 | /// A [StatefulWidget] that is compatible with [Nested]. 288 | abstract class SingleChildStatefulWidget extends StatefulWidget 289 | implements SingleChildWidget { 290 | /// Creates a widget that has exactly one child widget. 291 | const SingleChildStatefulWidget({Key? key, Widget? child}) 292 | : _child = child, 293 | super(key: key); 294 | 295 | final Widget? _child; 296 | 297 | @override 298 | SingleChildStatefulElement createElement() { 299 | return SingleChildStatefulElement(this); 300 | } 301 | } 302 | 303 | /// A [State] for [SingleChildStatefulWidget]. 304 | /// 305 | /// Do not override [build] and instead override [buildWithChild]. 306 | abstract class SingleChildState 307 | extends State { 308 | /// A [build] method that receives an extra `child` parameter. 309 | /// 310 | /// This method may be called with a `child` different from the parameter 311 | /// passed to the constructor of [SingleChildStatelessWidget]. 312 | /// It may also be called again with a different `child`, without this widget 313 | /// being recreated. 314 | Widget buildWithChild(BuildContext context, Widget? child); 315 | 316 | @override 317 | Widget build(BuildContext context) => buildWithChild(context, widget._child); 318 | } 319 | 320 | /// An [Element] that uses a [SingleChildStatefulWidget] as its configuration. 321 | class SingleChildStatefulElement extends StatefulElement 322 | with SingleChildWidgetElementMixin { 323 | /// Creates an element that uses the given widget as its configuration. 324 | SingleChildStatefulElement(SingleChildStatefulWidget widget) : super(widget); 325 | 326 | @override 327 | SingleChildStatefulWidget get widget => 328 | super.widget as SingleChildStatefulWidget; 329 | 330 | @override 331 | SingleChildState get state => 332 | super.state as SingleChildState; 333 | 334 | @override 335 | Widget build() { 336 | if (_parent != null) { 337 | return state.buildWithChild(this, _parent!.injectedChild!); 338 | } 339 | return super.build(); 340 | } 341 | } 342 | 343 | /// A [SingleChildWidget] that delegates its implementation to a callback. 344 | /// 345 | /// It works like [Builder], but is compatible with [Nested]. 346 | class SingleChildBuilder extends SingleChildStatelessWidget { 347 | /// Creates a widget that delegates its build to a callback. 348 | /// 349 | /// The [builder] argument must not be null. 350 | const SingleChildBuilder({Key? key, required this.builder, Widget? child}) 351 | : super(key: key, child: child); 352 | 353 | /// Called to obtain the child widget. 354 | /// 355 | /// The `child` parameter may be different from the one parameter passed to 356 | /// the constructor of [SingleChildBuilder]. 357 | final Widget Function(BuildContext context, Widget? child) builder; 358 | 359 | @override 360 | Widget buildWithChild(BuildContext context, Widget? child) { 361 | return builder(context, child); 362 | } 363 | } 364 | 365 | mixin SingleChildStatelessWidgetMixin 366 | implements StatelessWidget, SingleChildStatelessWidget { 367 | Widget? get child; 368 | 369 | @override 370 | Widget? get _child => child; 371 | 372 | @override 373 | SingleChildStatelessElement createElement() { 374 | return SingleChildStatelessElement(this); 375 | } 376 | 377 | @override 378 | Widget build(BuildContext context) { 379 | return buildWithChild(context, child); 380 | } 381 | } 382 | 383 | mixin SingleChildStatefulWidgetMixin on StatefulWidget 384 | implements SingleChildWidget { 385 | Widget? get child; 386 | 387 | @override 388 | _SingleChildStatefulMixinElement createElement() => 389 | _SingleChildStatefulMixinElement(this); 390 | } 391 | 392 | mixin SingleChildStateMixin on State { 393 | Widget buildWithChild(BuildContext context, Widget child); 394 | 395 | @override 396 | Widget build(BuildContext context) { 397 | return buildWithChild( 398 | context, 399 | (widget as SingleChildStatefulWidgetMixin).child!, 400 | ); 401 | } 402 | } 403 | 404 | class _SingleChildStatefulMixinElement extends StatefulElement 405 | with SingleChildWidgetElementMixin { 406 | _SingleChildStatefulMixinElement(SingleChildStatefulWidgetMixin widget) 407 | : super(widget); 408 | 409 | @override 410 | SingleChildStatefulWidgetMixin get widget => 411 | super.widget as SingleChildStatefulWidgetMixin; 412 | 413 | @override 414 | SingleChildStateMixin get state => 415 | super.state as SingleChildStateMixin; 416 | 417 | @override 418 | Widget build() { 419 | if (_parent != null) { 420 | return state.buildWithChild(this, _parent!.injectedChild!); 421 | } 422 | return super.build(); 423 | } 424 | } 425 | 426 | mixin SingleChildInheritedElementMixin 427 | on InheritedElement, SingleChildWidgetElementMixin { 428 | @override 429 | Widget build() { 430 | if (_parent != null) { 431 | return _parent!.injectedChild!; 432 | } 433 | return super.build(); 434 | } 435 | } 436 | -------------------------------------------------------------------------------- /pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: nested 2 | description: >- 3 | A Flutter Widget which helps nest multiple widgets without 4 | needing to manually nest them. 5 | version: 1.0.0 6 | repository: https://github.com/rrousselGit/nested 7 | 8 | environment: 9 | sdk: ">=2.12.0-0 <3.0.0" 10 | 11 | dependencies: 12 | flutter: 13 | sdk: flutter 14 | 15 | dev_dependencies: 16 | flutter_test: 17 | sdk: flutter -------------------------------------------------------------------------------- /scripts/flutter_test.sh: -------------------------------------------------------------------------------- 1 | set -e # abort CI if an error happens 2 | cd $1 3 | flutter packages get 4 | flutter format --set-exit-if-changed lib test 5 | flutter analyze --no-current-package lib test/ 6 | flutter test --no-pub --coverage 7 | # resets to the original state 8 | cd - 9 | -------------------------------------------------------------------------------- /test/common.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_test/flutter_test.dart'; 2 | 3 | Matcher matchesInOrder(List match) { 4 | return _MatchesInOrder(match); 5 | } 6 | 7 | class _SubMatcherResult { 8 | final bool matches; 9 | final Map matchState; 10 | 11 | _SubMatcherResult(this.matches, this.matchState); 12 | } 13 | 14 | class _MatchesInOrder extends Matcher { 15 | const _MatchesInOrder(this.itemMatchers); 16 | 17 | final List itemMatchers; 18 | 19 | @override 20 | bool matches(dynamic expectation, Map matchState) { 21 | var count = 0; 22 | 23 | List items; 24 | if (expectation is Finder) { 25 | items = 26 | expectation.evaluate().map((e) => e.widget).toList(growable: false); 27 | } else if (expectation is Iterable) { 28 | items = expectation.toList(); 29 | } else { 30 | throw StateError('Expectation of unknown kind $expectation'); 31 | } 32 | 33 | matchState['items'] = items; 34 | 35 | for (final item in items) { 36 | if (count >= itemMatchers.length) { 37 | return false; 38 | } 39 | 40 | final subMatcherState = {}; 41 | final matches = itemMatchers[count].matches(item, subMatcherState); 42 | matchState[count] = _SubMatcherResult(matches, subMatcherState); 43 | if (!matches) { 44 | return false; 45 | } 46 | 47 | count++; 48 | } 49 | return count == itemMatchers.length; 50 | } 51 | 52 | @override 53 | Description describe(Description description) { 54 | description.add('exactly ${itemMatchers.length} objects where:'); 55 | var index = 0; 56 | for (final matcher in itemMatchers) { 57 | description.add('\n - item ${index + 1} is '); 58 | matcher.describe(description); 59 | index++; 60 | } 61 | return description; 62 | } 63 | 64 | @override 65 | Description describeMismatch( 66 | dynamic item, 67 | Description mismatchDescription, 68 | Map matchState, 69 | bool verbose, 70 | ) { 71 | assert(item is Finder || item is Iterable); 72 | 73 | List items; 74 | if (item is Finder) { 75 | items = item.evaluate().toList(growable: false); 76 | } else if (item is Iterable) { 77 | items = item.toList(); 78 | } else { 79 | throw StateError('Item of unexpected tyoe $item'); 80 | } 81 | 82 | if (items.length != itemMatchers.length) { 83 | return mismatchDescription.add( 84 | 'means ${items.length} were found but ${itemMatchers.length} were expected'); 85 | } 86 | 87 | for (var i = 0; i < itemMatchers.length; i++) { 88 | final matcher = itemMatchers[i]; 89 | 90 | final subMatcherResult = matchState[i] as _SubMatcherResult; 91 | if (!subMatcherResult.matches) { 92 | return matcher.describeMismatch( 93 | items[i], 94 | mismatchDescription..add('fails on item ${i + 1} which'), 95 | subMatcherResult.matchState, 96 | verbose, 97 | ); 98 | } 99 | } 100 | 101 | return mismatchDescription; 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /test/nested_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/foundation.dart'; 2 | import 'package:flutter/material.dart'; 3 | import 'package:flutter_test/flutter_test.dart'; 4 | import 'package:nested/nested.dart'; 5 | 6 | import 'common.dart'; 7 | 8 | void main() { 9 | testWidgets('insert widgets in natural order', (tester) async { 10 | await tester.pumpWidget( 11 | Nested( 12 | children: [ 13 | MySizedBox(height: 0), 14 | MySizedBox(height: 1), 15 | ], 16 | child: const Text('foo', textDirection: TextDirection.ltr), 17 | ), 18 | ); 19 | 20 | expect(find.text('foo'), findsOneWidget); 21 | 22 | expect( 23 | find.byType(MySizedBox), 24 | matchesInOrder([ 25 | isA().having((s) => s.height, 'height', 0), 26 | isA().having((s) => s.height, 'height', 1), 27 | ]), 28 | ); 29 | 30 | await tester.pumpWidget( 31 | Nested( 32 | children: [ 33 | MySizedBox(height: 10), 34 | MySizedBox(height: 11), 35 | ], 36 | child: const Text('bar', textDirection: TextDirection.ltr), 37 | ), 38 | ); 39 | 40 | expect(find.text('bar'), findsOneWidget); 41 | 42 | expect( 43 | find.byType(MySizedBox), 44 | matchesInOrder([ 45 | isA().having((s) => s.height, 'height', 10), 46 | isA().having((s) => s.height, 'height', 11), 47 | ]), 48 | ); 49 | }); 50 | testWidgets('nested inside nested', (tester) async { 51 | await tester.pumpWidget(Nested( 52 | children: [ 53 | MySizedBox(height: 0), 54 | Nested( 55 | children: [ 56 | MySizedBox(height: 1), 57 | MySizedBox(height: 2), 58 | ], 59 | ), 60 | MySizedBox(height: 3), 61 | ], 62 | child: const Text('foo', textDirection: TextDirection.ltr), 63 | )); 64 | 65 | expect(find.text('foo'), findsOneWidget); 66 | 67 | expect( 68 | find.byType(MySizedBox), 69 | matchesInOrder([ 70 | isA().having((s) => s.height, 'height', 0), 71 | isA().having((s) => s.height, 'height', 1), 72 | isA().having((s) => s.height, 'height', 2), 73 | isA().having((s) => s.height, 'height', 3), 74 | ]), 75 | ); 76 | 77 | await tester.pumpWidget(Nested( 78 | children: [ 79 | MySizedBox(height: 10), 80 | Nested( 81 | children: [ 82 | MySizedBox(height: 11), 83 | MySizedBox(height: 12), 84 | ], 85 | ), 86 | MySizedBox(height: 13), 87 | ], 88 | child: const Text('bar', textDirection: TextDirection.ltr), 89 | )); 90 | 91 | expect(find.text('bar'), findsOneWidget); 92 | 93 | expect( 94 | find.byType(MySizedBox), 95 | matchesInOrder([ 96 | isA().having((s) => s.height, 'height', 10), 97 | isA().having((s) => s.height, 'height', 11), 98 | isA().having((s) => s.height, 'height', 12), 99 | isA().having((s) => s.height, 'height', 13), 100 | ]), 101 | ); 102 | }); 103 | 104 | test('children is required', () { 105 | expect( 106 | () => Nested( 107 | children: [], 108 | child: const Text('foo', textDirection: TextDirection.ltr), 109 | ), 110 | throwsAssertionError, 111 | ); 112 | 113 | Nested( 114 | children: [MySizedBox()], 115 | child: const Text('foo', textDirection: TextDirection.ltr), 116 | ); 117 | }); 118 | 119 | testWidgets('no unnecessary rebuild #2', (tester) async { 120 | var buildCount = 0; 121 | final child = Nested( 122 | children: [ 123 | MySizedBox(didBuild: (_, __) => buildCount++), 124 | ], 125 | child: Container(), 126 | ); 127 | 128 | await tester.pumpWidget(child); 129 | 130 | expect(buildCount, equals(1)); 131 | await tester.pumpWidget(child); 132 | 133 | expect(buildCount, equals(1)); 134 | }); 135 | 136 | testWidgets( 137 | 'a node change rebuilds only that node', 138 | (tester) async { 139 | var buildCount1 = 0; 140 | final first = MySizedBox(didBuild: (_, __) => buildCount1++); 141 | 142 | var buildCount2 = 0; 143 | final second = SingleChildBuilder( 144 | builder: (_, child) { 145 | buildCount2++; 146 | return child!; 147 | }, 148 | ); 149 | 150 | await tester.pumpWidget( 151 | Nested( 152 | children: [ 153 | first, 154 | second, 155 | SingleChildBuilder( 156 | builder: (_, __) => 157 | const Text('foo', textDirection: TextDirection.ltr), 158 | ), 159 | ], 160 | ), 161 | ); 162 | 163 | expect(buildCount1, equals(1)); 164 | expect(buildCount2, equals(1)); 165 | expect(find.text('foo'), findsOneWidget); 166 | 167 | await tester.pumpWidget( 168 | Nested( 169 | children: [ 170 | first, 171 | second, 172 | SingleChildBuilder( 173 | builder: (_, __) => 174 | const Text('bar', textDirection: TextDirection.ltr), 175 | ) 176 | ], 177 | ), 178 | ); 179 | 180 | expect(buildCount1, equals(1)); 181 | expect(buildCount2, equals(1)); 182 | expect(find.text('bar'), findsOneWidget); 183 | }, 184 | ); 185 | testWidgets( 186 | 'child change rebuilds last node', 187 | (tester) async { 188 | var buildCount1 = 0; 189 | final first = MySizedBox(didBuild: (_, __) => buildCount1++); 190 | 191 | var buildCount2 = 0; 192 | final second = SingleChildBuilder( 193 | builder: (_, child) { 194 | buildCount2++; 195 | return child!; 196 | }, 197 | ); 198 | 199 | await tester.pumpWidget( 200 | Nested( 201 | children: [ 202 | first, 203 | second, 204 | ], 205 | child: const Text('foo', textDirection: TextDirection.ltr), 206 | ), 207 | ); 208 | 209 | expect(buildCount1, equals(1)); 210 | expect(buildCount2, equals(1)); 211 | expect(find.text('foo'), findsOneWidget); 212 | 213 | await tester.pumpWidget( 214 | Nested( 215 | children: [first, second], 216 | child: const Text('bar', textDirection: TextDirection.ltr), 217 | ), 218 | ); 219 | 220 | expect(buildCount1, equals(1)); 221 | expect(buildCount2, equals(2)); 222 | expect(find.text('bar'), findsOneWidget); 223 | }, 224 | ); 225 | 226 | testWidgets( 227 | 'if only one node, the previous and next nodes may not rebuild', 228 | (tester) async { 229 | var buildCount1 = 0; 230 | final first = MySizedBox(didBuild: (_, __) => buildCount1++); 231 | var buildCount2 = 0; 232 | var buildCount3 = 0; 233 | final third = MySizedBox(didBuild: (_, __) => buildCount3++); 234 | 235 | final child = const Text('foo', textDirection: TextDirection.ltr); 236 | 237 | await tester.pumpWidget( 238 | Nested( 239 | children: [ 240 | first, 241 | MySizedBox( 242 | didBuild: (_, __) => buildCount2++, 243 | ), 244 | third, 245 | ], 246 | child: child, 247 | ), 248 | ); 249 | 250 | expect(buildCount1, equals(1)); 251 | expect(buildCount2, equals(1)); 252 | expect(buildCount3, equals(1)); 253 | expect(find.text('foo'), findsOneWidget); 254 | 255 | await tester.pumpWidget( 256 | Nested( 257 | children: [ 258 | first, 259 | MySizedBox( 260 | didBuild: (_, __) => buildCount2++, 261 | ), 262 | third, 263 | ], 264 | child: child, 265 | ), 266 | ); 267 | 268 | expect(buildCount1, equals(1)); 269 | expect(buildCount2, equals(2)); 270 | expect(buildCount3, equals(1)); 271 | expect(find.text('foo'), findsOneWidget); 272 | }, 273 | ); 274 | 275 | testWidgets( 276 | 'if child changes, rebuild the previous widget', 277 | (tester) async { 278 | var buildCount1 = 0; 279 | final first = MySizedBox(didBuild: (_, __) => buildCount1++); 280 | var buildCount2 = 0; 281 | final second = MySizedBox(didBuild: (_, __) => buildCount2++); 282 | 283 | await tester.pumpWidget( 284 | Nested( 285 | children: [first, second], 286 | child: const Text('foo', textDirection: TextDirection.ltr), 287 | ), 288 | ); 289 | 290 | expect(buildCount1, equals(1)); 291 | expect(buildCount2, equals(1)); 292 | expect(find.text('foo'), findsOneWidget); 293 | 294 | await tester.pumpWidget( 295 | Nested( 296 | children: [ 297 | first, 298 | second, 299 | ], 300 | child: const Text('bar', textDirection: TextDirection.ltr), 301 | ), 302 | ); 303 | 304 | expect(buildCount1, equals(1)); 305 | expect(buildCount2, equals(2)); 306 | expect(find.text('bar'), findsOneWidget); 307 | }, 308 | ); 309 | 310 | testWidgets('last node receives child directly', (tester) async { 311 | Widget? child; 312 | BuildContext? context; 313 | 314 | await tester.pumpWidget( 315 | Nested( 316 | children: [ 317 | SingleChildBuilder( 318 | builder: (ctx, c) { 319 | context = ctx; 320 | child = c; 321 | return Container(); 322 | }, 323 | ) 324 | ], 325 | child: null, 326 | ), 327 | ); 328 | 329 | expect(context, isNotNull); 330 | expect(child, isNull); 331 | 332 | final container = Container(); 333 | 334 | await tester.pumpWidget( 335 | Nested( 336 | children: [ 337 | SingleChildBuilder( 338 | builder: (ctx, c) { 339 | context = ctx; 340 | return (child = c)!; 341 | }, 342 | ) 343 | ], 344 | child: container, 345 | ), 346 | ); 347 | 348 | expect(context, isNotNull); 349 | expect(child, equals(container)); 350 | }); 351 | // TODO: assert keys order preserved (reorder unsupported) 352 | // TODO: nodes can be added optionally using [if] (_Hook takes a globalKey on the child's key) 353 | // TODO: a nested node moves to a new Nested 354 | 355 | testWidgets('SingleChildBuilder can be used alone', (tester) async { 356 | Widget? child; 357 | BuildContext? context; 358 | var container = Container(); 359 | 360 | await tester.pumpWidget( 361 | SingleChildBuilder( 362 | builder: (ctx, c) { 363 | context = ctx; 364 | child = c; 365 | return c!; 366 | }, 367 | child: container, 368 | ), 369 | ); 370 | 371 | expect(child, equals(container)); 372 | expect(context, equals(tester.element(find.byType(SingleChildBuilder)))); 373 | 374 | container = Container(); 375 | 376 | await tester.pumpWidget( 377 | SingleChildBuilder( 378 | builder: (ctx, c) { 379 | context = ctx; 380 | child = c; 381 | return c!; 382 | }, 383 | child: container, 384 | ), 385 | ); 386 | 387 | expect(child, equals(container)); 388 | expect(context, equals(tester.element(find.byType(SingleChildBuilder)))); 389 | }); 390 | testWidgets('SingleChildWidget can be used by itself', (tester) async { 391 | await tester.pumpWidget( 392 | MySizedBox( 393 | height: 42, 394 | child: const Text('foo', textDirection: TextDirection.ltr), 395 | ), 396 | ); 397 | 398 | expect(find.text('foo'), findsOneWidget); 399 | 400 | expect( 401 | find.byType(MySizedBox), 402 | matchesInOrder([ 403 | isA().having((e) => e.height, 'height', equals(42)), 404 | ]), 405 | ); 406 | }); 407 | testWidgets('SingleChildStatefulWidget can be used alone', (tester) async { 408 | Widget? child; 409 | BuildContext? context; 410 | 411 | final text = const Text('foo', textDirection: TextDirection.ltr); 412 | 413 | await tester.pumpWidget( 414 | MyStateful( 415 | didBuild: (ctx, c) { 416 | child = c; 417 | context = ctx; 418 | }, 419 | child: text, 420 | ), 421 | ); 422 | 423 | expect(find.text('foo'), findsOneWidget); 424 | expect(context, equals(tester.element(find.byType(MyStateful)))); 425 | expect(child, equals(text)); 426 | }); 427 | testWidgets('SingleChildStatefulWidget can be used in nested', 428 | (tester) async { 429 | Widget? child; 430 | BuildContext? context; 431 | 432 | final text = const Text('foo', textDirection: TextDirection.ltr); 433 | 434 | await tester.pumpWidget( 435 | Nested( 436 | children: [ 437 | MyStateful( 438 | didBuild: (ctx, c) { 439 | child = c; 440 | context = ctx; 441 | }, 442 | ), 443 | ], 444 | child: text, 445 | ), 446 | ); 447 | 448 | expect(find.text('foo'), findsOneWidget); 449 | expect(context, equals(tester.element(find.byType(MyStateful)))); 450 | expect(child, equals(text)); 451 | }); 452 | testWidgets( 453 | 'SingleChildStatelessWidget can be used as mixin instead of base class', 454 | (tester) async { 455 | await tester.pumpWidget( 456 | Nested( 457 | children: [ 458 | ConcreteStateless(height: 24), 459 | ], 460 | child: const Text('42', textDirection: TextDirection.ltr), 461 | ), 462 | ); 463 | 464 | expect(find.text('42'), findsOneWidget); 465 | 466 | expect( 467 | find.byType(ConcreteStateless), 468 | matchesInOrder([ 469 | isA().having((s) => s.height, 'height', 24), 470 | ]), 471 | ); 472 | 473 | await tester.pumpWidget( 474 | ConcreteStateless( 475 | height: 24, 476 | child: const Text('42', textDirection: TextDirection.ltr), 477 | ), 478 | ); 479 | 480 | expect(find.text('42'), findsOneWidget); 481 | 482 | expect( 483 | find.byType(ConcreteStateless), 484 | matchesInOrder([ 485 | isA().having((s) => s.height, 'height', 24), 486 | ]), 487 | ); 488 | }); 489 | testWidgets('SingleChildInheritedElementMixin', (tester) async { 490 | await tester.pumpWidget( 491 | Nested( 492 | children: [ 493 | MyInherited( 494 | height: 24, 495 | child: const SizedBox.shrink(), 496 | ), 497 | ], 498 | child: const Text('42', textDirection: TextDirection.ltr), 499 | ), 500 | ); 501 | 502 | expect(find.text('42'), findsOneWidget); 503 | 504 | expect( 505 | find.byType(MyInherited), 506 | matchesInOrder([ 507 | isA().having((s) => s.height, 'height', 24), 508 | ]), 509 | ); 510 | 511 | await tester.pumpWidget( 512 | MyInherited( 513 | height: 24, 514 | child: const Text('42', textDirection: TextDirection.ltr), 515 | ), 516 | ); 517 | 518 | expect(find.text('42'), findsOneWidget); 519 | 520 | expect( 521 | find.byType(MyInherited), 522 | matchesInOrder([ 523 | isA().having((s) => s.height, 'height', 24), 524 | ]), 525 | ); 526 | }); 527 | testWidgets('Nested with globalKeys', (tester) async { 528 | final firstKey = GlobalKey(debugLabel: 'first'); 529 | final secondKey = GlobalKey(debugLabel: 'second'); 530 | 531 | await tester.pumpWidget( 532 | Nested( 533 | children: [ 534 | MyStateful(key: firstKey), 535 | MyStateful(key: secondKey), 536 | ], 537 | child: Container(), 538 | ), 539 | ); 540 | 541 | // debugDumpApp(); 542 | 543 | expect( 544 | find.byType(MyStateful), 545 | matchesInOrder([ 546 | isA().having((s) => s.key, 'key', firstKey), 547 | isA().having((s) => s.key, 'key', secondKey), 548 | ]), 549 | ); 550 | 551 | await tester.pumpWidget( 552 | Nested( 553 | children: [ 554 | MyStateful(key: secondKey, didInit: () => throw Error()), 555 | MyStateful(key: firstKey, didInit: () => throw Error()), 556 | ], 557 | child: Container(), 558 | ), 559 | ); 560 | 561 | // print('\n\n'); 562 | 563 | // debugDumpApp(); 564 | 565 | expect( 566 | find.byType(MyStateful), 567 | matchesInOrder([ 568 | isA().having((s) => s.key, 'key', secondKey), 569 | isA().having((s) => s.key, 'key', firstKey), 570 | ]), 571 | ); 572 | }); 573 | testWidgets( 574 | 'SingleChildStatefulWidget can be used as mixin instead of base class', 575 | (tester) async { 576 | await tester.pumpWidget( 577 | Nested( 578 | children: [ 579 | ConcreteStateful(height: 24), 580 | ], 581 | child: const Text('42', textDirection: TextDirection.ltr), 582 | ), 583 | ); 584 | 585 | expect(find.text('42'), findsOneWidget); 586 | 587 | expect( 588 | find 589 | .byType(ConcreteStateful) 590 | .evaluate() 591 | .map((e) => (e as StatefulElement).state), 592 | matchesInOrder([ 593 | isA<_BaseStatefulState>() 594 | .having((s) => s.widget.height, 'widget.height', 24) 595 | .having((s) => s.width, 'width', 48), 596 | ]), 597 | ); 598 | 599 | await tester.pumpWidget( 600 | ConcreteStateful( 601 | height: 24, 602 | child: const Text('42', textDirection: TextDirection.ltr), 603 | ), 604 | ); 605 | 606 | expect(find.text('42'), findsOneWidget); 607 | 608 | expect( 609 | find 610 | .byType(ConcreteStateful) 611 | .evaluate() 612 | .map((e) => (e as StatefulElement).state), 613 | matchesInOrder([ 614 | isA<_BaseStatefulState>() 615 | .having((s) => s.widget.height, 'widget.height', 24) 616 | .having((s) => s.width, 'width', 48), 617 | ]), 618 | ); 619 | }); 620 | } 621 | 622 | class MyStateful extends SingleChildStatefulWidget { 623 | const MyStateful({Key? key, this.didBuild, this.didInit, Widget? child}) 624 | : super(key: key, child: child); 625 | 626 | final void Function(BuildContext, Widget?)? didBuild; 627 | final void Function()? didInit; 628 | 629 | @override 630 | _MyStatefulState createState() => _MyStatefulState(); 631 | } 632 | 633 | class _MyStatefulState extends SingleChildState { 634 | @override 635 | void initState() { 636 | super.initState(); 637 | widget.didInit?.call(); 638 | } 639 | 640 | @override 641 | Widget buildWithChild(BuildContext context, Widget? child) { 642 | widget.didBuild?.call(context, child); 643 | return child!; 644 | } 645 | } 646 | 647 | class MySizedBox extends SingleChildStatelessWidget { 648 | MySizedBox({Key? key, this.didBuild, this.height, Widget? child}) 649 | : super(key: key, child: child); 650 | 651 | final double? height; 652 | 653 | final void Function(BuildContext context, Widget? child)? didBuild; 654 | 655 | @override 656 | Widget buildWithChild(BuildContext context, Widget? child) { 657 | didBuild?.call(context, child); 658 | return child!; 659 | } 660 | 661 | @override 662 | void debugFillProperties(DiagnosticPropertiesBuilder properties) { 663 | super.debugFillProperties(properties); 664 | properties.add(DoubleProperty('height', height)); 665 | } 666 | } 667 | 668 | class MyInherited extends InheritedWidget implements SingleChildWidget { 669 | MyInherited({Key? key, this.height, required Widget child}) 670 | : super(key: key, child: child); 671 | 672 | final double? height; 673 | 674 | @override 675 | MyInheritedElement createElement() => MyInheritedElement(this); 676 | 677 | @override 678 | bool updateShouldNotify(MyInherited oldWidget) { 679 | return height != oldWidget.height; 680 | } 681 | } 682 | 683 | class MyInheritedElement extends InheritedElement 684 | with SingleChildWidgetElementMixin, SingleChildInheritedElementMixin { 685 | MyInheritedElement(MyInherited widget) : super(widget); 686 | 687 | @override 688 | MyInherited get widget => super.widget as MyInherited; 689 | } 690 | 691 | abstract class BaseStateless extends StatelessWidget { 692 | const BaseStateless({Key? key, this.height}) : super(key: key); 693 | 694 | final double? height; 695 | } 696 | 697 | class ConcreteStateless extends BaseStateless 698 | with SingleChildStatelessWidgetMixin { 699 | ConcreteStateless({Key? key, this.child, double? height}) 700 | : super(key: key, height: height); 701 | 702 | @override 703 | final Widget? child; 704 | 705 | @override 706 | Widget buildWithChild(BuildContext context, Widget? child) { 707 | return Container( 708 | height: height, 709 | child: child, 710 | ); 711 | } 712 | } 713 | 714 | abstract class BaseStateful extends StatefulWidget { 715 | const BaseStateful({Key? key, required this.height}) : super(key: key); 716 | 717 | final double height; 718 | @override 719 | _BaseStatefulState createState() => _BaseStatefulState(); 720 | 721 | Widget build(BuildContext context); 722 | } 723 | 724 | class _BaseStatefulState extends State { 725 | double? width; 726 | 727 | @override 728 | void initState() { 729 | super.initState(); 730 | width = widget.height * 2; 731 | } 732 | 733 | @override 734 | Widget build(BuildContext context) => widget.build(context); 735 | } 736 | 737 | class ConcreteStateful extends BaseStateful 738 | with SingleChildStatefulWidgetMixin { 739 | ConcreteStateful({Key? key, required double height, this.child}) 740 | : super(key: key, height: height); 741 | 742 | @override 743 | final Widget? child; 744 | 745 | @override 746 | Widget build(BuildContext context) => throw Error(); 747 | 748 | @override 749 | _ConcreteStatefulState createState() => _ConcreteStatefulState(); 750 | } 751 | 752 | class _ConcreteStatefulState extends _BaseStatefulState 753 | with SingleChildStateMixin { 754 | @override 755 | Widget buildWithChild(BuildContext context, Widget child) { 756 | return SizedBox(height: widget.height, width: width, child: child); 757 | } 758 | } 759 | --------------------------------------------------------------------------------