← 返回目录
🧩

核心组件与布局

掌握 Flutter 常用组件的使用方法与布局技巧,构建精美的用户界面

一、文本与样式

文本是界面中最基础的元素。Flutter 提供了 TextRichText 组件来展示文字内容,通过 TextStyle 控制字体样式。

1.1 Text 基础用法

Text 组件是最常用的文本展示方式,支持对齐、溢出处理、最大行数等属性。

// Text 组件的基础用法
Text(
  'Hello, Flutter!',
  style: TextStyle(
    fontSize: 24,           // 字号
    fontWeight: FontWeight.bold,  // 粗体
    color: Colors.blue,      // 颜色
    letterSpacing: 2.0,     // 字间距
    height: 1.5,             // 行高倍数
  ),
  textAlign: TextAlign.center,  // 文本居中
  maxLines: 2,                   // 最多显示两行
  overflow: TextOverflow.ellipsis, // 超出显示省略号
)

1.2 TextStyle 详解

TextStyle 提供了丰富的样式属性,可以精细控制文字的外观。

// TextStyle 的常用属性演示
Text(
  '样式丰富的文本',
  style: TextStyle(
    fontSize: 20,
    fontWeight: FontWeight.w600,     // 字重 100-900
    fontStyle: FontStyle.italic,     // 斜体
    color: Color(0xFF333333),        // 自定义颜色
    backgroundColor: Colors.yellow.withOpacity(0.3),
    decoration: TextDecoration.underline,    // 下划线
    decorationColor: Colors.red,             // 装饰线颜色
    decorationStyle: TextDecorationStyle.dashed, // 虚线
    shadows: [
      Shadow(
        color: Colors.grey,
        offset: Offset(2, 2),
        blurRadius: 4,
      ),
    ],
  ),
)

1.3 RichText 富文本

当需要在一段文字中使用不同样式时,使用 RichTextText.rich

// 使用 Text.rich 创建富文本
Text.rich(
  TextSpan(
    text: '欢迎来到 ',
    style: TextStyle(fontSize: 18, color: Colors.black),
    children: [
      TextSpan(
        text: 'Flutter',
        style: TextStyle(
          color: Colors.blue,
          fontWeight: FontWeight.bold,
          fontSize: 22,
        ),
      ),
      TextSpan(text: ' 的世界!'),
      // 可点击的文本片段
      WidgetSpan(
        child: GestureDetector(
          onTap: () => print('点击了链接'),
          child: Text(
            '了解更多',
            style: TextStyle(
              color: Colors.orange,
              decoration: TextDecoration.underline,
            ),
          ),
        ),
      ),
    ],
  ),
)
提示:使用 DefaultTextStyle 可以为子树中所有 Text 组件设置默认样式,避免重复定义。对于需要自定义字体的场景,在 pubspec.yaml 中声明字体文件,再通过 fontFamily 引用。

二、图片与图标

Flutter 支持多种图片加载方式,包括本地资源、网络图片和内存图片。Icon 组件则提供了丰富的矢量图标。

2.1 Image 组件

Flutter 通过不同的工厂构造函数加载不同来源的图片。

// 加载本地资源图片(需在 pubspec.yaml 中声明)
Image.asset(
  'assets/images/logo.png',
  width: 200,
  height: 200,
  fit: BoxFit.cover,  // 图片填充方式
)

// 加载网络图片
Image.network(
  'https://example.com/image.jpg',
  width: 300,
  height: 200,
  fit: BoxFit.contain,
  // 加载中的占位图
  loadingBuilder: (BuildContext context, Widget child,
      ImageChunkEvent? loadingProgress) {
    if (loadingProgress == null) return child;
    return Center(
      child: CircularProgressIndicator(
        value: loadingProgress.expectedTotalBytes != null
            ? loadingProgress.cumulativeBytesLoaded /
                loadingProgress.expectedTotalBytes!
            : null,
      ),
    );
  },
  // 加载失败时显示的组件
  errorBuilder: (BuildContext context, Object error,
      StackTrace? stackTrace) {
    return Icon(Icons.broken_image, size: 64, color: Colors.grey);
  },
)

2.2 CircleAvatar 与圆形图片

// 使用 CircleAvatar 展示圆形头像
CircleAvatar(
  radius: 50,
  backgroundImage: NetworkImage('https://example.com/avatar.jpg'),
  backgroundColor: Colors.grey[200],
)

// 使用 ClipOval 裁剪为圆形
ClipOval(
  child: Image.network(
    'https://example.com/photo.jpg',
    width: 100,
    height: 100,
    fit: BoxFit.cover,
  ),
)

// 使用 ClipRRect 裁剪为圆角矩形
ClipRRect(
  borderRadius: BorderRadius.circular(16),
  child: Image.asset('assets/images/banner.png', fit: BoxFit.cover),
)

2.3 Icon 图标

// Material Design 内置图标
Icon(
  Icons.favorite,
  color: Colors.red,
  size: 36,
)

// 带背景的图标按钮
Container(
  padding: EdgeInsets.all(12),
  decoration: BoxDecoration(
    color: Colors.blue.withOpacity(0.1),
    shape: BoxShape.circle,
  ),
  child: Icon(Icons.star, color: Colors.blue, size: 32),
)

// IconButton - 可点击的图标
IconButton(
  icon: Icon(Icons.share),
  iconSize: 28,
  color: Colors.teal,
  tooltip: '分享',
  onPressed: () => print('分享被点击'),
)
提示:网络图片建议使用第三方库 cached_network_image 来缓存图片,提升加载性能。BoxFit 的常用值包括:cover(裁剪填满)、contain(完整显示)、fill(拉伸填满)。

