Flutter: 布局系统

Flutter 布局像一条流水线——父组件传递约束,子组件报告尺寸,父组件决定位置。

📋 前置知识:需要先掌握以下内容

1. 你将学到


2. 一个 UI 设计师转开发的故事

(1) 痛点:布局溢出黄条噩梦

Bob 拿到 ShopApp 的 UI 设计稿,商品卡片需要左边图片、右边信息,底部价格行。他用 Row 套 Column,结果屏幕上全是黄黑条纹的溢出警告。调整了半天 padding 和 margin,不是文字被截断就是图片变形。1000 个商品页面,10 种屏幕尺寸,布局问题层出不穷。

(2) 约束传递的解法

Flutter 布局的核心规则只有一条:约束向下传递,尺寸向上报告,位置由父决定。理解了这个规则,Bob 就能预判每个 Widget 的行为。

DART
import 'package:flutter/material.dart';

// Parent gives tight constraints: exactly 200x100
SizedBox(
  width: 200,
  height: 100,
  child: Text('I will be 200x100'), // Text forced to 200x100
)

// Parent gives loose constraints: 0 to maxWidth
Row(
  children: [
    Expanded(child: Text('I take remaining space')),
    SizedBox(width: 80, child: Text('I am 80 wide')),
  ],
)
TEXT 📖 仅展示
> **输出:** 在本地 Flutter SDK(Flutter 3.x / Dart 3.x)运行。Piston 服务器未安装 Flutter,请在本机 `flutter run` 实操对照。实际 UI/状态会因平台略有差异。

(3) 收益:一次写对布局

Bob 掌握约束传递后,布局一次写对率从 30% 提升到 90%,溢出警告彻底消失。


3. 盒模型与约束传递

100%
graph TD
    subgraph Constraint Passing
        P[Parent Constraints] --> C[Child Layout]
        C --> S[Child Size]
        S --> P
    end
    subgraph Common Layouts
        R[Row] --> EX1[Expanded]
        CO[Column] --> EX2[Flexible]
        ST[Stack] --> POS[Positioned]
    end
TEXT 📖 仅展示
> **输出:** 在本地 Flutter SDK(Flutter 3.x / Dart 3.x)运行。Piston 服务器未安装 Flutter,请在本机 `flutter run` 实操对照。实际 UI/状态会因平台略有差异。

(1) 约束类型

类型 说明 示例
Tight 紧约束 min = max,尺寸固定 SizedBox(width: 100)
Loose 松约束 min = 0, max = 父最大 Center 中的子组件
Unbounded 无界 max = infinity ListView 中的 Column
Bounded 有界 min 和 max 都有限 Page 中的 Column

▶ 示例

TEXT 📖 仅展示
> **输出:** 在本地 Flutter SDK(Flutter 3.x / Dart 3.x)运行。Piston 服务器未安装 Flutter,请在本机 `flutter run` 实操对照。实际 UI/状态会因平台略有差异。

:约束传递演示

DART
import 'package:flutter/material.dart';

// Demo: constraint passing in action
class ConstraintDemo extends StatelessWidget {
  const ConstraintDemo({super.key});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Column(
        children: [
          // Tight constraint: exactly 200 height
          Container(
            height: 200,
            color: Colors.blue[100],
            child: const Center(child: Text('Height: 200 (tight)')),
          ),
          // Loose constraint: takes remaining space
          Expanded(
            child: Container(
              color: Colors.green[100],
              child: const Center(child: Text('Expanded (loose, fills remaining)')),
            ),
          ),
        ],
      ),
    );
  }
}
TEXT 📖 仅展示
> **输出:** 在本地 Flutter SDK(Flutter 3.x / Dart 3.x)运行。Piston 服务器未安装 Flutter,请在本机 `flutter run` 实操对照。实际 UI/状态会因平台略有差异。

4. 线性布局:Row 与 Column

(1) 对齐属性

属性 方向 Row 效果 Column 效果
mainAxisAlignment 主轴 水平对齐 垂直对齐
crossAxisAlignment 交叉轴 垂直对齐 水平对齐
mainAxisSize 主轴尺寸 宽度 高度

▶ 示例

TEXT 📖 仅展示
> **输出:** 在本地 Flutter SDK(Flutter 3.x / Dart 3.x)运行。Piston 服务器未安装 Flutter,请在本机 `flutter run` 实操对照。实际 UI/状态会因平台略有差异。

:商品信息行(Row)

DART
import 'package:flutter/material.dart';

