Last active
September 7, 2025 15:05
-
-
Save JohanScheepers/58c1aab94b4af6d0c61b516a78f91893 to your computer and use it in GitHub Desktop.
Use of MultiChildRenderObjectWidget layout widgets dependent on size
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| /*-------------------------------------------------------------------------------- | |
| A talk on MultiChildRenderObjectWidget @FlutterNFriends 2025 by Ingvild Sandstad | |
| https://gist.github.com/JohanScheepers/58c1aab94b4af6d0c61b516a78f91893 | |
| --------------------------------------------------------------------------------*/ | |
| import 'dart:math' as math; | |
| import 'package:flutter/material.dart'; | |
| import 'package:flutter/rendering.dart'; | |
| void main() { | |
| runApp(const MyApp()); | |
| } | |
| class MyApp extends StatelessWidget { | |
| const MyApp({super.key}); | |
| // This widget is the root of your application. | |
| @override | |
| Widget build(BuildContext context) { | |
| return MaterialApp( | |
| title: 'Multi Child Render Object Widget', | |
| theme: ThemeData( | |
| colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple), | |
| ), | |
| home: const MyHomePage(title: 'Multi Child Render Object Widget'), | |
| ); | |
| } | |
| } | |
| class MyHomePage extends StatefulWidget { | |
| const MyHomePage({super.key, required this.title}); | |
| final String title; | |
| @override | |
| State<MyHomePage> createState() => _MyHomePageState(); | |
| } | |
| class _MyHomePageState extends State<MyHomePage> { | |
| @override | |
| Widget build(BuildContext context) { | |
| return Scaffold( | |
| appBar: AppBar( | |
| backgroundColor: Theme.of(context).colorScheme.inversePrimary, | |
| title: Text(widget.title), | |
| ), | |
| body: Padding( | |
| padding: EdgeInsets.all(10), | |
| child: SingleChildScrollView( | |
| child: Column( | |
| mainAxisAlignment: MainAxisAlignment.center, | |
| crossAxisAlignment: CrossAxisAlignment.start, | |
| children: [ | |
| BlackBoxContainer( | |
| child: MaterialButton( | |
| child: Text( | |
| 'Left Right Alignment', | |
| style: TextStyle(fontSize: 20, color: Colors.white), | |
| ), | |
| onPressed: () { | |
| Navigator.push( | |
| context, | |
| MaterialPageRoute( | |
| builder: (context) => const LeftAlignmentExample(), | |
| ), | |
| ); | |
| }, | |
| ), | |
| ), | |
| BlackBoxContainer( | |
| child: MaterialButton( | |
| child: Text( | |
| 'Text and Icons', | |
| style: TextStyle(fontSize: 20, color: Colors.white), | |
| ), | |
| onPressed: () { | |
| Navigator.push( | |
| context, | |
| MaterialPageRoute( | |
| builder: (context) => const TextAndIconsExample(), | |
| ), | |
| ); | |
| }, | |
| ), | |
| ), | |
| ], | |
| ), | |
| ), | |
| ), | |
| ); | |
| } | |
| } | |
| class LeftAlignmentExample extends StatefulWidget { | |
| const LeftAlignmentExample({super.key}); | |
| @override | |
| State<LeftAlignmentExample> createState() => _LeftAlignmentExampleState(); | |
| } | |
| class _LeftAlignmentExampleState extends State<LeftAlignmentExample> { | |
| String leftText = 'Left Side '; | |
| String rightText = 'Right Side '; | |
| String addOn = 'gets longer '; | |
| String addOn2 = 'and longer '; | |
| int count = 0; | |
| void _buttonPressed() { | |
| setState(() { | |
| if (count == 0) { | |
| leftText += addOn; | |
| } else if (count == 1) { | |
| rightText += addOn; | |
| } else if (count > 1 && count < 4) { | |
| leftText += addOn2; | |
| } else { | |
| leftText = 'Left Side '; | |
| rightText = 'Right Side '; | |
| count = -1; | |
| } | |
| count++; | |
| }); | |
| } | |
| @override | |
| Widget build(BuildContext context) { | |
| return Scaffold( | |
| appBar: AppBar( | |
| title: Title(color: Colors.blue, child: Text('Left Right Alignment')), | |
| ), | |
| body: SingleChildScrollView( | |
| child: Center( | |
| child: Padding( | |
| padding: EdgeInsetsGeometry.all(10), | |
| child: Column( | |
| children: [ | |
| BlackBoxContainer( | |
| child: LeftRightAlignmentWidget( | |
| left: Text( | |
| leftText, | |
| style: TextStyle(fontSize: 20, color: Colors.white), | |
| ), | |
| right: Text( | |
| rightText, | |
| style: TextStyle(fontSize: 20, color: Colors.white), | |
| ), | |
| justifyVertically: true, | |
| ), | |
| ), | |
| BlackBoxContainer( | |
| child: LeftRightAlignmentWidget( | |
| left: Text( | |
| leftText, | |
| style: TextStyle(fontSize: 60, color: Colors.white), | |
| ), | |
| right: Text( | |
| rightText, | |
| style: TextStyle(fontSize: 20, color: Colors.white), | |
| ), | |
| justifyVertically: true, | |
| ), | |
| ), | |
| BlackBoxContainer( | |
| child: LeftRightAlignmentWidget( | |
| left: Text( | |
| leftText, | |
| style: TextStyle(fontSize: 20, color: Colors.white), | |
| ), | |
| right: Text( | |
| rightText, | |
| style: TextStyle(fontSize: 60, color: Colors.white), | |
| ), | |
| justifyVertically: true, | |
| ), | |
| ), | |
| ], | |
| ), | |
| ), | |
| ), | |
| ), | |
| floatingActionButton: FloatingActionButton( | |
| onPressed: _buttonPressed, | |
| tooltip: 'Increment', | |
| child: const Icon(Icons.add), | |
| ), | |
| ); | |
| } | |
| } | |
| class LeftRightAlignmentWidget extends MultiChildRenderObjectWidget { | |
| final bool justifyVertically; | |
| LeftRightAlignmentWidget({ | |
| super.key, | |
| required Widget left, | |
| required Widget right, | |
| this.justifyVertically = false, | |
| }) : super(children: [left, right]); | |
| @override | |
| LeftRightAlignmentRenderBox createRenderObject(BuildContext context) { | |
| return LeftRightAlignmentRenderBox(justifyVertically: justifyVertically); | |
| } | |
| @override | |
| updateRenderObject( | |
| BuildContext context, | |
| covariant LeftRightAlignmentRenderBox renderObject, | |
| ) { | |
| renderObject.justifyVertically = justifyVertically; | |
| } | |
| } | |
| class LeftRightAlignmentParentData extends ContainerBoxParentData<RenderBox> {} | |
| class LeftRightAlignmentRenderBox extends RenderBox | |
| with | |
| ContainerRenderObjectMixin<RenderBox, LeftRightAlignmentParentData>, | |
| RenderBoxContainerDefaultsMixin< | |
| RenderBox, | |
| LeftRightAlignmentParentData | |
| > { | |
| bool justifyVertically; | |
| LeftRightAlignmentRenderBox({ | |
| List<RenderBox>? children, | |
| required this.justifyVertically, | |
| }) { | |
| addAll(children ?? []); | |
| } | |
| @override | |
| void setupParentData(covariant RenderObject child) { | |
| if (child.parentData is! LeftRightAlignmentParentData) { | |
| child.parentData = LeftRightAlignmentParentData(); | |
| } | |
| } | |
| @override | |
| void performLayout() { | |
| final RenderBox? leftChild = firstChild; | |
| final RenderBox? rightChild = leftChild != null | |
| ? childAfter(leftChild) | |
| : null; | |
| // Safeguard: if either child is missing, size to smallest constraints | |
| if (leftChild == null || rightChild == null) { | |
| size = constraints.smallest; | |
| return; | |
| } | |
| // Lay out both children with the same constraints as this RenderBox | |
| leftChild.layout(constraints, parentUsesSize: true); | |
| rightChild.layout(constraints, parentUsesSize: true); | |
| // Size this RenderBox to fit both children side by side | |
| final Size leftChildSize = leftChild.size; | |
| final Size rightChildSize = rightChild.size; | |
| //Calculate if children need to be wrapped | |
| final bool wrapped = | |
| leftChildSize.width + rightChildSize.width > constraints.maxWidth; | |
| final double maxHeight = wrapped | |
| ? leftChildSize.height + rightChildSize.height | |
| : math.max(leftChildSize.height, rightChildSize.height); | |
| final LeftRightAlignmentParentData leftParentData = | |
| leftChild.parentData as LeftRightAlignmentParentData; | |
| final LeftRightAlignmentParentData rightParentData = | |
| rightChild.parentData as LeftRightAlignmentParentData; | |
| //Position children where we want them based on if they are wrapped or not | |
| leftParentData.offset = Offset( | |
| 0, | |
| !wrapped && justifyVertically | |
| ? (maxHeight - leftChildSize.height) / 2 | |
| : 0, | |
| ); | |
| rightParentData.offset = Offset( | |
| constraints.maxWidth - rightChildSize.width, | |
| wrapped | |
| ? leftChildSize.height | |
| : justifyVertically | |
| ? (maxHeight - rightChildSize.height) / 2 | |
| : 0, | |
| ); | |
| size = Size(constraints.maxWidth, maxHeight); | |
| } | |
| @override | |
| bool hitTestChildren(BoxHitTestResult result, {required Offset position}) { | |
| return defaultHitTestChildren(result, position: position); | |
| } | |
| @override | |
| void paint(PaintingContext context, Offset offset) { | |
| defaultPaint(context, offset); | |
| } | |
| } | |
| class TextAndIconsExample extends StatefulWidget { | |
| const TextAndIconsExample({super.key}); | |
| @override | |
| State<TextAndIconsExample> createState() => _TextAndIconsExampleState(); | |
| } | |
| class _TextAndIconsExampleState extends State<TextAndIconsExample> { | |
| @override | |
| Widget build(BuildContext context) { | |
| return Scaffold( | |
| appBar: AppBar(title: Text('Text and Icons')), | |
| body: SingleChildScrollView( | |
| child: BlackBoxContainer( | |
| child: MonitorAlignmentContent( | |
| first: Padding( | |
| padding: const EdgeInsets.all(8.0), | |
| child: Text('time', style: TextStyle(color: Colors.white)), | |
| ), | |
| second: Icon(Icons.face_2, color: Colors.white,size: 40,), | |
| third: Padding( | |
| padding: const EdgeInsets.all(8.0), | |
| child: Text('new time', style: TextStyle(color: Colors.white)), | |
| ), | |
| fourth: Padding( | |
| padding: const EdgeInsets.all(8.0), | |
| child: Text('track', style: TextStyle(color: Colors.white)), | |
| ), | |
| fifth: Padding( | |
| padding: const EdgeInsets.all(8.0), | |
| child: Text('message', style: TextStyle(color: Colors.white)), | |
| ), | |
| ), | |
| ), | |
| ), | |
| ); | |
| } | |
| } | |
| class MonitorAlignmentContent extends MultiChildRenderObjectWidget { | |
| MonitorAlignmentContent({ | |
| super.key, | |
| required Widget first, | |
| required Widget second, | |
| required Widget third, | |
| required Widget fourth, | |
| required Widget fifth, | |
| }) : super(children: [first, second, third, fourth, fifth]); | |
| @override | |
| RenderMonitorAlignmentContent createRenderObject(BuildContext context) { | |
| return RenderMonitorAlignmentContent(); | |
| } | |
| @override | |
| void updateRenderObject( | |
| BuildContext context, | |
| RenderMonitorAlignmentContent renderObject, | |
| ) { | |
| renderObject.checkChildrenSize(children); | |
| } | |
| } | |
| class MonitorAlignmentParentData extends ContainerBoxParentData<RenderBox> {} | |
| class RenderMonitorAlignmentContent extends RenderBox | |
| with | |
| ContainerRenderObjectMixin<RenderBox, MonitorAlignmentParentData>, | |
| RenderBoxContainerDefaultsMixin<RenderBox, MonitorAlignmentParentData> { | |
| List<Size> previousSizes = []; | |
| RenderMonitorAlignmentContent({List<RenderBox>? children}) { | |
| addAll(children ?? []); | |
| } | |
| void checkChildrenSize(List<Widget>? children) { | |
| final List<RenderBox?> children = []; | |
| visitChildren((RenderObject child) { | |
| children.add(child as RenderBox); | |
| }); | |
| if (children.isNotEmpty) { | |
| for (int i = 0; i < children.length; i++) { | |
| if (children[i] == null || children[i]!.hasSize) { | |
| continue; | |
| } | |
| Size newSize = Size(children[i]!.size.width, children[i]!.size.height); | |
| if (previousSizes[i] != newSize) { | |
| previousSizes[i] = newSize; | |
| markNeedsLayout(); | |
| } | |
| } | |
| } | |
| } | |
| @override | |
| void setupParentData(RenderObject child) { | |
| if (child.parentData is! MonitorAlignmentParentData) { | |
| child.parentData = MonitorAlignmentParentData(); | |
| } | |
| } | |
| @override | |
| void performLayout() { | |
| /* | |
| lay out children 1-4, ([time],[line + station],[new time],[track]) in a row, and the 5th child ([message}) underneath. | |
| ------------------------------------------------------ ---------------- | |
| | [time] [line + station] [new time] [track] | |[1] [2] [3] [4]| | |
| ----------------------------------------------------- or simplified as ---------------- | |
| | [message] | | [5] | | |
| ------------------------------------------------------ ---------------- | |
| If the total of [time] [line + station] [new time] [track] is greater than the constraints.maxWidth,, they will wrap in [time] [new time] with [line + station] taking all space not needed by [track] | |
| ---------------------------- ----------------- | |
| | [time] [new time] | |[1] [3]| | |
| ---------------------------- ----------------- | |
| | [line + station] [track]| or simplified as | [2] [4] | | |
| --------------------------- ----------------- | |
| | [message] | | [5] | | |
| --------------------------- ----------------- | |
| */ | |
| final BoxConstraints childConstraints = constraints.loosen(); | |
| final RenderBox? child1 = firstChild; | |
| final RenderBox? child2 = childAfter(child1!); | |
| final RenderBox? child3 = childAfter(child2!); | |
| final RenderBox? child4 = childAfter(child3!); | |
| final RenderBox? child5 = childAfter(child4!); | |
| child1.layout(childConstraints, parentUsesSize: true); | |
| child3.layout(childConstraints, parentUsesSize: true); | |
| child4.layout(childConstraints, parentUsesSize: true); | |
| child2.layout(childConstraints, parentUsesSize: true); | |
| final double child1Width = child1.size.width; | |
| final double child2Width = child2.size.width; | |
| final double child3Width = child3.size.width; | |
| final double child4Width = child4.size.width; | |
| final bool wrapped = | |
| child1Width + child2Width + child3Width + child4Width > | |
| constraints.maxWidth; | |
| if (wrapped && child2Width+ child4Width> constraints.maxWidth) { | |
| child2.layout( | |
| BoxConstraints(maxWidth: constraints.maxWidth - child4Width), | |
| parentUsesSize: true, | |
| ); | |
| } | |
| final MonitorAlignmentParentData child1ParentData = | |
| child1.parentData as MonitorAlignmentParentData; | |
| final MonitorAlignmentParentData child2ParentData = | |
| child2.parentData as MonitorAlignmentParentData; | |
| final MonitorAlignmentParentData child3ParentData = | |
| child3.parentData as MonitorAlignmentParentData; | |
| final MonitorAlignmentParentData child4ParentData = | |
| child4.parentData as MonitorAlignmentParentData; | |
| final double child1Height = child1.size.height; | |
| final double child2Height = child2.size.height; | |
| final verticalPadding = 8.0; | |
| if (wrapped) { | |
| child1ParentData.offset = const Offset(0.0, 0.0); | |
| child2ParentData.offset = Offset(0.0, child1Height + verticalPadding); | |
| child3ParentData.offset = Offset(constraints.maxWidth - child3Width, 0.0); | |
| child4ParentData.offset = Offset( | |
| constraints.maxWidth - child4Width, | |
| child1Height + verticalPadding + ((child2Height - child1Height) / 2), | |
| ); | |
| } else { | |
| child1ParentData.offset = Offset(0, ((child2Height - child1Height) / 2)); | |
| child2ParentData.offset = Offset(child1Width, 0); | |
| child3ParentData.offset = Offset( | |
| constraints.maxWidth - (child3Width + child4Width), | |
| ((child2Height - child1Height) / 2), | |
| ); | |
| child4ParentData.offset = Offset( | |
| constraints.maxWidth - child4Width, | |
| ((child2Height - child1Height) / 2), | |
| ); | |
| } | |
| double currentHeight = wrapped | |
| ? child1Height + child2Height + verticalPadding | |
| : math.max(child1Height, child2Height); | |
| child5?.layout(childConstraints, parentUsesSize: true); | |
| final double child5Height = child5?.size.height ?? 0.0; | |
| final MonitorAlignmentParentData child5ParentData = | |
| child5?.parentData as MonitorAlignmentParentData; | |
| child5ParentData.offset = Offset(0, currentHeight + verticalPadding); | |
| currentHeight = child5Height > 0.0 | |
| ? currentHeight + child5Height + verticalPadding | |
| : currentHeight; | |
| previousSizes = []; | |
| previousSizes.add(child1.size); | |
| previousSizes.add(child2.size); | |
| previousSizes.add(child3.size); | |
| previousSizes.add(child4.size); | |
| previousSizes.add(child5?.size ?? const Size(0.0, 0.0)); | |
| size = Size(constraints.maxWidth, currentHeight); | |
| } | |
| @override | |
| bool hitTestChildren(BoxHitTestResult result, {required Offset position}) { | |
| return defaultHitTestChildren(result, position: position); | |
| } | |
| @override | |
| void paint(PaintingContext context, Offset offset) { | |
| defaultPaint(context, offset); | |
| } | |
| } | |
| class BlackBoxContainer extends StatelessWidget { | |
| const BlackBoxContainer({super.key, required this.child}); | |
| final Widget child; | |
| @override | |
| Widget build(BuildContext context) { | |
| return Padding( | |
| padding: const EdgeInsets.all(16.0), | |
| child: Container( | |
| padding: EdgeInsets.all(8), | |
| decoration: BoxDecoration( | |
| color: Colors.black, | |
| border: Border.all(color: Colors.blue, width: 5), | |
| borderRadius: BorderRadius.all(Radius.circular(10)), | |
| ), | |
| child: child, | |
| ), | |
| ); | |
| } | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment