🏠 Home / Hub

Flutter 03 — Layout & Flex

Flutter has no CSS. Layouts are built entirely from widgets. Master these layout widgets and you can build any UI.

1. Layout Concepts

Flutter's layout system is based on constraints flowing down and sizes flowing up. A parent passes constraints (min/max width and height) to its children, the children decide their size within those constraints, and the parent positions them.

CategoryWidgetsDescription
Single-childContainer, Center, Padding, Align, SizedBox, FittedBox, AspectRatioWraps one child widget
Multi-childColumn, Row, Stack, Wrap, FlowArranges multiple children
ScrollableListView, GridView, SingleChildScrollView, CustomScrollViewChildren can scroll
Flex helpersExpanded, Flexible, SpacerFill remaining space in Row/Column
There is no CSS, no floats, no absolute positioning by default. Every layout choice is an explicit widget. This makes layouts predictable and testable.

2. Column

Column arranges children vertically (top to bottom). Its main axis is vertical; cross axis is horizontal.

Column(
  // mainAxisAlignment — vertical spacing of children
  mainAxisAlignment: MainAxisAlignment.center,
  // Options: start | end | center | spaceBetween | spaceAround | spaceEvenly

  // crossAxisAlignment — horizontal alignment of children
  crossAxisAlignment: CrossAxisAlignment.stretch,
  // Options: start | end | center | stretch | baseline

  // How much vertical space the Column occupies
  mainAxisSize: MainAxisSize.min,   // shrink-wrap (default: max = fill all height)

  children: [
    const Text('First item'),
    const SizedBox(height: 16),   // spacing
    const Text('Second item'),
    const SizedBox(height: 16),
    ElevatedButton(
      onPressed: () {},
      child: const Text('Button'),
    ),
  ],
)

// MainAxisAlignment options visualised:
// start       [A B C         ]
// end         [         A B C]
// center      [    A B C     ]
// spaceBetween[A     B     C ]
// spaceAround [ A   B   C   ]
// spaceEvenly [  A   B   C  ]

3. Row

Row arranges children horizontally (left to right). Works exactly like Column but rotated: main axis is horizontal, cross axis is vertical.

Row(
  mainAxisAlignment: MainAxisAlignment.spaceBetween,
  crossAxisAlignment: CrossAxisAlignment.center,
  children: [
    // Leading icon
    const Icon(Icons.menu, size: 28),

    // Title (middle)
    const Text('My App', style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold)),

    // Actions (trailing)
    Row(
      children: [
        IconButton(onPressed: () {}, icon: const Icon(Icons.search)),
        IconButton(onPressed: () {}, icon: const Icon(Icons.more_vert)),
      ],
    ),
  ],
)

// Row overflow — if children are too wide, use Flexible or Expanded
// or wrap in SingleChildScrollView with scrollDirection: Axis.horizontal

Row(
  children: [
    Expanded(child: Text('Long text that takes remaining space')),
    const SizedBox(width: 8),
    const Icon(Icons.chevron_right),
  ],
)
If a Row overflows (yellow-black stripe warning), wrap the wide child in Flexible or Expanded, or place the Row inside a SingleChildScrollView(scrollDirection: Axis.horizontal).

4. Stack & Positioned

Stack overlays children on top of each other (like CSS position: relative). Use Positioned to place children at specific offsets within the Stack.

Stack(
  // How to size non-positioned children
  fit: StackFit.loose,     // children choose own size (default)
  // fit: StackFit.expand, // children fill the stack

  // Alignment of non-positioned children
  alignment: Alignment.center,

  children: [
    // Bottom layer — full background image
    Image.network(
      'https://picsum.photos/400/300',
      width: 400,
      height: 300,
      fit: BoxFit.cover,
    ),

    // Middle layer — semi-transparent overlay
    Container(
      width: 400,
      height: 300,
      color: Colors.black45,
    ),

    // Top layer — positioned text at bottom-left
    const Positioned(
      left: 16,
      bottom: 16,
      child: Text(
        'Caption Text',
        style: TextStyle(color: Colors.white, fontSize: 20),
      ),
    ),

    // Badge at top-right
    const Positioned(
      right: 8,
      top: 8,
      child: CircleAvatar(
        radius: 12,
        backgroundColor: Colors.red,
        child: Text('3', style: TextStyle(color: Colors.white, fontSize: 11)),
      ),
    ),
  ],
)

