Skip to content

Flutter 常用 Widget 与布局技巧实战

前置知识

📚 前置知识建议

本文档讲解 Flutter 常用组件和布局技巧,建议先了解:

阅读完本文后,建议继续阅读:


一、核心要点速览

💡 核心考点

  • 布局三要素:Container(容器)、Row/Column(行列)、Stack(层叠)。
  • 尺寸约束:父组件传递约束,子组件返回尺寸(Constraints go down, Sizes go up)。
  • 常用组件:Text、Image、ListView、GridView、Card、AppBar 等基础组件必须熟练掌握。
  • 响应式布局:使用 LayoutBuilder、MediaQuery、Flexible/Expanded 实现自适应。
  • 自定义组件:通过组合现有 Widget 或继承 StatelessWidget/StatefulWidget 创建。

二、布局系统核心原理

1. 布局约束流程

Flutter 布局约束机制图

布局规则

  1. 约束下行:父组件告诉子组件"你可以在多大范围内布局"。
  2. 尺寸上行:子组件决定"我需要多大空间"并返回给父组件。
  3. 位置决定:父组件根据子组件的尺寸,决定"你放在哪里"。

2. 约束类型对比

约束类型说明示例组件
Tight Constraints强制固定尺寸SizedBox(width: 100, height: 100)
Loose Constraints允许自适应Padding, Center
Unbounded Constraints无限制(常见于滚动方向)ListView 的子组件高度不受限

三、基础布局组件详解

1. Container(万能容器)

功能:集装饰、变换、约束于一体的复合组件。

Container(
  width: 200,              // 宽度约束
  height: 100,             // 高度约束
  padding: EdgeInsets.all(16), // 内边距
  margin: EdgeInsets.symmetric(vertical: 8), // 外边距
  decoration: BoxDecoration(
    color: Colors.blue,
    borderRadius: BorderRadius.circular(12),
    boxShadow: [
      BoxShadow(
        color: Colors.black26,
        blurRadius: 8,
        offset: Offset(0, 4),
      ),
    ],
  ),
  child: Text('Hello'),
  transform: Matrix4.rotationZ(0.1), // 旋转变换
)

常用属性速查

属性类型作用
width/heightdouble固定尺寸
padding/marginEdgeInsets间距控制
decorationBoxDecoration背景、边框、阴影、圆角
constraintsBoxConstraints高级尺寸约束
transformMatrix4旋转、缩放、平移
alignmentAlignment子组件对齐方式

2. Row & Column(行列布局)

主轴与交叉轴

  • Row:主轴 = 水平方向,交叉轴 = 垂直方向
  • Column:主轴 = 垂直方向,交叉轴 = 水平方向
// Row 示例
Row(
  mainAxisAlignment: MainAxisAlignment.spaceBetween, // 主轴分布
  crossAxisAlignment: CrossAxisAlignment.center,     // 交叉轴对齐
  children: [
    Icon(Icons.star, size: 32),
    Expanded(  // 占据剩余空间
      child: Text('This text takes all remaining space'),
    ),
    Icon(Icons.star_border, size: 32),
  ],
)

// Column 示例
Column(
  mainAxisSize: MainAxisSize.min, // 主轴最小尺寸
  mainAxisAlignment: MainAxisAlignment.center,
  children: [
    Text('Title', style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold)),
    SizedBox(height: 8), // 间距
    Text('Subtitle', style: TextStyle(color: Colors.grey)),
  ],
)

MainAxisAlignment 枚举值

效果图示
start靠主轴起点[ABC ]
end靠主轴终点[ ABC]
center居中[ ABC ]
spaceBetween两端对齐[A B C]
spaceAround均匀分布(两侧半间距)[ A B C ]
spaceEvenly完全均匀(等间距)[ A B C ]

3. Stack & Positioned(层叠布局)

适用场景:卡片覆盖、角标、浮动按钮等需要重叠的场景。

Stack(
  alignment: Alignment.center, // 默认对齐方式
  children: [
    // 底层:背景图片
    Image.network('https://example.com/bg.jpg', fit: BoxFit.cover),
    
    // 中层:渐变遮罩
    Container(
      decoration: BoxDecoration(
        gradient: LinearGradient(
          begin: Alignment.topCenter,
          end: Alignment.bottomCenter,
          colors: [Colors.transparent, Colors.black54],
        ),
      ),
    ),
    
    // 顶层:文字内容
    Positioned(
      bottom: 16,
      left: 16,
      child: Text(
        'Card Title',
        style: TextStyle(color: Colors.white, fontSize: 18),
      ),
    ),
    
    // 右上角角标
    Positioned(
      top: 8,
      right: 8,
      child: Container(
        padding: EdgeInsets.symmetric(horizontal: 8, vertical: 4),
        decoration: BoxDecoration(
          color: Colors.red,
          borderRadius: BorderRadius.circular(12),
        ),
        child: Text('NEW', style: TextStyle(color: Colors.white, fontSize: 10)),
      ),
    ),
  ],
)