三、按钮组件

Flutter 3.x 推荐使用 Material 3 的按钮体系:ElevatedButton(凸起按钮)、TextButton(文字按钮)、OutlinedButton(边框按钮)。

3.1 三种基础按钮

// ElevatedButton - 带阴影的凸起按钮,用于主要操作
ElevatedButton(
  onPressed: () {
    print('主要操作');
  },
  child: Text('确认'),
)

// TextButton - 扁平文字按钮,用于次要操作
TextButton(
  onPressed: () {
    print('取消操作');
  },
  child: Text('取消'),
)

// OutlinedButton - 带边框的按钮
OutlinedButton(
  onPressed: () {
    print('边框按钮');
  },
  child: Text('更多选项'),
)

// 禁用按钮 - 将 onPressed 设为 null
ElevatedButton(
  onPressed: null,  // 禁用状态
  child: Text('不可点击'),
)

3.2 自定义按钮样式

// 使用 styleFrom 自定义按钮外观
ElevatedButton(
  onPressed: () {},
  style: ElevatedButton.styleFrom(
    backgroundColor: Colors.deepPurple,     // 背景色
    foregroundColor: Colors.white,           // 文字颜色
    padding: EdgeInsets.symmetric(
      horizontal: 32,
      vertical: 16,
    ),
    shape: RoundedRectangleBorder(           // 圆角
      borderRadius: BorderRadius.circular(12),
    ),
    elevation: 6,                              // 阴影高度
    textStyle: TextStyle(
      fontSize: 18,
      fontWeight: FontWeight.bold,
    ),
  ),
  child: Text('自定义按钮'),
)

// 带图标的按钮
ElevatedButton.icon(
  onPressed: () {},
  icon: Icon(Icons.send),
  label: Text('发送消息'),
)

3.3 渐变色按钮

// 使用 Container + InkWell 实现渐变按钮
Container(
  decoration: BoxDecoration(
    gradient: LinearGradient(
      colors: [Colors.blue, Colors.purple],
    ),
    borderRadius: BorderRadius.circular(30),
    boxShadow: [
      BoxShadow(
        color: Colors.blue.withOpacity(0.4),
        blurRadius: 12,
        offset: Offset(0, 4),
      ),
    ],
  ),
  child: Material(
    color: Colors.transparent,
    child: InkWell(
      onTap: () => print('渐变按钮被点击'),
      borderRadius: BorderRadius.circular(30),
      child: Padding(
        padding: EdgeInsets.symmetric(horizontal: 32, vertical: 14),
        child: Text(
          '渐变按钮',
          style: TextStyle(color: Colors.white, fontSize: 16),
        ),
      ),
    ),
  ),
)
提示:按钮的 onPressednull 时按钮自动变为禁用状态。建议使用 ButtonStyle 主题统一管理按钮样式,避免每个按钮单独设置。使用 FloatingActionButton 作为页面的主要悬浮操作按钮。

四、输入与表单

表单是用户交互的核心。Flutter 提供了 TextFieldTextFormFieldForm 来构建完整的表单体验。

4.1 TextField 基础输入

// 基础文本输入框
TextField(
  decoration: InputDecoration(
    labelText: '用户名',           // 浮动标签
    hintText: '请输入用户名',       // 占位提示
    prefixIcon: Icon(Icons.person), // 前缀图标
    suffixIcon: Icon(Icons.clear),  // 后缀图标
    border: OutlineInputBorder(     // 边框样式
      borderRadius: BorderRadius.circular(12),
    ),
    filled: true,                    // 填充背景
    fillColor: Colors.grey[50],
  ),
  onChanged: (String value) {
    print('输入内容: $value');      // 实时监听输入变化
  },
)

// 密码输入框
TextField(
  obscureText: true,               // 隐藏输入内容
  decoration: InputDecoration(
    labelText: '密码',
    prefixIcon: Icon(Icons.lock),
    border: OutlineInputBorder(),
  ),
)

4.2 TextEditingController 控制器

class SearchPage extends StatefulWidget {
  @override
  _SearchPageState createState() => _SearchPageState();
}

class _SearchPageState extends State<SearchPage> {
  // 声明控制器
  final _controller = TextEditingController();

  @override
  void initState() {
    super.initState();
    // 可以设置初始值
    _controller.text = '默认搜索词';
    // 监听文本变化
    _controller.addListener(() {
      print('当前值: ${_controller.text}');
    });
  }

  @override
  void dispose() {
    _controller.dispose();  // 释放控制器,防止内存泄漏
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return TextField(
      controller: _controller,
      decoration: InputDecoration(
        hintText: '搜索...',
        suffixIcon: IconButton(
          icon: Icon(Icons.clear),
          onPressed: () => _controller.clear(), // 清空内容
        ),
      ),
    );
  }
}

4.3 Form 表单验证

使用 Form + TextFormField 构建带验证功能的完整表单。

class RegistrationForm extends StatefulWidget {
  @override
  _RegistrationFormState createState() => _RegistrationFormState();
}

class _RegistrationFormState extends State<RegistrationForm> {
  // 表单的全局 Key,用于验证和保存
  final _formKey = GlobalKey<FormState>();
  String _email = '';
  String _password = '';
  String _nickname = '';

  void _submitForm() {
    // validate() 会触发所有 TextFormField 的 validator
    if (_formKey.currentState!.validate()) {
      _formKey.currentState!.save();  // 触发 onSaved
      print('注册信息: $_nickname, $_email');
      // 提交到服务器...
    }
  }

