Search code examples
flutterflutter-layoutflutter-web

How to align/render flutter widgets around a single widget?


So basically this is what I'm trying to achieve

enter image description here

WITH THE WIDGET WITH THE RED X BEING THE ROOT WIDGET IN WHICH ALL WIDGETS NEEDS TO BE BUILT AROUND IT

All the widgets here are going to be 'rendered' dynamically which means I get their informations from somewhere, so to achieve this I thought of two options:

  • Use the Stack widget or another mysterious layout widget I did not find yet
  • Use simple columns and rows in which this has created a problem: this is the code I'm using
 Widget panelWidget(Panel panel) {
   return Row(
     mainAxisAlignment: MainAxisAlignment.center,
     crossAxisAlignment: CrossAxisAlignment.center,
     children: [
       //WIDGETS ON THE LEFT
       ...panel.attachedPanels
           .where((element) => element.attachedToSide == 3)
           .map(
             (e) => Container(
               height: getH(e.panelHeight),
               width: getW(e.panelWidth),
               decoration:
                   BoxDecoration(border: Border.all(color: Colors.black)),
             ),
           )
           .toList(),
       Column(
         mainAxisAlignment: MainAxisAlignment.center,
         children: [
           //WIDGETS ON TOP
           ...panel.attachedPanels
               .where((element) => element.attachedToSide == 2)
               .map(
                 (e) => Container(
                   height: getH(e.panelHeight),
                   width: getW(e.panelWidth),
                   decoration:
                       BoxDecoration(border: Border.all(color: Colors.black)),
                 ),
               )
               .toList(),
           Center( //THIS IS THE ROOT WIDGET
             child: Container(
               height: getH(panel.panelHeight),
               width: getW(panel.panelWidth),
               decoration:
                   BoxDecoration(border: Border.all(color: Colors.black)),
             ),
           ),
           //WIDGETS ON THE BOTTOM
           ...panel.attachedPanels
               .where((element) => element.attachedToSide == 0)
               .map(
                 (e) => Container(
                   height: getH(e.panelHeight),
                   width: getW(e.panelWidth),
                   decoration:
                       BoxDecoration(border: Border.all(color: Colors.black)),
                 ),
               )
               .toList(),
         ],
       ),
       //WIDGETS ON THE RITGHT
       ...panel.attachedPanels
           .where((element) => element.attachedToSide == 1)
           .map(
             (e) => Container(
               height: getH(e.panelHeight),
               width: getW(e.panelWidth),
               decoration:
                   BoxDecoration(border: Border.all(color: Colors.black)),
             ),
           )
           .toList(),
     ],
   );
 }

please mind the logic as I'm focused now on figuring this out first, you can see bellow the root widget commented and around it from 4 sides there is the list of the widgets I need to render, on paper it looks fine but this is the output, I'm using Flutter Web : enter image description here

As you can see the left and right widgets are NOT aligned with the root widget because they are following the Row's cross axis alignment. Flutter is doing it's job of putting these 2 widgets at the center but the widgets don't have the same size thus shifting the center point a bit for the whole widget tree, I tried different combinations but they all did not work, Any help is highly welcomed


Solution

  • Use the Table widget, which does what you're trying to do for you.

    Don't think of building a widget off to the side of another "root" widget. Think of everything in terms of a tree where everything has a parent-child/children relationship. Each of your widgets is at the same level in the tree, they just need the right parent to put them in the right place.

    Your desired layout using Placeholders in the Table is implemented in the following code. It can be copy-pasted into DartPad.

    import 'package:flutter/material.dart';
    
    final Color darkBlue = Color.fromARGB(255, 18, 32, 47);
    
    void main() {
      runApp(MyApp());
    }
    
    class MyApp extends StatelessWidget {
      @override
      Widget build(BuildContext context) {
        return MaterialApp(
          theme: ThemeData.dark().copyWith(scaffoldBackgroundColor: darkBlue),
          debugShowCheckedModeBanner: false,
          home: Scaffold(
            body: Center(
              child: MyWidget(),
            ),
          ),
        );
      }
    }
    
    class MyWidget extends StatelessWidget {
      @override
      Widget build(BuildContext context) {
        return Table(
          defaultVerticalAlignment: TableCellVerticalAlignment.middle,
          children: <TableRow>[
            TableRow(
              children: <Widget>[
                const SizedBox(),
                SizedBox(
                  height: 100,
                  child: Placeholder(color: Colors.white),
                ),
                const SizedBox(),
              ],
            ),
            TableRow(
              children: <Widget>[
                SizedBox(
                  height: 100,
                  child: Placeholder(color: Colors.white),
                ),
                SizedBox(
                  height: 100,
                  child: Placeholder(color: Colors.white),
                ),
                SizedBox(
                  height: 100,
                  child: Placeholder(color: Colors.white),
                ),
              ],
            ),
            TableRow(
              children: <Widget>[
                const SizedBox(),
                SizedBox(
                  height: 100,
                  child: Placeholder(color: Colors.white),
                ),
                const SizedBox(),
              ],
            ),
          ],
        );
      }
    }
    

    Result: Result from above code


    For accommodating a "dynamic" layout, the changes to the above code are trivial. The following adjusts MyWidget to take optional top, bottom, left, and right widgets and adjusts everything accordingly so that the Table works correctly.

    import 'package:flutter/material.dart';
    
    final Color darkBlue = Color.fromARGB(255, 18, 32, 47);
    
    void main() {
      runApp(MyApp());
    }
    
    class MyApp extends StatelessWidget {
      @override
      Widget build(BuildContext context) {
        return MaterialApp(
          theme: ThemeData.dark().copyWith(scaffoldBackgroundColor: darkBlue),
          debugShowCheckedModeBanner: false,
          home: Scaffold(
            body: Center(
              child: MyWidget(
                root: SizedBox(
                  height: 100,
                  child: Placeholder(color: Colors.white),
                ),
                top: SizedBox(
                  height: 100,
                  child: Placeholder(color: Colors.white),
                ),
                right: SizedBox(
                  height: 100,
                  child: Placeholder(color: Colors.white),
                ),
              ),
            ),
          ),
        );
      }
    }
    
    class MyWidget extends StatelessWidget {
      final Widget root;
      final Widget? right, left, top, bottom;
    
      const MyWidget({
        required this.root,
        this.right,
        this.left,
        this.top,
        this.bottom,
      });
    
      @override
      Widget build(BuildContext context) {
        final List<Widget> middleRow = [
          if(left != null)
            left!,
          root,
          if(right != null)
            right!,
        ];
        
        final List<Widget> topRow = [
          if(left != null)
            const SizedBox(),
          top ?? const SizedBox(),
          if(right != null)
            const SizedBox(),
        ];
        
        final List<Widget> bottomRow = [
          if(left != null)
            const SizedBox(),
          bottom ?? const SizedBox(),
          if(right != null)
            const SizedBox(),
        ];
        
        return Table(
          defaultVerticalAlignment: TableCellVerticalAlignment.middle,
          children: <TableRow>[
            TableRow(
              children: topRow,
            ),
            TableRow(
              children: middleRow,
            ),
            TableRow(
              children: bottomRow,
            ),
          ],
        );
      }
    }
    

    "Dynamic" example result

    Or if you want empty space where widgets are omitted:

    import 'package:flutter/material.dart';
    
    final Color darkBlue = Color.fromARGB(255, 18, 32, 47);
    
    void main() {
      runApp(MyApp());
    }
    
    class MyApp extends StatelessWidget {
      @override
      Widget build(BuildContext context) {
        return MaterialApp(
          theme: ThemeData.dark().copyWith(scaffoldBackgroundColor: darkBlue),
          debugShowCheckedModeBanner: false,
          home: Scaffold(
            body: Center(
              child: MyWidget(
                root: SizedBox(
                  height: 100,
                  child: Placeholder(color: Colors.white),
                ),
                top: SizedBox(
                  height: 100,
                  child: Placeholder(color: Colors.white),
                ),
                right: SizedBox(
                  height: 100,
                  child: Placeholder(color: Colors.white),
                ),
              ),
            ),
          ),
        );
      }
    }
    
    class MyWidget extends StatelessWidget {
      final Widget root;
      final Widget? right, left, top, bottom;
    
      const MyWidget({
        required this.root,
        this.right,
        this.left,
        this.top,
        this.bottom,
      });
    
      @override
      Widget build(BuildContext context) {
        final List<Widget> middleRow = [
          left ?? const SizedBox(),
          root,
          right ?? const SizedBox(),
        ];
        
        final List<Widget> topRow = [
          const SizedBox(),
          top ?? const SizedBox(),
          const SizedBox(),
        ];
        
        final List<Widget> bottomRow = [
          const SizedBox(),
          bottom ?? const SizedBox(),
          const SizedBox(),
        ];
        
        return Table(
          defaultVerticalAlignment: TableCellVerticalAlignment.middle,
          children: <TableRow>[
            TableRow(
              children: topRow,
            ),
            TableRow(
              children: middleRow,
            ),
            TableRow(
              children: bottomRow,
            ),
          ],
        );
      }
    }
    

    Empty space example result