4. ListView & GridView(滚动列表)

ListView 四种构造方式

// 方式一:静态列表(少量固定项)
ListView(
  children: [
    ListTile(title: Text('Item 1')),
    ListTile(title: Text('Item 2')),
  ],
)

// 方式二:builder 懒加载(长列表推荐)
ListView.builder(
  itemCount: 1000,
  itemBuilder: (context, index) {
    return ListTile(title: Text('Item $index'));
  },
)

// 方式三:separated 加分隔线
ListView.separated(
  itemCount: 100,
  itemBuilder: (context, index) => ListTile(title: Text('Item $index')),
  separatorBuilder: (context, index) => Divider(),
)

// 方式四:custom 高级定制
ListView.custom(
  childrenDelegate: SliverChildBuilderDelegate(
    (context, index) => ItemWidget(index: index),
    childCount: 100,
  ),
)

GridView 网格布局

// 固定列数
GridView.count(
  crossAxisCount: 3,           // 3 列
  crossAxisSpacing: 8,         // 列间距
  mainAxisSpacing: 8,          // 行间距
  childAspectRatio: 0.75,      // 宽高比
  children: List.generate(20, (index) {
    return GridTile(child: Image.network('url'));
  }),
)

// 自适应列宽
GridView.extent(
  maxCrossAxisExtent: 150,     // 每项最大宽度
  children: items.map((item) => GridTile(child: ItemWidget(item))).toList(),
)

// builder 模式(大数据集)
GridView.builder(
  gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
    crossAxisCount: 4,
  ),
  itemCount: 1000,
  itemBuilder: (context, index) => GridTile(child: ItemWidget()),
)

四、高级布局技巧

1. 响应式布局方案

使用 MediaQuery

class ResponsiveLayout extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    final screenWidth = MediaQuery.of(context).size.width;
    final isTablet = screenWidth > 600;
    final isDesktop = screenWidth > 1200;
    
    return Scaffold(
      body: Row(
        children: [
          // 侧边栏:仅平板和桌面显示
          if (isTablet)
            Sidebar(width: isDesktop ? 300 : 200),
          
          // 主内容区
          Expanded(
            child: MainContent(),
          ),
        ],
      ),
    );
  }
}

使用 LayoutBuilder

LayoutBuilder(
  builder: (context, constraints) {
    if (constraints.maxWidth < 600) {
      // 手机布局
      return MobileLayout();
    } else if (constraints.maxWidth < 1200) {
      // 平板布局
      return TabletLayout();
    } else {
      // 桌面布局
      return DesktopLayout();
    }
  },
)

使用 Flexible 和 Expanded

Row(
  children: [
    // 固定宽度
    Container(width: 100, child: Sidebar()),
    
    // 弹性占比
    Flexible(flex: 2, child: ContentArea()),  // 占 2/3
    Flexible(flex: 1, child: RightPanel()),   // 占 1/3
    
    // Expanded 是 flex: 1 的 Flexible 简写
    Expanded(child: Footer()),
  ],
)

2. 复杂布局组合实战

卡片列表布局

class CardListPage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('Cards')),
      body: ListView.builder(
        padding: EdgeInsets.all(16),
        itemCount: 20,
        itemBuilder: (context, index) {
          return Card(
            elevation: 4,
            shape: RoundedRectangleBorder(
              borderRadius: BorderRadius.circular(12),
            ),
            margin: EdgeInsets.only(bottom: 16),
            child: Column(
              crossAxisAlignment: CrossAxisAlignment.start,
              children: [
                // 顶部图片
                ClipRRect(
                  borderRadius: BorderRadius.vertical(top: Radius.circular(12)),
                  child: Image.network(
                    'https://example.com/image.jpg',
                    height: 200,
                    width: double.infinity,
                    fit: BoxFit.cover,
                  ),
                ),
                
                // 内容区
                Padding(
                  padding: EdgeInsets.all(16),
                  child: Column(
                    crossAxisAlignment: CrossAxisAlignment.start,
                    children: [
                      Text(
                        'Card Title',
                        style: Theme.of(context).textTheme.titleLarge,
                      ),
                      SizedBox(height: 8),
                      Text(
                        'This is a description text for the card item.',
                        style: Theme.of(context).textTheme.bodyMedium?.copyWith(
                          color: Colors.grey[600],
                        ),
                      ),
                      SizedBox(height: 16),
                      
                      // 底部操作按钮
                      Row(
                        mainAxisAlignment: MainAxisAlignment.end,
                        children: [
                          TextButton.icon(
                            icon: Icon(Icons.favorite_border),
                            label: Text('Like'),
                            onPressed: () {},
                          ),
                          SizedBox(width: 8),
                          ElevatedButton.icon(
                            icon: Icon(Icons.share),
                            label: Text('Share'),
                            onPressed: () {},
                          ),
                        ],
                      ),
                    ],
                  ),
                ),
              ],
            ),
          );
        },
      ),
    );
  }
}