  @override
  Widget build(BuildContext context) {
    return Form(
      key: _formKey,
      child: Column(
        children: [
          // 昵称输入
          TextFormField(
            decoration: InputDecoration(
              labelText: '昵称',
              prefixIcon: Icon(Icons.person),
              border: OutlineInputBorder(),
            ),
            validator: (String? value) {
              if (value == null || value.isEmpty) {
                return '请输入昵称';
              }
              if (value.length < 2) {
                return '昵称至少需要2个字符';
              }
              return null;  // 返回 null 表示验证通过
            },
            onSaved: (value) => _nickname = value!,
          ),
          SizedBox(height: 16),

          // 邮箱输入
          TextFormField(
            decoration: InputDecoration(
              labelText: '邮箱',
              prefixIcon: Icon(Icons.email),
              border: OutlineInputBorder(),
            ),
            keyboardType: TextInputType.emailAddress,
            validator: (value) {
              if (value == null || !value.contains('@')) {
                return '请输入有效的邮箱地址';
              }
              return null;
            },
            onSaved: (value) => _email = value!,
          ),
          SizedBox(height: 16),

          // 密码输入
          TextFormField(
            decoration: InputDecoration(
              labelText: '密码',
              prefixIcon: Icon(Icons.lock),
              border: OutlineInputBorder(),
            ),
            obscureText: true,
            validator: (value) {
              if (value == null || value.length < 6) {
                return '密码至少需要6位';
              }
              return null;
            },
            onSaved: (value) => _password = value!,
          ),
          SizedBox(height: 24),

          // 提交按钮
          SizedBox(
            width: double.infinity,
            height: 48,
            child: ElevatedButton(
              onPressed: _submitForm,
              child: Text('注册', style: TextStyle(fontSize: 18)),
            ),
          ),
        ],
      ),
    );
  }
}
提示:使用 TextEditingController 时务必在 dispose() 中释放,防止内存泄漏。FormautovalidateMode 属性可设置为 AutovalidateMode.onUserInteraction 实现实时验证。

五、容器与装饰

Container 是 Flutter 最灵活的布局组件之一,结合 BoxDecoration 可以实现丰富的视觉效果。

5.1 Container 详解

// Container 的完整用法
Container(
  width: 300,
  height: 200,
  margin: EdgeInsets.all(16),        // 外边距
  padding: EdgeInsets.all(20),       // 内边距
  alignment: Alignment.center,      // 子组件对齐方式
  decoration: BoxDecoration(
    color: Colors.white,              // 背景色
    borderRadius: BorderRadius.circular(16),  // 圆角
    border: Border.all(               // 边框
      color: Colors.grey[300]!,
      width: 1,
    ),
    boxShadow: [                       // 阴影
      BoxShadow(
        color: Colors.black.withOpacity(0.1),
        blurRadius: 10,
        spreadRadius: 2,
        offset: Offset(0, 4),
      ),
    ],
  ),
  child: Text('装饰容器', style: TextStyle(fontSize: 20)),
)

5.2 BoxDecoration 渐变与图片背景

// 渐变背景
Container(
  width: double.infinity,
  height: 200,
  decoration: BoxDecoration(
    gradient: LinearGradient(
      begin: Alignment.topLeft,
      end: Alignment.bottomRight,
      colors: [
        Color(0xFF6A11CB),
        Color(0xFF2575FC),
      ],
    ),
    borderRadius: BorderRadius.circular(20),
  ),
  child: Center(
    child: Text(
      '渐变背景',
      style: TextStyle(color: Colors.white, fontSize: 24),
    ),
  ),
)

// 图片背景容器
Container(
  width: 300,
  height: 180,
  decoration: BoxDecoration(
    image: DecorationImage(
      image: AssetImage('assets/images/bg.jpg'),
      fit: BoxFit.cover,
      colorFilter: ColorFilter.mode(
        Colors.black.withOpacity(0.3),  // 半透明遮罩
        BlendMode.darken,
      ),
    ),
    borderRadius: BorderRadius.circular(16),
  ),
)

5.3 Padding、SizedBox 与 Card

// Padding - 仅添加内边距的轻量组件
Padding(
  padding: EdgeInsets.symmetric(horizontal: 16, vertical: 8),
  child: Text('带内边距的文本'),
)

// SizedBox - 固定尺寸或间距
Column(
  children: [
    Text('第一行'),
    SizedBox(height: 16),   // 常用作间距组件
    Text('第二行'),
    SizedBox(height: 16),
    SizedBox(             // 限制子组件的宽高
      width: 200,
      height: 50,
      child: ElevatedButton(onPressed: () {}, child: Text('按钮')),
    ),
  ],
)

// Card - 带圆角阴影的 Material 卡片
Card(
  elevation: 4,                       // 阴影高度
  margin: EdgeInsets.all(12),
  shape: RoundedRectangleBorder(
    borderRadius: BorderRadius.circular(16),
  ),
  child: Padding(
    padding: EdgeInsets.all(16),
    child: Column(
      crossAxisAlignment: CrossAxisAlignment.start,
      children: [
        Text('卡片标题', style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold)),
        SizedBox(height: 8),
        Text('这是卡片的内容描述,Card 组件自带阴影和圆角效果。'),
      ],
    ),
  ),
)
提示:当只需要间距时,优先使用 SizedBoxPadding,它们比 Container 更轻量。Container 内部实际上是多个组件的组合(Padding、DecoratedBox、ConstrainedBox 等),仅在需要装饰效果时使用。

六、线性布局

Row(水平排列)和 Column(垂直排列)是最基础的线性布局组件,搭配 ExpandedFlexible 实现弹性布局。

6.1 Row 与 Column 基础

