Skip to content

Instantly share code, notes, and snippets.

@JohanScheepers
Last active September 7, 2025 15:05
Show Gist options
  • Select an option

  • Save JohanScheepers/58c1aab94b4af6d0c61b516a78f91893 to your computer and use it in GitHub Desktop.

Select an option

Save JohanScheepers/58c1aab94b4af6d0c61b516a78f91893 to your computer and use it in GitHub Desktop.
Use of MultiChildRenderObjectWidget layout widgets dependent on size
/*--------------------------------------------------------------------------------
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