// Positioned parameters
Positioned(
  left: 10,     // from left edge
  top: 10,      // from top edge
  right: 10,    // from right edge (or use width)
  bottom: 10,   // from bottom edge (or use height)
  width: 100,   // explicit width (optional)
  height: 50,   // explicit height (optional)
  child: myWidget,
)

5. Expanded & Flexible

Expanded and Flexible are used inside Row or Column to distribute remaining space among children.

// Expanded — child fills ALL remaining space along the main axis
Row(
  children: [
    const Icon(Icons.person),
    const SizedBox(width: 8),
    Expanded(
      child: Text('This text fills all remaining horizontal space'),
    ),
    const Icon(Icons.chevron_right),
  ],
)

// Multiple Expanded with flex ratios
Row(
  children: [
    Expanded(flex: 2, child: Container(color: Colors.blue)),  // 2/3
    Expanded(flex: 1, child: Container(color: Colors.red)),   // 1/3
  ],
)

// Flexible — similar but child CAN be smaller than its share
// Use FlexFit.tight (same as Expanded) or FlexFit.loose (can be smaller)
Flexible(
  flex: 1,
  fit: FlexFit.loose,   // child can be smaller than available space
  child: Text('Short'),
)

// Column example with Expanded
Column(
  children: [
    const Text('Header', style: TextStyle(fontSize: 24)),
    Expanded(
      // This widget fills all remaining vertical space
      child: ListView(
        children: List.generate(20, (i) => ListTile(title: Text('Item $i'))),
      ),
    ),
    ElevatedButton(onPressed: () {}, child: const Text('Footer Button')),
  ],
)

6. SizedBox, Spacer, Padding, Center, Align

// SizedBox — fixed-size box or spacing gap
SizedBox(width: 16)               // horizontal gap in Row
SizedBox(height: 16)              // vertical gap in Column
SizedBox(width: 200, height: 100, child: myWidget)  // constrain child size
SizedBox.expand(child: myWidget)  // fill all available space
SizedBox.shrink()                 // takes zero space (useful as placeholder)

// Spacer — flexible empty space inside Row/Column (like a spring)
Row(
  children: [
    const Text('Left'),
    const Spacer(),              // pushes items apart
    const Text('Right'),
  ],
)

// Spacer with flex
Row(
  children: [
    const Text('Left'),
    const Spacer(flex: 2),
    const Text('Center'),
    const Spacer(flex: 1),
    const Text('Right'),
  ],
)

// Padding — add space around a child
Padding(
  padding: const EdgeInsets.all(16),
  child: const Text('Padded text'),
)

// Center — centers child in available space
const Center(
  child: Text('I am centered'),
)

// Align — position child at specific alignment within parent
Align(
  alignment: Alignment.topRight,      // or Alignment(1.0, -1.0)
  child: const Text('Top right'),
)
// Common Alignment values:
// Alignment.topLeft    Alignment.topCenter    Alignment.topRight
// Alignment.centerLeft Alignment.center       Alignment.centerRight
// Alignment.bottomLeft Alignment.bottomCenter Alignment.bottomRight

7. Scaffold

Scaffold is the top-level visual structure of a screen. It provides slots for AppBar, body, drawer, bottom navigation, and FAB.

Scaffold(
  // Top app bar
  appBar: AppBar(
    title: const Text('Home'),
    actions: [
      IconButton(onPressed: () {}, icon: const Icon(Icons.search)),
      IconButton(onPressed: () {}, icon: const Icon(Icons.more_vert)),
    ],
  ),

  // Main content area (fills space between appBar and bottom)
  body: const Center(child: Text('Body content')),

  // Floating action button
  floatingActionButton: FloatingActionButton(
    onPressed: () {},
    child: const Icon(Icons.add),
  ),

  // FAB position
  floatingActionButtonLocation: FloatingActionButtonLocation.endFloat,

  // Bottom navigation bar
  bottomNavigationBar: BottomNavigationBar(
    currentIndex: 0,
    onTap: (index) {},
    items: const [
      BottomNavigationBarItem(icon: Icon(Icons.home), label: 'Home'),
      BottomNavigationBarItem(icon: Icon(Icons.search), label: 'Search'),
      BottomNavigationBarItem(icon: Icon(Icons.person), label: 'Profile'),
    ],
  ),

  // Side drawer
  drawer: const Drawer(child: Text('Drawer content')),

  // Right-side drawer
  endDrawer: const Drawer(child: Text('End drawer')),

  // Background color of the body area
  backgroundColor: Colors.grey.shade100,

  // Show/hide resize when keyboard appears
  resizeToAvoidBottomInset: true,
)