// Row - 水平排列子组件
Row(
  // 主轴(水平方向)对齐方式
  mainAxisAlignment: MainAxisAlignment.spaceBetween,
  // 交叉轴(垂直方向)对齐方式
  crossAxisAlignment: CrossAxisAlignment.center,
  children: [
    Icon(Icons.home, size: 32),
    Text('首页', style: TextStyle(fontSize: 18)),
    Icon(Icons.arrow_forward),
  ],
)

// Column - 垂直排列子组件
Column(
  mainAxisAlignment: MainAxisAlignment.center,
  crossAxisAlignment: CrossAxisAlignment.start,  // 左对齐
  children: [
    Text('标题', style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold)),
    SizedBox(height: 8),
    Text('副标题', style: TextStyle(fontSize: 16, color: Colors.grey)),
    SizedBox(height: 16),
    ElevatedButton(onPressed: () {}, child: Text('操作')),
  ],
)

6.2 MainAxisAlignment 对齐方式

// MainAxisAlignment 的六种对齐方式示意

// start    : [A][B][C]              (默认)
// end      :              [A][B][C]
// center   :       [A][B][C]
// spaceBetween : [A]    [B]    [C]  (两端对齐)
// spaceAround  :  [A]   [B]   [C]  (均匀间距,两端为一半)
// spaceEvenly  :  [A]  [B]  [C]    (完全均匀间距)

// 实际使用示例 - 底部操作栏
Row(
  mainAxisAlignment: MainAxisAlignment.spaceEvenly,
  children: [
    Column(
      mainAxisSize: MainAxisSize.min,
      children: [Icon(Icons.home), Text('首页')],
    ),
    Column(
      mainAxisSize: MainAxisSize.min,
      children: [Icon(Icons.search), Text('搜索')],
    ),
    Column(
      mainAxisSize: MainAxisSize.min,
      children: [Icon(Icons.person), Text('我的')],
    ),
  ],
)

6.3 Expanded 与 Flexible

// Expanded - 填充剩余空间
Row(
  children: [
    // 头像区域 - 固定宽度
    CircleAvatar(radius: 25, child: Icon(Icons.person)),
    SizedBox(width: 12),
    // 文本区域 - 自动填满剩余空间
    Expanded(
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          Text('张三', style: TextStyle(fontWeight: FontWeight.bold)),
          Text('这是一条很长的消息,超出部分会自动换行或截断...',
              maxLines: 1, overflow: TextOverflow.ellipsis),
        ],
      ),
    ),
    // 时间 - 固定宽度
    Text('14:30', style: TextStyle(color: Colors.grey)),
  ],
)

// flex 比例分配空间
Row(
  children: [
    Expanded(
      flex: 2,  // 占 2/3 空间
      child: Container(height: 80, color: Colors.blue[200]),
    ),
    SizedBox(width: 8),
    Expanded(
      flex: 1,  // 占 1/3 空间
      child: Container(height: 80, color: Colors.red[200]),
    ),
  ],
)

// Flexible - 可以选择不填满剩余空间
Row(
  children: [
    Flexible(
      fit: FlexFit.loose,  // 子组件可以小于分配的空间
      child: Container(
        width: 100,
        height: 50,
        color: Colors.green[200],
      ),
    ),
    Text('后面的内容'),
  ],
)
提示:Expanded 等价于 Flexible(fit: FlexFit.tight)。当 Row/Column 中的子组件总宽/高超出时会报溢出错误(黄黑条纹),此时可以用 Expanded 包裹让子组件自适应,或使用 SingleChildScrollView 让内容可滚动。mainAxisSize: MainAxisSize.min 可以让 Row/Column 仅占用子组件所需的空间。

七、层叠与流式布局

Stack 实现层叠布局(类似 CSS 的 absolute 定位),Wrap 实现自动换行的流式布局。

7.1 Stack 层叠布局

// Stack - 子组件层叠在一起
Stack(
  alignment: Alignment.center,  // 未定位子组件的默认对齐方式
  clipBehavior: Clip.none,     // 允许子组件超出边界
  children: [
    // 底层 - 背景图片
    Container(
      width: 300,
      height: 200,
      decoration: BoxDecoration(
        borderRadius: BorderRadius.circular(16),
        color: Colors.blue[100],
      ),
    ),
    // 中间层 - 文字
    Text(
      '层叠布局',
      style: TextStyle(fontSize: 28, fontWeight: FontWeight.bold),
    ),
    // 顶层 - 使用 Positioned 精确定位
    Positioned(
      top: 10,
      right: 10,
      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: 12)),
      ),
    ),
  ],
)

7.2 Positioned 精确定位

// 使用 Positioned 的各种定位方式
Stack(
  children: [
    Container(width: 300, height: 300, color: Colors.grey[200]),

    // 左上角定位
    Positioned(
      top: 10,
      left: 10,
      child: Icon(Icons.star, color: Colors.amber),
    ),

    // 底部拉伸 - 同时设置 left 和 right
    Positioned(
      bottom: 0,
      left: 0,
      right: 0,
      child: Container(
        padding: EdgeInsets.all(12),
        color: Colors.black54,
        child: Text('底部标题栏',
          style: TextStyle(color: Colors.white),
          textAlign: TextAlign.center),
      ),
    ),

    // Positioned.fill - 充满整个 Stack
    Positioned.fill(
      child: Center(child: Text('居中内容')),
    ),
  ],
)

7.3 Wrap 流式布局