// Product info row: image + details
Row(
  crossAxisAlignment: CrossAxisAlignment.start,
  children: [
    // Fixed-size image
    ClipRRect(
      borderRadius: BorderRadius.circular(8),
      child: Image.network(
        'https://cdn.shopapp.com/laptop.jpg',
        width: 100,
        height: 100,
        fit: BoxFit.cover,
      ),
    ),
    const SizedBox(width: 12),
    // Flexible details
    Expanded(
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          const Text('Pro Laptop 16"',
            style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold)),
          const SizedBox(height: 4),
          Text('Electronics > Laptops',
            style: TextStyle(fontSize: 12, color: Colors.grey[600])),
          const SizedBox(height: 8),
          Row(
            children: [
              const Text('\$1,299.99',
                style: TextStyle(fontSize: 18, color: Colors.green)),
              const SizedBox(width: 8),
              Text('\$1,599.99',
                style: TextStyle(
                  fontSize: 14,
                  decoration: TextDecoration.lineThrough,
                  color: Colors.grey[400],
                )),
            ],
          ),
        ],
      ),
    ),
  ],
)
TEXT 📖 仅展示
> **输出:** 在本地 Flutter SDK(Flutter 3.x / Dart 3.x)运行。Piston 服务器未安装 Flutter,请在本机 `flutter run` 实操对照。实际 UI/状态会因平台略有差异。

(2) 常见对齐值对比

MainAxisAlignment 效果
start 起始对齐(默认)
center 居中
end 末尾对齐
spaceBetween 首尾贴边,中间等距
spaceAround 每项两侧等距
spaceEvenly 所有间距相等

5. 弹性布局:Expanded 与 Flexible

▶ 示例

TEXT 📖 仅展示
> **输出:** 在本地 Flutter SDK(Flutter 3.x / Dart 3.x)运行。Piston 服务器未安装 Flutter,请在本机 `flutter run` 实操对照。实际 UI/状态会因平台略有差异。

:价格与按钮弹性布局

DART
import 'package:flutter/material.dart';

// Bottom bar: price (expanded) + button (fixed)
Container(
  padding: const EdgeInsets.all(12),
  child: Row(
    children: [
      Expanded(
        flex: 2, // Takes 2/3 of space
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          mainAxisSize: MainAxisSize.min,
          children: [
            const Text('Total', style: TextStyle(fontSize: 12)),
            Text('\$${total.toStringAsFixed(2)}',
              style: const TextStyle(fontSize: 24, fontWeight: FontWeight.bold)),
          ],
        ),
      ),
      Expanded(
        flex: 1, // Takes 1/3 of space
        child: ElevatedButton(
          onPressed: onCheckout,
          style: ElevatedButton.styleFrom(
            padding: const EdgeInsets.symmetric(vertical: 16)),
          child: const Text('Checkout'),
        ),
      ),
    ],
  ),
)
TEXT 📖 仅展示
> **输出:** 在本地 Flutter SDK(Flutter 3.x / Dart 3.x)运行。Piston 服务器未安装 Flutter,请在本机 `flutter run` 实操对照。实际 UI/状态会因平台略有差异。
Widget 行为 剩余空间 适用场景
Expanded 强制填满 按比例分配 需要占满空间
Flexible 可选填满 按比例分配,可小于剩余 需要弹性但不强制

▶ 示例

TEXT 📖 仅展示
> **输出:** 在本地 Flutter SDK(Flutter 3.x / Dart 3.x)运行。Piston 服务器未安装 Flutter,请在本机 `flutter run` 实操对照。实际 UI/状态会因平台略有差异。

:Expanded vs Flexible

DART
import 'package:flutter/material.dart';

Row(
  children: [
    // Expanded: must fill allocated space
    Expanded(
      flex: 1,
      child: Container(color: Colors.red, child: const Text('Expanded')),
    ),
    // Flexible: can be smaller than allocated space
    Flexible(
      flex: 1,
      child: Container(
        color: Colors.blue,
        child: const Text('Flexible'), // May not fill all space
      ),
    ),
    // Fixed width
    SizedBox(width: 80, child: Container(color: Colors.green, child: const Text('Fixed'))),
  ],
)
TEXT 📖 仅展示
> **输出:** 在本地 Flutter SDK(Flutter 3.x / Dart 3.x)运行。Piston 服务器未安装 Flutter,请在本机 `flutter run` 实操对照。实际 UI/状态会因平台略有差异。

6. 层叠布局:Stack 与 Positioned

Stack 允许子组件按绘制顺序层叠,Positioned 控制绝对定位。

▶ 示例

