// 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/rendering.dart'; import 'package:flutter/services.dart'; import 'package:flutter/src/widgets/basic.dart'; import 'package:flutter/src/widgets/framework.dart'; import 'package:flutter/src/widgets/layout_builder.dart'; import 'package:flutter/src/widgets/media_query.dart'; import 'package:flutter/src/widgets/scroll_view.dart'; import 'package:flutter/src/widgets/sliver_layout_builder.dart'; import 'package:flutter_test/flutter_test.dart'; class Wrapper extends StatelessWidget { const Wrapper({ super.key, required this.child, }) : assert(child != null); final Widget child; @override Widget build(BuildContext context) => child; } void main() { testWidgets('Moving a global key from another LayoutBuilder at layout time', (WidgetTester tester) async { final GlobalKey victimKey = GlobalKey(); await tester.pumpWidget(Row( textDirection: TextDirection.ltr, children: <Widget>[ Wrapper( child: LayoutBuilder(builder: (BuildContext context, BoxConstraints constraints) { return const SizedBox(); }), ), Wrapper( child: Wrapper( child: LayoutBuilder(builder: (BuildContext context, BoxConstraints constraints) { return Wrapper( child: SizedBox(key: victimKey), ); }), ), ), ], )); await tester.pumpWidget(Row( textDirection: TextDirection.ltr, children: <Widget>[ Wrapper( child: LayoutBuilder(builder: (BuildContext context, BoxConstraints constraints) { return Wrapper( child: SizedBox(key: victimKey), ); }), ), Wrapper( child: Wrapper( child: LayoutBuilder(builder: (BuildContext context, BoxConstraints constraints) { return const SizedBox(); }), ), ), ], )); expect(tester.takeException(), null); }); testWidgets('Moving a global key from another SliverLayoutBuilder at layout time', (WidgetTester tester) async { final GlobalKey victimKey1 = GlobalKey(); final GlobalKey victimKey2 = GlobalKey(); await tester.pumpWidget( Directionality( textDirection: TextDirection.ltr, child: CustomScrollView( slivers: <Widget>[ SliverLayoutBuilder( builder: (BuildContext context, SliverConstraints constraint) { return SliverPadding(key: victimKey1, padding: const EdgeInsets.fromLTRB(1, 2, 3, 4)); }, ), SliverLayoutBuilder( builder: (BuildContext context, SliverConstraints constraint) { return SliverPadding(key: victimKey2, padding: const EdgeInsets.fromLTRB(5, 7, 11, 13)); }, ), SliverLayoutBuilder( builder: (BuildContext context, SliverConstraints constraint) { return const SliverPadding(padding: EdgeInsets.fromLTRB(5, 7, 11, 13)); }, ), ], ), ), ); await tester.pumpWidget( Directionality( textDirection: TextDirection.ltr, child: CustomScrollView( slivers: <Widget>[ SliverLayoutBuilder( builder: (BuildContext context, SliverConstraints constraint) { return SliverPadding(key: victimKey2, padding: const EdgeInsets.fromLTRB(1, 2, 3, 4)); }, ), SliverLayoutBuilder( builder: (BuildContext context, SliverConstraints constraint) { return const SliverPadding(padding: EdgeInsets.fromLTRB(5, 7, 11, 13)); }, ), SliverLayoutBuilder( builder: (BuildContext context, SliverConstraints constraint) { return SliverPadding(key: victimKey1, padding: const EdgeInsets.fromLTRB(5, 7, 11, 13)); }, ), ], ), ), ); expect(tester.takeException(), null); }); testWidgets('LayoutBuilder does not layout twice', (WidgetTester tester) async { // This widget marks itself dirty when the closest MediaQuery changes. final _LayoutCount widget = _LayoutCount(); late StateSetter setState; bool updated = false; await tester.pumpWidget( Directionality( textDirection: TextDirection.ltr, child: StatefulBuilder( builder: (BuildContext context, StateSetter setter) { setState = setter; return MediaQuery( data: updated ? const MediaQueryData(platformBrightness: Brightness.dark) : const MediaQueryData(), child: LayoutBuilder( builder: (BuildContext context, BoxConstraints constraints) { return Center( child: SizedBox.square( dimension: 20, child: Center( child: SizedBox.square( dimension: updated ? 10 : 20, child: widget, ), ), ), ); }, ), ); } ), ), ); assert(widget._renderObject.layoutCount == 1); setState(() { updated = true; }); await tester.pump(); expect(widget._renderObject.layoutCount, 2); }); } class _LayoutCount extends LeafRenderObjectWidget { late final _RenderLayoutCount _renderObject; @override RenderObject createRenderObject(BuildContext context) { return _renderObject = _RenderLayoutCount(MediaQuery.of(context)); } @override void updateRenderObject(BuildContext context, _RenderLayoutCount renderObject) { renderObject.mediaQuery = MediaQuery.of(context); } } class _RenderLayoutCount extends RenderProxyBox { _RenderLayoutCount(this._mediaQuery); int layoutCount = 0; MediaQueryData get mediaQuery => _mediaQuery; MediaQueryData _mediaQuery; set mediaQuery(MediaQueryData newValue) { if (newValue != _mediaQuery) { _mediaQuery = newValue; markNeedsLayout(); } } @override bool get sizedByParent => true; @override void performLayout() { layoutCount += 1; } }