// Wrap - 子组件超出一行时自动换行
Wrap(
  spacing: 8,       // 水平间距
  runSpacing: 8,    // 行间距
  alignment: WrapAlignment.start,
  children: [
    'Flutter', 'Dart', 'Widget', 'State', '布局',
    '动画', '导航', '网络请求', '状态管理',
  ].map((tag) => Chip(
    label: Text(tag),
    avatar: Icon(Icons.tag, size: 18),
    backgroundColor: Colors.blue[50],
    deleteIcon: Icon(Icons.close, size: 16),
    onDeleted: () => print('删除 $tag'),
  )).toList(),
)

// Wrap 实现标签选择器
Wrap(
  spacing: 10,
  runSpacing: 10,
  children: List.generate(8, (index) {
    return ChoiceChip(
      label: Text('标签 $index'),
      selected: index == 0,   // 选中状态
      onSelected: (selected) {},
    );
  }),
)
提示:Stack 中未被 Positioned 包裹的子组件会根据 alignment 属性对齐。Positioned 必须是 Stack 的直接子组件。当需要实现类似"标签云"或"商品标签"等自适应换行的布局时,使用 Wrap 非常方便。

八、列表与滚动

列表是移动应用中最常见的界面模式。Flutter 提供了 ListViewGridViewCustomScrollView 等滚动组件。

8.1 ListView.builder 高性能列表

// ListView.builder - 按需构建列表项,适合长列表
ListView.builder(
  itemCount: 100,         // 列表项总数
  itemBuilder: (BuildContext context, int index) {
    return ListTile(
      leading: CircleAvatar(
        child: Text('${index + 1}'),
      ),
      title: Text('列表项 $index'),
      subtitle: Text('这是第 $index 项的描述'),
      trailing: Icon(Icons.chevron_right),
      onTap: () => print('点击了第 $index 项'),
    );
  },
)

// ListView.separated - 带分隔线的列表
ListView.separated(
  itemCount: 50,
  separatorBuilder: (BuildContext context, int index) {
    return Divider(height: 1, indent: 72);  // 分隔线
  },
  itemBuilder: (BuildContext context, int index) {
    return ListTile(
      leading: Icon(Icons.article),
      title: Text('文章标题 $index'),
    );
  },
)

8.2 GridView 网格布局

// GridView.builder - 高性能网格列表
GridView.builder(
  gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
    crossAxisCount: 2,        // 每行 2 列
    crossAxisSpacing: 12,     // 列间距
    mainAxisSpacing: 12,      // 行间距
    childAspectRatio: 0.75,   // 宽高比
  ),
  padding: EdgeInsets.all(12),
  itemCount: 20,
  itemBuilder: (BuildContext context, int index) {
    return Card(
      clipBehavior: Clip.antiAlias,
      shape: RoundedRectangleBorder(
        borderRadius: BorderRadius.circular(12),
      ),
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          Expanded(
            child: Container(
              color: Colors.primaries[index % 18][100],
              child: Center(child: Icon(Icons.image, size: 48)),
            ),
          ),
          Padding(
            padding: EdgeInsets.all(8),
            child: Text('商品 $index', style: TextStyle(fontWeight: FontWeight.bold)),
          ),
          Padding(
            padding: EdgeInsets.symmetric(horizontal: 8),
            child: Text('¥${(index + 1) * 29.9}',
              style: TextStyle(color: Colors.red)),
          ),
          SizedBox(height: 8),
        ],
      ),
    );
  },
)

// 自适应列数的网格
GridView.builder(
  gridDelegate: SliverGridDelegateWithMaxCrossAxisExtent(
    maxCrossAxisExtent: 180,  // 每项最大宽度,列数自动计算
    crossAxisSpacing: 10,
    mainAxisSpacing: 10,
  ),
  itemCount: 30,
  itemBuilder: (context, index) => Container(color: Colors.teal[100]),
)

8.3 CustomScrollView 与 Slivers

CustomScrollView 允许组合多种 Sliver 组件,实现复杂的滚动效果。

// CustomScrollView - 组合多种滚动组件
CustomScrollView(
  slivers: [
    // SliverAppBar - 可折叠的应用栏
    SliverAppBar(
      expandedHeight: 200,
      floating: false,
      pinned: true,          // 收缩后固定在顶部
      flexibleSpace: FlexibleSpaceBar(
        title: Text('我的主页'),
        background: Image.network(
          'https://example.com/header.jpg',
          fit: BoxFit.cover,
        ),
      ),
    ),

    // SliverToBoxAdapter - 放置普通组件
    SliverToBoxAdapter(
      child: Padding(
        padding: EdgeInsets.all(16),
        child: Text('热门推荐', style: TextStyle(fontSize: 20)),
      ),
    ),

    // SliverGrid - 网格列表
    SliverGrid(
      gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
        crossAxisCount: 3,
        mainAxisSpacing: 8,
        crossAxisSpacing: 8,
      ),
      delegate: SliverChildBuilderDelegate(
        (context, index) => Container(
          color: Colors.primaries[index % 18],
          child: Center(child: Text('$index', style: TextStyle(color: Colors.white))),
        ),
        childCount: 9,
      ),
    ),

    // SliverList - 列表
    SliverList(
      delegate: SliverChildBuilderDelegate(
        (context, index) => ListTile(
          title: Text('列表项 $index'),
        ),
        childCount: 20,
      ),
    ),
  ],
)

8.4 PageView 与 RefreshIndicator

// PageView - 页面切换(轮播图)
SizedBox(
  height: 200,
  child: PageView.builder(
    itemCount: 5,
    controller: PageController(viewportFraction: 0.85), // 显示相邻页的一部分
    itemBuilder: (context, index) {
      return Padding(
        padding: EdgeInsets.symmetric(horizontal: 8),
        child: Container(
          decoration: BoxDecoration(
            color: Colors.primaries[index % 18][200],
            borderRadius: BorderRadius.circular(16),
          ),
          child: Center(child: Text('页面 ${index + 1}', style: TextStyle(fontSize: 24))),
        ),
      );
    },
  ),
)