8. AppBar

AppBar(
  // Left side — auto-shows back arrow or hamburger menu if drawer exists
  leading: IconButton(
    onPressed: () => Navigator.pop(context),
    icon: const Icon(Icons.arrow_back),
  ),
  automaticallyImplyLeading: false,  // suppress auto back arrow

  // Center title
  title: const Text('My Screen'),
  centerTitle: true,          // center on both Android and iOS

  // Right-side action buttons
  actions: [
    IconButton(onPressed: () {}, icon: const Icon(Icons.search)),
    IconButton(onPressed: () {}, icon: const Icon(Icons.notifications)),
    PopupMenuButton<String>(
      onSelected: (value) {},
      itemBuilder: (context) => [
        const PopupMenuItem(value: 'settings', child: Text('Settings')),
        const PopupMenuItem(value: 'logout', child: Text('Logout')),
      ],
    ),
  ],

  // Styling
  backgroundColor: Colors.blue.shade800,
  foregroundColor: Colors.white,      // color of title and icons
  elevation: 4,                       // shadow depth
  shadowColor: Colors.black38,

  // Widget shown below the AppBar (e.g., TabBar)
  bottom: const TabBar(tabs: [Tab(text: 'All'), Tab(text: 'Active')]),
)

9. ListView

// Simple ListView (small lists only — all items built at once)
ListView(
  padding: const EdgeInsets.all(8),
  children: [
    ListTile(
      leading: const Icon(Icons.inbox),
      title: const Text('Inbox'),
      subtitle: const Text('5 new messages'),
      trailing: const Icon(Icons.chevron_right),
      onTap: () {},
    ),
    const Divider(),
    ListTile(
      leading: const CircleAvatar(child: Text('A')),
      title: const Text('Alice'),
      onTap: () {},
    ),
  ],
)

// ListView.builder — efficient for long/infinite lists
// Only builds items that are visible on screen
ListView.builder(
  itemCount: items.length,
  itemExtent: 72,            // fixed height per item (optional, more efficient)
  itemBuilder: (context, index) {
    final item = items[index];
    return ListTile(
      key: ValueKey(item.id),
      leading: CircleAvatar(child: Text('${index + 1}')),
      title: Text(item.title),
      subtitle: Text(item.subtitle),
      trailing: IconButton(
        icon: const Icon(Icons.delete),
        onPressed: () => deleteItem(index),
      ),
    );
  },
)

// ListView.separated — adds separator between items
ListView.separated(
  itemCount: items.length,
  separatorBuilder: (context, index) => const Divider(height: 1),
  itemBuilder: (context, index) => ListTile(title: Text(items[index])),
)

10. GridView.builder

// GridView with fixed number of columns
GridView.builder(
  padding: const EdgeInsets.all(8),
  gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
    crossAxisCount: 2,         // 2 columns
    crossAxisSpacing: 8,       // horizontal gap
    mainAxisSpacing: 8,        // vertical gap
    childAspectRatio: 1.0,     // width / height ratio (1.0 = square)
  ),
  itemCount: products.length,
  itemBuilder: (context, index) {
    final product = products[index];
    return Card(
      child: Column(
        children: [
          Expanded(
            child: Image.network(product.imageUrl, fit: BoxFit.cover),
          ),
          Padding(
            padding: const EdgeInsets.all(8),
            child: Text(product.name, textAlign: TextAlign.center),
          ),
        ],
      ),
    );
  },
)

// GridView with max item extent (auto-calculates columns based on screen width)
GridView.builder(
  gridDelegate: const SliverGridDelegateWithMaxCrossAxisExtent(
    maxCrossAxisExtent: 200,   // each item max 200px wide
    mainAxisExtent: 200,       // each item 200px tall
    crossAxisSpacing: 8,
    mainAxisSpacing: 8,
  ),
  itemCount: 20,
  itemBuilder: (context, index) => Card(
    child: Center(child: Text('Item $index')),
  ),
)
Always use ListView.builder or GridView.builder for lists with more than ~20 items. The builder variants only construct visible items, keeping memory usage low.

📌 Study Checklist