瀑布流布局(Staggered Grid)

import 'package:flutter_staggered_grid_view/flutter_staggered_grid_view.dart';

MasonryGridView.count(
  crossAxisCount: 2,
  mainAxisSpacing: 8,
  crossAxisSpacing: 8,
  itemCount: 20,
  itemBuilder: (context, index) {
    // 随机高度模拟瀑布流
    final height = 100 + (index % 5) * 50;
    return Container(
      height: height.toDouble(),
      decoration: BoxDecoration(
        color: Colors.primaries[index % Colors.primaries.length],
        borderRadius: BorderRadius.circular(8),
      ),
      child: Center(child: Text('Item $index')),
    );
  },
)

3. 自定义布局组件

封装通用卡片

class CommonCard extends StatelessWidget {
  final String title;
  final String subtitle;
  final String? imageUrl;
  final VoidCallback onTap;

  const CommonCard({
    Key? key,
    required this.title,
    required this.subtitle,
    this.imageUrl,
    required this.onTap,
  }) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Card(
      elevation: 2,
      shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
      child: InkWell(
        onTap: onTap,
        borderRadius: BorderRadius.circular(12),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            if (imageUrl != null)
              ClipRRect(
                borderRadius: BorderRadius.vertical(top: Radius.circular(12)),
                child: Image.network(imageUrl!, height: 150, fit: BoxFit.cover),
              ),
            Padding(
              padding: EdgeInsets.all(16),
              child: Column(
                crossAxisAlignment: CrossAxisAlignment.start,
                children: [
                  Text(title, style: Theme.of(context).textTheme.titleMedium),
                  SizedBox(height: 4),
                  Text(subtitle, style: Theme.of(context).textTheme.bodySmall),
                ],
              ),
            ),
          ],
        ),
      ),
    );
  }
}

使用

CommonCard(
  title: 'Flutter 入门',
  subtitle: '学习 Dart 语言和 Flutter 框架',
  imageUrl: 'https://example.com/flutter.jpg',
  onTap: () => Navigator.push(context, MaterialPageRoute(builder: (_) => DetailPage())),
)

五、常见布局问题与解决方案

1. Overflow 溢出错误

错误示例

A RenderFlex overflowed by 50 pixels on the right.

原因:子组件总宽度超过父组件约束。

解决方案

// ❌ 错误:Text 可能溢出
Row(
  children: [
    Text('Very long text that might overflow the available space'),
    Icon(Icons.info),
  ],
)

// ✅ 正确:使用 Expanded 包裹
Row(
  children: [
    Expanded(
      child: Text(
        'Very long text that might overflow the available space',
        overflow: TextOverflow.ellipsis, // 省略号
      ),
    ),
    Icon(Icons.info),
  ],
)

2. SingleChildScrollView 嵌套 ListView

错误:两个可滚动组件嵌套导致冲突。

解决方案

// ❌ 错误:会抛出异常
SingleChildScrollView(
  child: Column(
    children: [
      ListView.builder(/* ... */),
    ],
  ),
)

// ✅ 正确:禁用 ListView 滚动
SingleChildScrollView(
  child: Column(
    children: [
      ListView.builder(
        shrinkWrap: true,      // 根据内容收缩
        physics: NeverScrollableScrollPhysics(), // 禁用滚动
        itemCount: 10,
        itemBuilder: (context, index) => ListTile(title: Text('Item $index')),
      ),
    ],
  ),
)

// ✅ 更优:使用 CustomScrollView(见下文)

3. 使用 CustomScrollView 统一滚动

CustomScrollView(
  slivers: [
    // 顶部 AppBar 效果
    SliverAppBar(
      expandedHeight: 200,
      flexibleSpace: FlexibleSpaceBar(
        title: Text('Parallax Header'),
        background: Image.network('https://example.com/header.jpg', fit: BoxFit.cover),
      ),
    ),
    
    // 网格列表
    SliverGrid(
      gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(crossAxisCount: 3),
      delegate: SliverChildBuilderDelegate(
        (context, index) => GridTile(child: Icon(Icons.star)),
        childCount: 20,
      ),
    ),
    
    // 普通列表
    SliverList(
      delegate: SliverChildBuilderDelegate(
        (context, index) => ListTile(title: Text('Item $index')),
        childCount: 30,
      ),
    ),
  ],
)