// RefreshIndicator - 下拉刷新
RefreshIndicator(
  onRefresh: () async {
    // 模拟网络请求
    await Future.delayed(Duration(seconds: 2));
    // 刷新数据...
  },
  child: ListView.builder(
    itemCount: 30,
    itemBuilder: (context, index) => ListTile(
      title: Text('可下拉刷新的列表项 $index'),
    ),
  ),
)
提示:ListView.builderGridView.builder 采用懒加载机制,只会构建可见区域内的组件,非常适合长列表。避免在 ListView 内嵌套 ListView,如需要请对内层列表设置 shrinkWrap: truephysics: NeverScrollableScrollPhysics()。复杂滚动页面推荐使用 CustomScrollView

九、弹窗与提示

弹窗和提示是与用户交互的重要方式。Flutter 提供了 AlertDialogBottomSheetSnackBar 等组件。

9.1 AlertDialog 对话框

// 显示确认对话框
void showConfirmDialog(BuildContext context) {
  showDialog(
    context: context,
    barrierDismissible: false,  // 点击外部不关闭
    builder: (BuildContext context) {
      return AlertDialog(
        title: Text('确认删除'),
        content: Text('删除后将无法恢复,确定要删除吗?'),
        shape: RoundedRectangleBorder(
          borderRadius: BorderRadius.circular(16),
        ),
        actions: [
          TextButton(
            onPressed: () => Navigator.pop(context),  // 关闭弹窗
            child: Text('取消'),
          ),
          ElevatedButton(
            onPressed: () {
              // 执行删除操作
              Navigator.pop(context);
              print('已删除');
            },
            style: ElevatedButton.styleFrom(backgroundColor: Colors.red),
            child: Text('删除'),
          ),
        ],
      );
    },
  );
}

// 获取对话框返回值
Future<void> showChoiceDialog(BuildContext context) async {
  final result = await showDialog<String>(
    context: context,
    builder: (context) => SimpleDialog(
      title: Text('选择语言'),
      children: [
        SimpleDialogOption(
          onPressed: () => Navigator.pop(context, '中文'),
          child: Text('中文'),
        ),
        SimpleDialogOption(
          onPressed: () => Navigator.pop(context, 'English'),
          child: Text('English'),
        ),
      ],
    ),
  );
  print('选择了: $result');
}

9.2 BottomSheet 底部弹出面板

// 模态底部面板
void showBottomOptions(BuildContext context) {
  showModalBottomSheet(
    context: context,
    shape: RoundedRectangleBorder(
      borderRadius: BorderRadius.vertical(top: Radius.circular(20)),
    ),
    builder: (BuildContext context) {
      return Container(
        padding: EdgeInsets.all(20),
        child: Column(
          mainAxisSize: MainAxisSize.min,  // 高度自适应内容
          children: [
            // 顶部拖动指示条
            Container(
              width: 40,
              height: 4,
              decoration: BoxDecoration(
                color: Colors.grey[300],
                borderRadius: BorderRadius.circular(2),
              ),
            ),
            SizedBox(height: 20),
            ListTile(
              leading: Icon(Icons.camera_alt, color: Colors.blue),
              title: Text('拍照'),
              onTap: () => Navigator.pop(context),
            ),
            ListTile(
              leading: Icon(Icons.photo_library, color: Colors.green),
              title: Text('从相册选择'),
              onTap: () => Navigator.pop(context),
            ),
            ListTile(
              leading: Icon(Icons.delete, color: Colors.red),
              title: Text('删除图片'),
              onTap: () => Navigator.pop(context),
            ),
          ],
        ),
      );
    },
  );
}

9.3 SnackBar 底部提示条

// 显示 SnackBar 提示
void showSnackMessage(BuildContext context) {
  ScaffoldMessenger.of(context).showSnackBar(
    SnackBar(
      content: Text('操作成功!'),
      duration: Duration(seconds: 3),  // 显示时长
      backgroundColor: Colors.green,
      behavior: SnackBarBehavior.floating,  // 悬浮样式
      shape: RoundedRectangleBorder(
        borderRadius: BorderRadius.circular(10),
      ),
      margin: EdgeInsets.all(16),
      action: SnackBarAction(
        label: '撤销',
        textColor: Colors.white,
        onPressed: () {
          print('撤销操作');
        },
      ),
    ),
  );
}
提示:showDialog 返回 Future,可以通过 await 获取弹窗的返回值。SnackBar 使用 ScaffoldMessenger 来显示,确保页面有 Scaffold 组件。底部面板可以设置 isScrollControlled: true 来实现全屏或可拖拽的效果。

十、响应式布局

为了适配不同屏幕尺寸,Flutter 提供了 MediaQueryLayoutBuilder 来获取屏幕信息和约束条件。

10.1 MediaQuery 获取屏幕信息

// 使用 MediaQuery 获取屏幕尺寸
@override
Widget build(BuildContext context) {
  // 获取屏幕尺寸
  final screenSize = MediaQuery.of(context).size;
  final screenWidth = screenSize.width;
  final screenHeight = screenSize.height;

  // 获取安全区域(避开刘海、底部导航条等)
  final padding = MediaQuery.of(context).padding;
  final topPadding = padding.top;      // 状态栏高度
  final bottomPadding = padding.bottom; // 底部安全区

  // 获取设备像素比
  final devicePixelRatio = MediaQuery.of(context).devicePixelRatio;

  // 横竖屏判断
  final isLandscape = screenWidth > screenHeight;

  // 根据屏幕宽度决定布局
  final isTablet = screenWidth > 600;

  return Scaffold(
    body: isTablet
        ? _buildTabletLayout()    // 平板布局
        : _buildPhoneLayout(),     // 手机布局
  );
}