TEXT 📖 仅展示
> **输出:** 在本地 Flutter SDK(Flutter 3.x / Dart 3.x)运行。Piston 服务器未安装 Flutter,请在本机 `flutter run` 实操对照。实际 UI/状态会因平台略有差异。

:商品图片角标

DART
import 'package:flutter/material.dart';

// 简化类定义
class Product {
  final String imageUrl;
  const Product({required this.imageUrl});
}

// Product image with discount badge and favorite button
Stack(
  children: [
    // Base image
    ClipRRect(
      borderRadius: BorderRadius.circular(12),
      child: Image.network(
        product.imageUrl,
        height: 200,
        width: double.infinity,
        fit: BoxFit.cover,
      ),
    ),
    // Discount badge (top-left)
    Positioned(
      top: 8,
      left: 8,
      child: Container(
        padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
        decoration: BoxDecoration(
          color: Colors.red,
          borderRadius: BorderRadius.circular(4),
        ),
        child: Text('${discountPercent}% OFF',
          style: const TextStyle(color: Colors.white, fontSize: 12)),
      ),
    ),
    // Favorite button (top-right)
    Positioned(
      top: 8,
      right: 8,
      child: CircleAvatar(
        backgroundColor: Colors.white.withOpacity(0.8),
        child: const Icon(Icons.favorite_border, size: 18),
      ),
    ),
    // Stock indicator (bottom)
    Positioned(
      bottom: 8,
      left: 8,
      child: Container(
        padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
        decoration: BoxDecoration(
          color: Colors.black54,
          borderRadius: BorderRadius.circular(4),
        ),
        child: const Text('In Stock', style: TextStyle(color: Colors.white, fontSize: 10)),
      ),
    ),
  ],
)
TEXT 📖 仅展示
> **输出:** 在本地 Flutter SDK(Flutter 3.x / Dart 3.x)运行。Piston 服务器未安装 Flutter,请在本机 `flutter run` 实操对照。实际 UI/状态会因平台略有差异。
Stack 属性 说明 默认值
alignment 未 Positioned 子组件对齐 AlignmentDirectional.topStart
fit 子组件适配方式 StackFit.loose
clipBehavior 溢出裁剪 Clip.hardEdge

7. GridView 网格布局

▶ 示例

TEXT 📖 仅展示
> **输出:** 在本地 Flutter SDK(Flutter 3.x / Dart 3.x)运行。Piston 服务器未安装 Flutter,请在本机 `flutter run` 实操对照。实际 UI/状态会因平台略有差异。

:ShopApp 商品网格

DART
import 'package:flutter/material.dart';

// 简化类定义
class Product {
  final int id;
  final String name;
  final double price;
  const Product({required this.id, required this.name, required this.price});
}

// Two-column product grid
GridView.count(
  crossAxisCount: 2,
  mainAxisSpacing: 8,
  crossAxisSpacing: 8,
  childAspectRatio: 0.75, // height = width / 0.75
  padding: const EdgeInsets.all(8),
  children: products.map((product) => ProductCard(
    key: ValueKey(product.id),
    product: product,
    onAddToCart: () => addToCart(product),
  )).toList(),
)

// Responsive grid with LayoutBuilder
LayoutBuilder(
  builder: (context, constraints) {
    int columns = (constraints.maxWidth / 180).floor().clamp(2, 4);
    return GridView.count(
      crossAxisCount: columns,
      mainAxisSpacing: 8,
      crossAxisSpacing: 8,
      childAspectRatio: 0.75,
      children: productWidgets,
    );
  },
)
TEXT 📖 仅展示
> **输出:** 在本地 Flutter SDK(Flutter 3.x / Dart 3.x)运行。Piston 服务器未安装 Flutter,请在本机 `flutter run` 实操对照。实际 UI/状态会因平台略有差异。
GridView 属性 说明
crossAxisCount 每行列数
mainAxisSpacing 主轴间距
crossAxisSpacing 交叉轴间距
childAspectRatio 子项宽高比
padding 内边距

8. 完整示例:ShopApp 首页布局

DART
import 'package:flutter/material.dart';