优势:多个滚动区域统一管理,性能更优,支持视差滚动等高级效果。


六、面试高频问答

Q1: Flutter 的布局约束机制是怎样的?

标准回答

Flutter 采用单向布局约束系统

  1. 约束下行:父组件通过 Constraints 告诉子组件可用的最小和最大尺寸。
  2. 尺寸上行:子组件根据自身内容和约束,决定实际尺寸并返回给父组件。
  3. 位置决定:父组件根据子组件的尺寸,计算并确定其位置。

关键原则

  • 子组件不能违背父组件的约束(如超出最大宽度)。
  • 父组件不直接指定子组件的尺寸,而是通过约束引导。
  • 这种机制保证了布局的可预测性和高性能。

记忆口诀"约束下传、尺寸上传、父定位置"


Q2: Expanded 和 Flexible 有什么区别?

标准回答

两者都用于 Row/Column 中的弹性布局,核心区别:

特性ExpandedFlexible
本质flex: 1 的 Flexible 简写可自定义 flex 值
填充行为强制填满分配的空间可以不填满(fit: loose)
使用场景均分剩余空间灵活控制占比

示例

// Expanded 强制填满
Row(
  children: [
    Expanded(child: Container(color: Colors.red)),   // 占满全部
    Expanded(child: Container(color: Colors.blue)),  // 占满全部
  ],
)

// Flexible 可选填充
Row(
  children: [
    Flexible(fit: FlexFit.tight, flex: 2, child: Container(color: Colors.red)),
    Flexible(fit: FlexFit.loose, flex: 1, child: Container(color: Colors.blue, width: 50)),
  ],
)

总结:Expanded = Flexible(fit: FlexFit.tight, flex: 1)


Q3: 如何实现 Flutter 的响应式布局?

标准回答

有三种主流方案:

  1. MediaQuery 屏幕尺寸判断

    dart
    final width = MediaQuery.of(context).size.width;
    if (width < 600) return MobileLayout();
    if (width < 1200) return TabletLayout();
    return DesktopLayout();
  2. LayoutBuilder 约束判断

    dart
    LayoutBuilder(
      builder: (context, constraints) {
        if (constraints.maxWidth < 600) return SmallLayout();
        return LargeLayout();
      },
    )
  3. Flexible/Expanded 弹性布局

    • 使用 flex 属性按比例分配空间。
    • 配合 Wrap 实现自动换行。

最佳实践:结合使用,MediaQuery 用于大布局切换,LayoutBuilder 用于局部适配,Flexible 用于细节调整。


Q4: ListView.builder 为什么性能好?

标准回答

核心在于懒加载复用机制

  1. 按需创建:只渲染可见区域的 item,滚动时动态创建新 item。
  2. 对象复用:移出屏幕的 item 会被回收并重新配置数据,而不是销毁重建。
  3. 内存优化:无论列表多长,内存中只保留屏幕内的 item(O(可见数量) 而非 O(total))。

对比

  • 普通 ListView(children: [...]):一次性创建所有 item,适合少于 20 项。
  • ListView.builder:懒加载,适合 100+ 项甚至无限列表。

Q5: Stack 和 IndexedStack 的区别是什么?

标准回答

Stack

  • 所有子组件同时构建和渲染(只是视觉上重叠)。
  • 适用于需要叠加效果的场景(如图片+文字+角标)。

IndexedStack

  • 只显示指定索引的子组件,其他子组件不渲染(但保持状态)。
  • 适用于 Tab 切换,避免重复初始化。

示例

// Stack:三个组件都渲染
Stack(
  children: [Page1(), Page2(), Page3()], // 都执行 initState
)

// IndexedStack:只显示一个
IndexedStack(
  index: _currentIndex,
  children: [Page1(), Page2(), Page3()], // 只有当前页渲染
)

性能影响:Tab 场景用 IndexedStack 可减少重复渲染,但占用更多内存(所有状态保留)。


七、记忆口诀总结

🧠 核心口诀

Container 万能箱,Row Column 排行列。
Stack 层叠 Positioned 定,ListView 懒加载快。
Expanded 均分空间,Flexible 弹性占。
约束下传尺寸上,响应布局三件套。
MediaQuery 判尺寸,LayoutBuilder 看约束。


八、扩展阅读


九、相关资源

资源类型链接说明
Widget 目录https://flutter.dev/docs/development/ui/widgets官方完整 Widget 索引
布局教程https://docs.flutter.dev/development/ui/layout官方布局指南
Interactive Widgethttps://flutter.github.io/samples/交互式组件演示
Awesome Flutterhttps://github.com/Solido/awesome-flutter优质开源组件集合
Pub.dev Packageshttps://pub.dev/flutter/packages第三方组件市场
最近更新