10.2 LayoutBuilder 响应约束变化

// LayoutBuilder - 根据父组件给出的约束条件来决定布局
LayoutBuilder(
  builder: (BuildContext context, BoxConstraints constraints) {
    // constraints.maxWidth 是父组件给出的最大宽度
    if (constraints.maxWidth > 900) {
      // 大屏幕 - 三栏布局
      return Row(
        children: [
          SizedBox(width: 250, child: _buildSidebar()),
          Expanded(child: _buildMainContent()),
          SizedBox(width: 300, child: _buildRightPanel()),
        ],
      );
    } else if (constraints.maxWidth > 600) {
      // 中等屏幕 - 两栏布局
      return Row(
        children: [
          SizedBox(width: 250, child: _buildSidebar()),
          Expanded(child: _buildMainContent()),
        ],
      );
    } else {
      // 小屏幕 - 单栏布局
      return _buildMainContent();
    }
  },
)

// 实用示例:自适应网格列数
LayoutBuilder(
  builder: (context, constraints) {
    // 根据宽度动态计算列数
    int crossAxisCount = (constraints.maxWidth / 180).floor();
    crossAxisCount = crossAxisCount.clamp(2, 6);  // 限制 2-6 列

    return GridView.builder(
      gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
        crossAxisCount: crossAxisCount,
        crossAxisSpacing: 12,
        mainAxisSpacing: 12,
      ),
      itemCount: 20,
      itemBuilder: (context, index) => Card(
        child: Center(child: Text('Item $index')),
      ),
    );
  },
)

10.3 完整响应式页面示例

class ResponsivePage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('响应式布局')),
      // 仅在小屏幕显示侧边栏抽屉
      drawer: MediaQuery.of(context).size.width < 600
          ? Drawer(child: _buildNavMenu())
          : null,
      body: SafeArea(
        child: LayoutBuilder(
          builder: (context, constraints) {
            if (constraints.maxWidth >= 600) {
              // 大屏 - 左侧导航 + 右侧内容
              return Row(
                children: [
                  NavigationRail(
                    selectedIndex: 0,
                    destinations: [
                      NavigationRailDestination(
                        icon: Icon(Icons.home),
                        label: Text('首页'),
                      ),
                      NavigationRailDestination(
                        icon: Icon(Icons.settings),
                        label: Text('设置'),
                      ),
                    ],
                    onDestinationSelected: (index) {},
                  ),
                  VerticalDivider(width: 1),
                  Expanded(child: _buildContent()),
                ],
              );
            }
            // 小屏 - 底部导航
            return _buildContent();
          },
        ),
      ),
      // 小屏幕时显示底部导航
      bottomNavigationBar: MediaQuery.of(context).size.width < 600
          ? BottomNavigationBar(
              items: [
                BottomNavigationBarItem(icon: Icon(Icons.home), label: '首页'),
                BottomNavigationBarItem(icon: Icon(Icons.settings), label: '设置'),
              ],
            )
          : null,
    );
  }
}
提示:LayoutBuilder 获取的是父组件给出的约束,而 MediaQuery 获取的是整个屏幕的信息。优先使用 LayoutBuilder,因为它能正确响应组件在不同位置时的宽度变化。SafeArea 组件可以自动避开状态栏、底部导航条等系统区域。

十一、实践练习

通过以下练习巩固本章学习的组件和布局知识。

练习一:个人名片卡

使用 CardRowColumnCircleAvatar 等组件,构建一个包含头像、姓名、职位和联系方式的个人名片卡。

要求:

  • 左侧显示圆形头像
  • 右侧显示姓名(大号加粗)、职位(灰色)和邮箱
  • 底部有两个操作按钮:"发消息"和"打电话"
  • 卡片带圆角和阴影效果
// 参考实现
Card(
  elevation: 4,
  margin: EdgeInsets.all(16),
  shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
  child: Padding(
    padding: EdgeInsets.all(16),
    child: Column(
      mainAxisSize: MainAxisSize.min,
      children: [
        Row(
          children: [
            CircleAvatar(
              radius: 36,
              backgroundImage: NetworkImage('https://example.com/avatar.jpg'),
            ),
            SizedBox(width: 16),
            Expanded(
              child: Column(
                crossAxisAlignment: CrossAxisAlignment.start,
                children: [
                  Text('张三', style: TextStyle(fontSize: 22, fontWeight: FontWeight.bold)),
                  SizedBox(height: 4),
                  Text('高级 Flutter 开发工程师', style: TextStyle(color: Colors.grey[600])),
                  SizedBox(height: 4),
                  Text('zhangsan@example.com', style: TextStyle(color: Colors.blue)),
                ],
              ),
            ),
          ],
        ),
        SizedBox(height: 16),
        Divider(),
        Row(
          mainAxisAlignment: MainAxisAlignment.spaceEvenly,
          children: [
            TextButton.icon(
              icon: Icon(Icons.message),
              label: Text('发消息'),
              onPressed: () {},
            ),
            TextButton.icon(
              icon: Icon(Icons.phone),
              label: Text('打电话'),
              onPressed: () {},
            ),
          ],
        ),
      ],
    ),
  ),
)

练习二:商品列表页

使用 GridView.builderCard 构建一个商品列表页面。

要求:

  • 每行显示两个商品卡片
  • 每个卡片包含商品图片、名称、价格
  • 价格用红色显示
  • 使用 LayoutBuilder 在宽屏时显示更多列