class ShopHomePage extends StatelessWidget {
  const ShopHomePage({super.key});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('ShopApp'),
        actions: [
          Stack(
            children: [
              IconButton(icon: const Icon(Icons.shopping_cart), onPressed: () {}),
              Positioned(right: 4, top: 4,
                child: CircleAvatar(radius: 8, backgroundColor: Colors.red,
                  child: const Text('5', style: TextStyle(fontSize: 10, color: Colors.white)))),
            ],
          ),
        ],
      ),
      body: CustomScrollView(
        slivers: [
          // Banner section
          SliverToBoxAdapter(
            child: Container(
              height: 180,
              color: Colors.blue[50],
              child: const Center(child: Text('Flash Sale - Up to 50% OFF',
                style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold))),
            ),
          ),
          // Category chips
          SliverToBoxAdapter(
            child: SizedBox(
              height: 50,
              child: ListView(
                scrollDirection: Axis.horizontal,
                padding: const EdgeInsets.symmetric(horizontal: 8),
                children: ['All', 'Electronics', 'Clothing', 'Books', 'Home']
                  .map((c) => Padding(
                    padding: const EdgeInsets.symmetric(horizontal: 4),
                    child: Chip(label: Text(c)),
                  )).toList(),
              ),
            ),
          ),
          // Product grid
          SliverPadding(
            padding: const EdgeInsets.all(8),
            sliver: SliverGrid.count(
              crossAxisCount: 2,
              mainAxisSpacing: 8,
              crossAxisSpacing: 8,
              childAspectRatio: 0.7,
              children: List.generate(20, (i) => _buildProductCard(i)),
            ),
          ),
        ],
      ),
    );
  }

  Widget _buildProductCard(int index) {
    return Card(
      clipBehavior: Clip.antiAlias,
      shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.stretch,
        children: [
          Expanded(
            flex: 3,
            child: Container(
              color: Colors.grey[200],
              child: const Icon(Icons.image, size: 48, color: Colors.grey),
            ),
          ),
          Expanded(
            flex: 2,
            child: Padding(
              padding: const EdgeInsets.all(8),
              child: Column(
                crossAxisAlignment: CrossAxisAlignment.start,
                children: [
                  Text('Product ${index + 1}', maxLines: 1, overflow: TextOverflow.ellipsis),
                  const Spacer(),
                  Row(
                    mainAxisAlignment: MainAxisAlignment.spaceBetween,
                    children: [
                      Text('\$${(19.99 + index * 10).toStringAsFixed(2)}',
                        style: const TextStyle(color: Colors.green)),
                      const Icon(Icons.add_circle, size: 20),
                    ],
                  ),
                ],
              ),
            ),
          ),
        ],
      ),
    );
  }
}
TEXT 📖 仅展示
> **输出:** 在本地 Flutter SDK(Flutter 3.x / Dart 3.x)运行。Piston 服务器未安装 Flutter,请在本机 `flutter run` 实操对照。实际 UI/状态会因平台略有差异。

❓ 常见问题

Q Row 里面出现黄黑溢出条纹怎么办?
A Row 子组件总宽超过 Row 宽度。用 Expanded 包裹可变宽度的子组件,或用 Flexible 让子组件自适应。
Q Expanded 和 Flexible 有什么区别?
A Expanded 强制填满分配空间(相当于 Flexible(fit: FlexFit.tight));Flexible 允许子组件小于分配空间(fit: FlexFit.loose)。
Q Stack 中未用 Positioned 的子组件怎么定位?
A 按 Stack 的 alignment 属性定位,默认左上角。
Q GridView.count 和 GridView.builder 有什么区别?
A count 适合少量固定项;builder 懒加载适合大数据列表(千万级商品用 builder)。
Q 为什么 Column 里的 ListView 会报错?
A Column 给子组件无界高度约束,ListView 无法确定自身高度。用 Expanded 包 ListView,或用 shrinkWrap: true。
Q childAspectRatio 怎么算?
A ratio = width / height。如果每项宽 180、高 240,则 ratio = 180/240 = 0.75。

📖 小节


📝 作业

  1. 基础题(难度⭐):创建一个水平滚动分类标签栏(Chip + ListView 水平方向)。
  2. 进阶题(难度⭐⭐):实现一个商品详情页布局:顶部大图 + 中间信息区 + 底部价格/购买按钮栏,用 Column + Expanded。
  3. 挑战题(难度⭐⭐⭐):用 LayoutBuilder 实现响应式商品网格:手机 2 列、平板 3 列、桌面 4 列,窗口缩放时实时切换。

← 上一课 | 下一课 →

Web-Tutorial.com

Web-Tutorial 技术团队

由多位开发者共同维护的编程教程平台。每篇教程由对应领域的开发者编写和审核,确保内容准确可靠。如发现任何问题,欢迎向我们反馈。

100%

🙏 帮我们做得更好

我们是刚上线的编程教程站,几个人的小团队,精力有限。页面虽经检查,难免还有疏漏——链接失效、排版错乱、内容有误、语言生硬……

如果您发现了,麻烦告诉我们,我们会在收到反馈后第一时间进行修复,再次感谢您的光临 🙏