// 参考实现 - 自适应商品网格
LayoutBuilder(
  builder: (context, constraints) {
    int columns = (constraints.maxWidth / 200).floor().clamp(2, 4);
    return GridView.builder(
      padding: EdgeInsets.all(12),
      gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
        crossAxisCount: columns,
        crossAxisSpacing: 12,
        mainAxisSpacing: 12,
        childAspectRatio: 0.7,
      ),
      itemCount: 10,
      itemBuilder: (context, index) {
        return Card(
          clipBehavior: Clip.antiAlias,
          shape: RoundedRectangleBorder(
            borderRadius: BorderRadius.circular(12),
          ),
          child: Column(
            crossAxisAlignment: CrossAxisAlignment.start,
            children: [
              // 商品图片占位
              Expanded(
                child: Container(
                  width: double.infinity,
                  color: Colors.grey[200],
                  child: Icon(Icons.shopping_bag, size: 48, color: Colors.grey),
                ),
              ),
              Padding(
                padding: EdgeInsets.fromLTRB(8, 8, 8, 4),
                child: Text('精选商品 ${index + 1}',
                  style: TextStyle(fontWeight: FontWeight.bold),
                  maxLines: 1, overflow: TextOverflow.ellipsis),
              ),
              Padding(
                padding: EdgeInsets.fromLTRB(8, 0, 8, 8),
                child: Text('¥${((index + 1) * 49.9).toStringAsFixed(1)}',
                  style: TextStyle(color: Colors.red, fontSize: 16, fontWeight: FontWeight.bold)),
              ),
            ],
          ),
        );
      },
    );
  },
)

练习三:登录页面

综合运用 FormContainerColumn 等组件构建完整的登录页面。

要求:

  • 顶部显示应用 Logo 和标题
  • 包含邮箱和密码两个输入框,带验证功能
  • 包含"记住密码"复选框和"忘记密码"文字按钮
  • 渐变色的登录按钮
  • 底部有"注册账号"的入口
// 参考实现 - 登录页面
class LoginPage extends StatefulWidget {
  @override
  _LoginPageState createState() => _LoginPageState();
}

class _LoginPageState extends State<LoginPage> {
  final _formKey = GlobalKey<FormState>();
  bool _rememberMe = false;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: SafeArea(
        child: Center(
          child: SingleChildScrollView(
            padding: EdgeInsets.all(32),
            child: Form(
              key: _formKey,
              child: Column(
                children: [
                  // Logo
                  Icon(Icons.flutter_dash, size: 80, color: Colors.blue),
                  SizedBox(height: 16),
                  Text('欢迎回来', style: TextStyle(fontSize: 28, fontWeight: FontWeight.bold)),
                  SizedBox(height: 8),
                  Text('请登录您的账号', style: TextStyle(color: Colors.grey)),
                  SizedBox(height: 40),

                  // 邮箱输入
                  TextFormField(
                    decoration: InputDecoration(
                      labelText: '邮箱',
                      prefixIcon: Icon(Icons.email_outlined),
                      border: OutlineInputBorder(
                        borderRadius: BorderRadius.circular(12),
                      ),
                    ),
                    validator: (v) => v != null && v.contains('@')
                        ? null : '请输入有效邮箱',
                  ),
                  SizedBox(height: 16),

                  // 密码输入
                  TextFormField(
                    obscureText: true,
                    decoration: InputDecoration(
                      labelText: '密码',
                      prefixIcon: Icon(Icons.lock_outlined),
                      border: OutlineInputBorder(
                        borderRadius: BorderRadius.circular(12),
                      ),
                    ),
                    validator: (v) => v != null && v.length >= 6
                        ? null : '密码至少6位',
                  ),
                  SizedBox(height: 8),

                  // 记住密码 & 忘记密码
                  Row(
                    mainAxisAlignment: MainAxisAlignment.spaceBetween,
                    children: [
                      Row(
                        children: [
                          Checkbox(
                            value: _rememberMe,
                            onChanged: (v) => setState(() => _rememberMe = v!),
                          ),
                          Text('记住密码'),
                        ],
                      ),
                      TextButton(
                        onPressed: () {},
                        child: Text('忘记密码?'),
                      ),
                    ],
                  ),
                  SizedBox(height: 24),

                  // 渐变登录按钮
                  SizedBox(
                    width: double.infinity,
                    height: 50,
                    child: Container(
                      decoration: BoxDecoration(
                        gradient: LinearGradient(
                          colors: [Colors.blue, Colors.purple],
                        ),
                        borderRadius: BorderRadius.circular(12),
                      ),
                      child: ElevatedButton(
                        onPressed: () {
                          if (_formKey.currentState!.validate()) {
                            print('登录成功');
                          }
                        },
                        style: ElevatedButton.styleFrom(
                          backgroundColor: Colors.transparent,
                          shadowColor: Colors.transparent,
                          shape: RoundedRectangleBorder(
                            borderRadius: BorderRadius.circular(12),
                          ),
                        ),
                        child: Text('登 录',
                          style: TextStyle(fontSize: 18, color: Colors.white)),
                      ),
                    ),
                  ),
                  SizedBox(height: 24),

                  // 注册入口
                  Row(
                    mainAxisAlignment: MainAxisAlignment.center,
                    children: [
                      Text('还没有账号?', style: TextStyle(color: Colors.grey)),
                      TextButton(
                        onPressed: () {},
                        child: Text('立即注册'),
                      ),
                    ],
                  ),
                ],
              ),
            ),
          ),
        ),
      ),
    );
  }
}

目录