Flutter 笔记(下)

42 阅读20分钟

Flutter 笔记(下)

四、布局组件

5. 基础容器组件

Container(容器组件)

定义: Container 是最常用的容器组件,可以包含一个子组件,并提供装饰、定位、尺寸等功能。

常用属性:

Container(
  // 尺寸
  width: 200,
  height: 100,
  
  // 内边距
  padding: EdgeInsets.all(16),
  
  // 外边距
  margin: EdgeInsets.symmetric(horizontal: 20, vertical: 10),
  
  // 对齐方式
  alignment: Alignment.center,
  
  // 装饰(背景色、边框、圆角等)
  decoration: BoxDecoration(
    color: Colors.blue,
    borderRadius: BorderRadius.circular(10),
    border: Border.all(color: Colors.black, width: 2),
    boxShadow: [
      BoxShadow(
        color: Colors.grey,
        blurRadius: 5,
        offset: Offset(2, 2),
      ),
    ],
  ),
  
  // 子组件
  child: Text('Hello'),
)

EdgeInsets 用法:

// 所有方向相同
EdgeInsets.all(16)

// 上下左右分别设置
EdgeInsets.only(left: 10, top: 20, right: 10, bottom: 20)

// 水平和垂直
EdgeInsets.symmetric(horizontal: 20, vertical: 10)

// 从 LTRB(左、上、右、下)
EdgeInsets.fromLTRB(10, 20, 10, 20)

实际示例:

Container(
  width: 300,
  height: 200,
  margin: EdgeInsets.all(20),
  padding: EdgeInsets.all(16),
  decoration: BoxDecoration(
    gradient: LinearGradient(
      colors: [Colors.blue, Colors.purple],
    ),
    borderRadius: BorderRadius.circular(15),
  ),
  child: Column(
    mainAxisAlignment: MainAxisAlignment.center,
    children: [
      Icon(Icons.star, size: 50, color: Colors.white),
      SizedBox(height: 10),
      Text(
        '精美卡片',
        style: TextStyle(color: Colors.white, fontSize: 24),
      ),
    ],
  ),
)

Center(居中组件)

定义: 将子组件在父组件中居中显示。

用法:

Center(
  child: Text('我在中间'),
)

// 等价于
Container(
  alignment: Alignment.center,
  child: Text('我在中间'),
)

Align(对齐组件)

定义: 可以精确控制子组件的对齐位置。

Alignment 常用值:

Align(
  alignment: Alignment.topLeft,      // 左上
  // alignment: Alignment.topCenter,    // 上中
  // alignment: Alignment.topRight,     // 右上
  // alignment: Alignment.centerLeft,   // 左中
  // alignment: Alignment.center,       // 中心
  // alignment: Alignment.centerRight,  // 右中
  // alignment: Alignment.bottomLeft,   // 左下
  // alignment: Alignment.bottomCenter, // 下中
  // alignment: Alignment.bottomRight,  // 右下
  child: Text('对齐位置'),
)

自定义对齐:

Align(
  // x, y 取值范围 -1.0 到 1.0
  // (-1, -1) 为左上角,(1, 1) 为右下角
  alignment: Alignment(0.5, 0.5),
  child: Text('自定义位置'),
)

Padding(内边距组件)

定义: 为子组件添加内边距。

用法:

Padding(
  padding: EdgeInsets.all(20),
  child: Text('有内边距的文本'),
)

// 不同方向的内边距
Padding(
  padding: EdgeInsets.only(left: 16, top: 8),
  child: Text('左上有内边距'),
)

提示: PaddingContainer 更轻量,如果只需要内边距,使用 Padding 更高效。


6. 线性布局

Column(垂直布局)

定义: 将子组件垂直排列。

核心属性:

Column(
  // 主轴对齐(垂直方向)
  mainAxisAlignment: MainAxisAlignment.start,
  // MainAxisAlignment.start      - 顶部对齐
  // MainAxisAlignment.end        - 底部对齐
  // MainAxisAlignment.center     - 居中对齐
  // MainAxisAlignment.spaceBetween - 两端对齐,中间平分空间
  // MainAxisAlignment.spaceAround  - 每个组件周围有相等空间
  // MainAxisAlignment.spaceEvenly  - 空间均匀分布
  
  // 交叉轴对齐(水平方向)
  crossAxisAlignment: CrossAxisAlignment.center,
  // CrossAxisAlignment.start   - 左对齐
  // CrossAxisAlignment.end     - 右对齐
  // CrossAxisAlignment.center  - 居中对齐
  // CrossAxisAlignment.stretch - 拉伸填充
  
  // 主轴尺寸
  mainAxisSize: MainAxisSize.max,  // max 或 min
  
  children: [
    Text('第一行'),
    Text('第二行'),
    Text('第三行'),
  ],
)

实际示例:

Column(
  mainAxisAlignment: MainAxisAlignment.spaceEvenly,
  crossAxisAlignment: CrossAxisAlignment.start,
  children: [
    Container(
      width: 100,
      height: 50,
      color: Colors.red,
      child: Center(child: Text('Box 1')),
    ),
    Container(
      width: 150,
      height: 50,
      color: Colors.green,
      child: Center(child: Text('Box 2')),
    ),
    Container(
      width: 120,
      height: 50,
      color: Colors.blue,
      child: Center(child: Text('Box 3')),
    ),
  ],
)

Row(水平布局)

定义: 将子组件水平排列。

用法:(属性与 Column 类似,但主轴和交叉轴相反)

Row(
  mainAxisAlignment: MainAxisAlignment.spaceBetween,
  crossAxisAlignment: CrossAxisAlignment.center,
  children: [
    Icon(Icons.home, size: 30),
    Icon(Icons.search, size: 30),
    Icon(Icons.person, size: 30),
  ],
)

组合使用示例:

Column(
  children: [
    Row(
      mainAxisAlignment: MainAxisAlignment.spaceBetween,
      children: [
        Text('标题'),
        Icon(Icons.more_vert),
      ],
    ),
    SizedBox(height: 10),
    Row(
      children: [
        Icon(Icons.star, color: Colors.yellow),
        Icon(Icons.star, color: Colors.yellow),
        Icon(Icons.star, color: Colors.yellow),
        Text('4.5'),
      ],
    ),
  ],
)

Expanded 和 Flexible

Expanded(填充可用空间)

定义: 在 Row 或 Column 中,按比例分配剩余空间。

用法:

Row(
  children: [
    // 固定宽度
    Container(
      width: 50,
      height: 50,
      color: Colors.red,
    ),
    
    // 占据剩余空间
    Expanded(
      child: Container(
        height: 50,
        color: Colors.green,
        child: Center(child: Text('填充剩余空间')),
      ),
    ),
    
    // 固定宽度
    Container(
      width: 50,
      height: 50,
      color: Colors.blue,
    ),
  ],
)

flex 属性(按比例分配):

Row(
  children: [
    Expanded(
      flex: 1,  // 占 1 份
      child: Container(
        height: 50,
        color: Colors.red,
        child: Center(child: Text('1')),
      ),
    ),
    Expanded(
      flex: 2,  // 占 2 份
      child: Container(
        height: 50,
        color: Colors.green,
        child: Center(child: Text('2')),
      ),
    ),
    Expanded(
      flex: 1,  // 占 1 份
      child: Container(
        height: 50,
        color: Colors.blue,
        child: Center(child: Text('1')),
      ),
    ),
  ],
)
// 比例:1:2:1

Flexible(灵活布局)

定义: 与 Expanded 类似,但不强制填充空间。

区别:

Row(
  children: [
    // Flexible 不强制占满
    Flexible(
      child: Container(
        width: 100,  // 可以有自己的宽度
        height: 50,
        color: Colors.red,
      ),
    ),
    
    // Expanded 强制占满
    Expanded(
      child: Container(
        height: 50,
        color: Colors.green,
      ),
    ),
  ],
)

Flex 布局

定义: Row 和 Column 的父类,可以通过 direction 属性动态决定方向。

用法:

Flex(
  direction: Axis.horizontal,  // 或 Axis.vertical
  children: [
    Expanded(
      flex: 1,
      child: Container(color: Colors.red, height: 50),
    ),
    Expanded(
      flex: 2,
      child: Container(color: Colors.green, height: 50),
    ),
  ],
)

7. 弹性布局(Flex Layout)

使用场景: 当需要根据可用空间动态调整子组件大小时使用。

完整示例:

class FlexExample extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        // 等比例分配
        Container(
          height: 100,
          color: Colors.grey[300],
          child: Row(
            children: [
              Expanded(flex: 1, child: _buildBox(Colors.red, '1')),
              Expanded(flex: 1, child: _buildBox(Colors.green, '1')),
              Expanded(flex: 1, child: _buildBox(Colors.blue, '1')),
            ],
          ),
        ),
        
        SizedBox(height: 20),
        
        // 不同比例分配
        Container(
          height: 100,
          color: Colors.grey[300],
          child: Row(
            children: [
              Expanded(flex: 1, child: _buildBox(Colors.red, '1')),
              Expanded(flex: 2, child: _buildBox(Colors.green, '2')),
              Expanded(flex: 3, child: _buildBox(Colors.blue, '3')),
            ],
          ),
        ),
      ],
    );
  }
  
  Widget _buildBox(Color color, String text) {
    return Container(
      color: color,
      child: Center(
        child: Text(
          text,
          style: TextStyle(color: Colors.white, fontSize: 24),
        ),
      ),
    );
  }
}

Flexible 和 Expanded 详解:

1. Expanded(占满剩余空间):

Row(
  children: [
    Container(width: 100, height: 50, color: Colors.red),
    Expanded(
      child: Container(height: 50, color: Colors.green),  // 占满剩余空间
    ),
    Container(width: 100, height: 50, color: Colors.blue),
  ],
)

2. Flexible(灵活空间分配):

Row(
  children: [
    // 不会自动填充,根据内容大小
    Flexible(
      child: Container(
        height: 50,
        color: Colors.red,
        child: Text('短文本'),
      ),
    ),
    // fit: FlexFit.tight 等同于 Expanded
    Flexible(
      fit: FlexFit.tight,
      child: Container(height: 50, color: Colors.green),
    ),
  ],
)

3. Expanded vs Flexible:

// Expanded = Flexible(fit: FlexFit.tight)
// 区别:
// - Expanded:必须填满分配的空间
// - Flexible:可以小于分配的空间

Row(
  children: [
    Flexible(
      flex: 1,
      child: Container(
        height: 50,
        color: Colors.red,
        child: Text('我只占我需要的空间'),
      ),
    ),
    Expanded(
      flex: 1,
      child: Container(
        height: 50,
        color: Colors.green,
        child: Text('我占满分配的空间'),
      ),
    ),
  ],
)

4. flex 比例:

Column(
  children: [
    Expanded(
      flex: 1,  // 占 1/6 的空间
      child: Container(color: Colors.red),
    ),
    Expanded(
      flex: 2,  // 占 2/6 的空间
      child: Container(color: Colors.green),
    ),
    Expanded(
      flex: 3,  // 占 3/6 的空间
      child: Container(color: Colors.blue),
    ),
  ],
)

8. 间距组件

SizedBox(固定尺寸盒子)

定义: 创建固定宽高的空间,常用于添加间距。

用法:

Column(
  children: [
    Text('第一行'),
    SizedBox(height: 20),  // 垂直间距
    Text('第二行'),
    SizedBox(height: 40),
    Text('第三行'),
  ],
)

Row(
  children: [
    Text('左侧'),
    SizedBox(width: 30),  // 水平间距
    Text('右侧'),
  ],
)

作为固定尺寸容器:

SizedBox(
  width: 200,
  height: 100,
  child: ElevatedButton(
    onPressed: () {},
    child: Text('固定大小按钮'),
  ),
)

Spacer(弹性空白)

定义: 在 Flex 布局中创建可伸缩的空白空间。

用法:

Row(
  children: [
    Text('左侧'),
    Spacer(),  // 占据所有剩余空间
    Text('右侧'),
  ],
)

// 多个 Spacer 按比例分配空间
Row(
  children: [
    Text('左'),
    Spacer(flex: 1),  // 占 1/3
    Text('中'),
    Spacer(flex: 2),  // 占 2/3
    Text('右'),
  ],
)

Divider(分隔线)

定义: 水平分隔线。

用法:

Column(
  children: [
    Text('项目 1'),
    Divider(),  // 默认分隔线
    Text('项目 2'),
    Divider(
      height: 20,          // 分隔线占据的高度
      thickness: 2,        // 线的粗细
      indent: 16,          // 左侧缩进
      endIndent: 16,       // 右侧缩进
      color: Colors.grey,
    ),
    Text('项目 3'),
  ],
)

VerticalDivider(垂直分隔线):

Row(
  children: [
    Expanded(child: Text('左侧')),
    VerticalDivider(
      width: 20,
      thickness: 2,
      color: Colors.grey,
    ),
    Expanded(child: Text('右侧')),
  ],
)

9. 显示隐藏控制

Visibility(显示隐藏组件)

定义: 控制组件的显示和隐藏。

用法:

class VisibilityDemo extends StatefulWidget {
  @override
  _VisibilityDemoState createState() => _VisibilityDemoState();
}

class _VisibilityDemoState extends State<VisibilityDemo> {
  bool _isVisible = true;

  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        Visibility(
          visible: _isVisible,
          child: Container(
            width: 200,
            height: 100,
            color: Colors.blue,
            child: Center(child: Text('可见的组件')),
          ),
        ),
        ElevatedButton(
          onPressed: () {
            setState(() {
              _isVisible = !_isVisible;
            });
          },
          child: Text('切换显示'),
        ),
      ],
    );
  }
}

保持空间的隐藏:

Visibility(
  visible: false,
  maintainSize: true,         // 保持尺寸
  maintainAnimation: true,    // 保持动画
  maintainState: true,        // 保持状态
  child: Container(
    width: 200,
    height: 100,
    color: Colors.blue,
  ),
)

Opacity(透明度控制)

定义: 控制组件的透明度。

用法:

class OpacityDemo extends StatefulWidget {
  @override
  _OpacityDemoState createState() => _OpacityDemoState();
}

class _OpacityDemoState extends State<OpacityDemo> {
  double _opacity = 1.0;

  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        Opacity(
          opacity: _opacity,  // 0.0 完全透明,1.0 完全不透明
          child: Container(
            width: 200,
            height: 100,
            color: Colors.blue,
            child: Center(child: Text('透明度控制')),
          ),
        ),
        Slider(
          value: _opacity,
          min: 0.0,
          max: 1.0,
          onChanged: (value) {
            setState(() {
              _opacity = value;
            });
          },
        ),
      ],
    );
  }
}

Offstage(完全隐藏)

定义: 完全移除组件,不占据空间,不参与布局。

用法:

Offstage(
  offstage: true,  // true: 隐藏,false: 显示
  child: Container(
    width: 200,
    height: 100,
    color: Colors.blue,
  ),
)

Visibility vs Opacity vs Offstage:

Column(
  children: [
    // Visibility: 隐藏后不占空间,可以选择是否保持状态
    Visibility(visible: false, child: Text('Visibility')),
    
    // Opacity: 隐藏后仍占空间,可以渐变动画
    Opacity(opacity: 0.0, child: Text('Opacity')),
    
    // Offstage: 完全移除,不占空间,不参与布局
    Offstage(offstage: true, child: Text('Offstage')),
  ],
)

10. 层叠布局

Stack(堆叠组件)

定义: 允许子组件堆叠在一起,后面的组件会覆盖前面的组件。

基础用法:

Stack(
  children: [
    // 底层
    Container(
      width: 300,
      height: 300,
      color: Colors.blue,
    ),
    
    // 中层
    Container(
      width: 200,
      height: 200,
      color: Colors.green,
    ),
    
    // 顶层
    Container(
      width: 100,
      height: 100,
      color: Colors.red,
    ),
  ],
)

alignment 属性:

Stack(
  alignment: Alignment.center,  // 所有子组件居中对齐
  children: [
    Container(width: 300, height: 300, color: Colors.blue),
    Container(width: 200, height: 200, color: Colors.green),
    Container(width: 100, height: 100, color: Colors.red),
  ],
)

Positioned(定位组件)

定义: 在 Stack 中精确控制子组件的位置。

用法:

Stack(
  children: [
    // 背景
    Container(
      width: 300,
      height: 300,
      color: Colors.grey[300],
    ),
    
    // 左上角
    Positioned(
      left: 10,
      top: 10,
      child: Container(
        width: 50,
        height: 50,
        color: Colors.red,
      ),
    ),
    
    // 右上角
    Positioned(
      right: 10,
      top: 10,
      child: Container(
        width: 50,
        height: 50,
        color: Colors.green,
      ),
    ),
    
    // 底部居中
    Positioned(
      left: 0,
      right: 0,
      bottom: 10,
      child: Container(
        height: 50,
        color: Colors.blue,
        child: Center(child: Text('底部栏')),
      ),
    ),
  ],
)

实际应用 - 图片上的角标:

Stack(
  children: [
    // 主图片
    Image.network(
      'https://example.com/image.jpg',
      width: 200,
      height: 200,
      fit: BoxFit.cover,
    ),
    
    // 右上角角标
    Positioned(
      right: 5,
      top: 5,
      child: Container(
        padding: EdgeInsets.symmetric(horizontal: 8, vertical: 4),
        decoration: BoxDecoration(
          color: Colors.red,
          borderRadius: BorderRadius.circular(10),
        ),
        child: Text(
          'NEW',
          style: TextStyle(color: Colors.white, fontSize: 12),
        ),
      ),
    ),
    
    // 底部信息栏
    Positioned(
      left: 0,
      right: 0,
      bottom: 0,
      child: Container(
        padding: EdgeInsets.all(8),
        color: Colors.black.withOpacity(0.5),
        child: Text(
          '商品标题',
          style: TextStyle(color: Colors.white),
        ),
      ),
    ),
  ],
)

9. 流式布局

Wrap(自动换行组件)

定义: 当一行空间不足时,自动换行显示子组件。

用法:

Wrap(
  spacing: 8,          // 主轴方向间距(水平)
  runSpacing: 10,      // 交叉轴方向间距(垂直)
  alignment: WrapAlignment.start,  // 对齐方式
  children: [
    Chip(label: Text('标签1')),
    Chip(label: Text('标签2')),
    Chip(label: Text('标签3')),
    Chip(label: Text('标签4')),
    Chip(label: Text('标签5')),
    Chip(label: Text('标签6')),
  ],
)

实际应用 - 标签云:

class TagCloud extends StatelessWidget {
  final List<String> tags = [
    'Flutter', 'Dart', 'Material Design', 'iOS', 'Android',
    'Web', 'Desktop', 'Mobile', 'UI', 'UX',
  ];

  @override
  Widget build(BuildContext context) {
    return Wrap(
      spacing: 10,
      runSpacing: 10,
      children: tags.map((tag) {
        return Chip(
          label: Text(tag),
          backgroundColor: Colors.blue[100],
          deleteIcon: Icon(Icons.close, size: 18),
          onDeleted: () {
            print('删除: $tag');
          },
        );
      }).toList(),
    );
  }
}

Wrap 方向:

Wrap(
  direction: Axis.vertical,  // 垂直方向排列,自动换列
  spacing: 8,
  runSpacing: 10,
  children: [
    Container(width: 50, height: 50, color: Colors.red),
    Container(width: 50, height: 50, color: Colors.green),
    Container(width: 50, height: 50, color: Colors.blue),
  ],
)

10. 滚动布局基础

滚动布局类型:

  • SingleChildScrollView - 单个子组件可滚动
  • ListView - 列表滚动
  • GridView - 网格滚动
  • CustomScrollView - 自定义滚动(使用 Sliver)
  • PageView - 整页滚动

何时使用滚动:

  • 内容超出屏幕可见区域
  • 需要上下或左右滑动查看更多内容
  • 列表、网格等长列表场景

详细内容见第六章"滚动组件"


11. 尺寸约束组件

AspectRatio(宽高比)

定义: 强制子组件保持指定的宽高比。

基础用法:

AspectRatio(
  aspectRatio: 16 / 9,  // 宽高比 16:9
  child: Container(
    color: Colors.blue,
    child: Center(child: Text('16:9 宽高比')),
  ),
)

实际应用 - 视频播放器容器:

Container(
  width: double.infinity,
  child: AspectRatio(
    aspectRatio: 16 / 9,
    child: Container(
      color: Colors.black,
      child: Center(
        child: Icon(Icons.play_circle_outline, color: Colors.white, size: 64),
      ),
    ),
  ),
)

不同宽高比示例:

Column(
  children: [
    AspectRatio(
      aspectRatio: 1,  // 1:1 正方形
      child: Container(color: Colors.red),
    ),
    SizedBox(height: 10),
    AspectRatio(
      aspectRatio: 16 / 9,  // 16:9 横屏视频
      child: Container(color: Colors.green),
    ),
    SizedBox(height: 10),
    AspectRatio(
      aspectRatio: 4 / 3,  // 4:3 传统显示器
      child: Container(color: Colors.blue),
    ),
  ],
)

FractionallySizedBox(相对尺寸)

定义: 根据父容器的尺寸设置子组件的大小比例。

基础用法:

Container(
  width: 300,
  height: 200,
  color: Colors.grey[300],
  child: FractionallySizedBox(
    widthFactor: 0.5,   // 宽度是父容器的50%
    heightFactor: 0.8,  // 高度是父容器的80%
    alignment: Alignment.center,
    child: Container(
      color: Colors.blue,
      child: Center(child: Text('50% x 80%')),
    ),
  ),
)

实际应用 - 响应式布局:

// 在不同屏幕上保持相对大小
FractionallySizedBox(
  widthFactor: 0.9,  // 屏幕宽度的90%
  child: Container(
    padding: EdgeInsets.all(16),
    decoration: BoxDecoration(
      color: Colors.white,
      borderRadius: BorderRadius.circular(12),
      boxShadow: [
        BoxShadow(
          color: Colors.grey.withOpacity(0.3),
          blurRadius: 10,
          offset: Offset(0, 5),
        ),
      ],
    ),
    child: Text('卡片内容'),
  ),
)

不同对齐方式:

Container(
  width: 300,
  height: 200,
  color: Colors.grey[300],
  child: FractionallySizedBox(
    widthFactor: 0.6,
    heightFactor: 0.6,
    alignment: Alignment.topLeft,  // 左上角对齐
    child: Container(color: Colors.red),
  ),
)

ConstrainedBox(尺寸约束)

定义: 对子组件的尺寸施加额外的约束条件。

基础用法:

ConstrainedBox(
  constraints: BoxConstraints(
    minWidth: 100,      // 最小宽度
    maxWidth: 200,      // 最大宽度
    minHeight: 50,      // 最小高度
    maxHeight: 100,     // 最大高度
  ),
  child: Container(
    color: Colors.blue,
    child: Text('受约束的容器'),
  ),
)

只约束宽度:

ConstrainedBox(
  constraints: BoxConstraints(
    minWidth: 200,
    maxWidth: 400,
  ),
  child: ElevatedButton(
    onPressed: () {},
    child: Text('按钮'),
  ),
)

只约束高度:

ConstrainedBox(
  constraints: BoxConstraints(
    minHeight: 100,
    maxHeight: 200,
  ),
  child: TextField(
    maxLines: null,
    decoration: InputDecoration(hintText: '多行输入框'),
  ),
)

实际应用 - 限制图片大小:

ConstrainedBox(
  constraints: BoxConstraints(
    maxWidth: 300,
    maxHeight: 300,
  ),
  child: Image.network(
    'https://example.com/large-image.jpg',
    fit: BoxFit.contain,
  ),
)

BoxConstraints 常用构造函数:

// 紧约束(固定尺寸)
BoxConstraints.tight(Size(200, 100))

// 松约束(最大尺寸)
BoxConstraints.loose(Size(300, 200))

// 只约束宽度
BoxConstraints.tightFor(width: 200)

// 扩展到最大
BoxConstraints.expand(width: 200, height: 100)

12. 裁剪组件

ClipRRect(圆角裁剪)

定义: 使用圆角矩形裁剪子组件。

基础用法:

ClipRRect(
  borderRadius: BorderRadius.circular(12),
  child: Image.network(
    'https://picsum.photos/200/200',
    width: 200,
    height: 200,
    fit: BoxFit.cover,
  ),
)

不同圆角样式:

// 完全圆角
ClipRRect(
  borderRadius: BorderRadius.circular(100),
  child: Image.network(url, width: 200, height: 200),
)

// 只圆角某些角
ClipRRect(
  borderRadius: BorderRadius.only(
    topLeft: Radius.circular(20),
    topRight: Radius.circular(20),
  ),
  child: Image.network(url, width: 200, height: 200),
)

// 上下左右不同圆角
ClipRRect(
  borderRadius: BorderRadius.vertical(top: Radius.circular(20)),
  child: Container(
    width: 200,
    height: 200,
    color: Colors.blue,
  ),
)

实际应用 - 卡片顶部圆角图片:

Card(
  clipBehavior: Clip.antiAlias,
  child: Column(
    children: [
      ClipRRect(
        borderRadius: BorderRadius.vertical(top: Radius.circular(12)),
        child: Image.network(
          'https://picsum.photos/400/200',
          width: double.infinity,
          height: 200,
          fit: BoxFit.cover,
        ),
      ),
      Padding(
        padding: EdgeInsets.all(16),
        child: Text('卡片内容'),
      ),
    ],
  ),
)

ClipOval(圆形裁剪)

定义: 使用椭圆或圆形裁剪子组件。

基础用法:

ClipOval(
  child: Image.network(
    'https://picsum.photos/200/200',
    width: 100,
    height: 100,
    fit: BoxFit.cover,
  ),
)

实际应用 - 圆形头像:

ClipOval(
  child: Container(
    width: 80,
    height: 80,
    color: Colors.blue,
    child: Image.network(
      'https://example.com/avatar.jpg',
      fit: BoxFit.cover,
    ),
  ),
)

椭圆形裁剪:

ClipOval(
  child: Container(
    width: 200,
    height: 100,  // 不同的宽高产生椭圆
    color: Colors.blue,
    child: Center(child: Text('椭圆')),
  ),
)

ClipPath(自定义路径裁剪)

定义: 使用自定义路径裁剪子组件。

三角形裁剪:

ClipPath(
  clipper: TriangleClipper(),
  child: Container(
    width: 200,
    height: 200,
    color: Colors.blue,
  ),
)

// 自定义裁剪器
class TriangleClipper extends CustomClipper<Path> {
  @override
  Path getClip(Size size) {
    final path = Path();
    path.moveTo(size.width / 2, 0);
    path.lineTo(size.width, size.height);
    path.lineTo(0, size.height);
    path.close();
    return path;
  }

  @override
  bool shouldReclip(CustomClipper<Path> oldClipper) => false;
}

波浪形裁剪:

ClipPath(
  clipper: WaveClipper(),
  child: Container(
    width: double.infinity,
    height: 200,
    color: Colors.blue,
  ),
)

class WaveClipper extends CustomClipper<Path> {
  @override
  Path getClip(Size size) {
    final path = Path();
    path.lineTo(0, size.height - 40);
    
    var firstControlPoint = Offset(size.width / 4, size.height);
    var firstEndPoint = Offset(size.width / 2, size.height - 40);
    path.quadraticBezierTo(
      firstControlPoint.dx,
      firstControlPoint.dy,
      firstEndPoint.dx,
      firstEndPoint.dy,
    );
    
    var secondControlPoint = Offset(size.width * 3 / 4, size.height - 80);
    var secondEndPoint = Offset(size.width, size.height - 40);
    path.quadraticBezierTo(
      secondControlPoint.dx,
      secondControlPoint.dy,
      secondEndPoint.dx,
      secondEndPoint.dy,
    );
    
    path.lineTo(size.width, 0);
    path.close();
    return path;
  }

  @override
  bool shouldReclip(CustomClipper<Path> oldClipper) => false;
}

实际应用 - 优惠券卡片:

ClipPath(
  clipper: CouponClipper(),
  child: Container(
    width: 300,
    height: 100,
    color: Colors.orange,
    child: Center(child: Text('优惠券', style: TextStyle(fontSize: 24))),
  ),
)

class CouponClipper extends CustomClipper<Path> {
  @override
  Path getClip(Size size) {
    final path = Path();
    path.lineTo(0, size.height);
    path.lineTo(size.width - 20, size.height);
    path.lineTo(size.width, size.height / 2);
    path.lineTo(size.width - 20, 0);
    path.close();
    return path;
  }

  @override
  bool shouldReclip(CustomClipper<Path> oldClipper) => false;
}

ClipRect(矩形裁剪)

定义: 使用矩形裁剪子组件,通常配合 Align 使用。

基础用法:

ClipRect(
  child: Align(
    alignment: Alignment.topCenter,
    heightFactor: 0.5,  // 只显示上半部分
    child: Image.network(
      'https://picsum.photos/200/200',
      width: 200,
      height: 200,
    ),
  ),
)

实际应用 - 局部图片展示:

ClipRect(
  child: Align(
    alignment: Alignment.center,
    widthFactor: 0.8,
    heightFactor: 0.8,
    child: Image.network(url),
  ),
)

五、基础组件

11. 文本组件

Text(文本组件)

定义: 显示单一样式的文本内容。

基础用法:

Text('Hello Flutter')

常用属性:

Text(
  '这是一段文本',
  style: TextStyle(
    fontSize: 20,              // 字体大小
    fontWeight: FontWeight.bold,  // 字体粗细
    color: Colors.blue,        // 文字颜色
    letterSpacing: 2.0,        // 字母间距
    height: 1.5,               // 行高(倍数)
    decoration: TextDecoration.underline,  // 下划线
    decorationColor: Colors.red,  // 装饰线颜色
    fontStyle: FontStyle.italic,  // 斜体
  ),
  textAlign: TextAlign.center,  // 对齐方式
  maxLines: 2,                  // 最大行数
  overflow: TextOverflow.ellipsis,  // 溢出处理(省略号)
  softWrap: true,               // 是否自动换行
)

TextStyle 常用属性:

TextStyle(
  // 字体大小和粗细
  fontSize: 16,
  fontWeight: FontWeight.w400,  // w100-w900 或 bold, normal
  
  // 颜色
  color: Colors.black,
  backgroundColor: Colors.yellow,
  
  // 间距
  letterSpacing: 1.0,    // 字母间距
  wordSpacing: 2.0,      // 单词间距
  height: 1.2,           // 行高
  
  // 装饰
  decoration: TextDecoration.underline,  // none, underline, overline, lineThrough
  decorationStyle: TextDecorationStyle.solid,  // solid, double, dotted, dashed, wavy
  decorationColor: Colors.red,
  decorationThickness: 2.0,
  
  // 字体
  fontFamily: 'Roboto',
  fontStyle: FontStyle.italic,  // normal 或 italic
  
  // 阴影
  shadows: [
    Shadow(
      color: Colors.grey,
      offset: Offset(2, 2),
      blurRadius: 3,
    ),
  ],
)

文本对齐:

Text(
  '文本对齐',
  textAlign: TextAlign.left,    // 左对齐
  // textAlign: TextAlign.center,  // 居中
  // textAlign: TextAlign.right,   // 右对齐
  // textAlign: TextAlign.justify, // 两端对齐
)

文本溢出处理:

Text(
  '这是一段很长很长很长很长很长很长的文本',
  maxLines: 1,
  overflow: TextOverflow.ellipsis,  // 省略号
  // overflow: TextOverflow.clip,      // 裁剪
  // overflow: TextOverflow.fade,      // 渐隐
  // overflow: TextOverflow.visible,   // 可见(溢出)
)

RichText(富文本)

定义: 可以在一段文本中使用多种不同的样式。

基础用法:

RichText(
  text: TextSpan(
    text: '默认样式 ',
    style: TextStyle(color: Colors.black, fontSize: 16),
    children: [
      TextSpan(
        text: '粗体 ',
        style: TextStyle(fontWeight: FontWeight.bold),
      ),
      TextSpan(
        text: '红色 ',
        style: TextStyle(color: Colors.red),
      ),
      TextSpan(
        text: '大字号',
        style: TextStyle(fontSize: 24),
      ),
    ],
  ),
)

实际应用示例:

class PriceDisplay extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return RichText(
      text: TextSpan(
        style: TextStyle(color: Colors.black, fontSize: 16),
        children: [
          TextSpan(text: '原价 '),
          TextSpan(
            text: '¥199',
            style: TextStyle(
              decoration: TextDecoration.lineThrough,
              color: Colors.grey,
            ),
          ),
          TextSpan(text: ' 现价 '),
          TextSpan(
            text: '¥99',
            style: TextStyle(
              color: Colors.red,
              fontSize: 24,
              fontWeight: FontWeight.bold,
            ),
          ),
        ],
      ),
    );
  }
}

带点击事件的富文本:

RichText(
  text: TextSpan(
    text: '请阅读并同意 ',
    style: TextStyle(color: Colors.black),
    children: [
      TextSpan(
        text: '《用户协议》',
        style: TextStyle(color: Colors.blue),
        recognizer: TapGestureRecognizer()
          ..onTap = () {
            print('点击了用户协议');
          },
      ),
      TextSpan(text: ' 和 '),
      TextSpan(
        text: '《隐私政策》',
        style: TextStyle(color: Colors.blue),
        recognizer: TapGestureRecognizer()
          ..onTap = () {
            print('点击了隐私政策');
          },
      ),
    ],
  ),
)

12. 图片组件

Image(图片组件)

定义: 用于显示图片,支持多种图片来源。

1. 加载网络图片:

Image.network(
  'https://example.com/image.jpg',
  width: 200,
  height: 200,
  fit: BoxFit.cover,  // 填充方式
  loadingBuilder: (context, child, loadingProgress) {
    if (loadingProgress == null) return child;
    return Center(
      child: CircularProgressIndicator(
        value: loadingProgress.expectedTotalBytes != null
            ? loadingProgress.cumulativeBytesLoaded / 
              loadingProgress.expectedTotalBytes!
            : null,
      ),
    );
  },
  errorBuilder: (context, error, stackTrace) {
    return Icon(Icons.error, size: 50, color: Colors.red);
  },
)

2. 加载本地资源图片:

首先在 pubspec.yaml 中配置:

flutter:
  assets:
    - assets/images/
    - assets/icons/logo.png

然后使用:

Image.asset(
  'assets/images/logo.png',
  width: 100,
  height: 100,
)

3. 加载文件系统图片:

Image.file(
  File('/path/to/image.jpg'),
  width: 200,
  height: 200,
)

4. 加载内存图片:

Image.memory(
  bytes,  // Uint8List
  width: 200,
  height: 200,
)

BoxFit 图片填充方式
Image.network(
  'https://example.com/image.jpg',
  width: 200,
  height: 200,
  fit: BoxFit.cover,
  // BoxFit.cover     - 充满容器,可能裁剪
  // BoxFit.contain   - 完整显示,可能留白
  // BoxFit.fill      - 拉伸填满,可能变形
  // BoxFit.fitWidth  - 宽度填满
  // BoxFit.fitHeight - 高度填满
  // BoxFit.none      - 原始大小
  // BoxFit.scaleDown - 缩小以适应,不放大
)

实际应用 - 圆形头像:

ClipOval(
  child: Image.network(
    'https://example.com/avatar.jpg',
    width: 80,
    height: 80,
    fit: BoxFit.cover,
  ),
)

// 或者使用 CircleAvatar
CircleAvatar(
  radius: 40,
  backgroundImage: NetworkImage('https://example.com/avatar.jpg'),
)

圆角图片:

ClipRRect(
  borderRadius: BorderRadius.circular(10),
  child: Image.network(
    'https://example.com/image.jpg',
    width: 200,
    height: 200,
    fit: BoxFit.cover,
  ),
)

13. 文本输入组件

TextField(文本输入框)

定义: 用于接收用户输入的文本。

基础用法:

TextField(
  decoration: InputDecoration(
    labelText: '用户名',
    hintText: '请输入用户名',
    prefixIcon: Icon(Icons.person),
  ),
)

常用属性:

TextField(
  // 控制器
  controller: TextEditingController(),
  
  // 装饰
  decoration: InputDecoration(
    labelText: '标签',          // 浮动标签
    hintText: '提示文本',       // 占位符
    helperText: '帮助文本',     // 底部帮助
    errorText: '错误提示',      // 错误信息
    
    // 图标
    prefixIcon: Icon(Icons.person),  // 前置图标
    suffixIcon: Icon(Icons.clear),   // 后置图标
    
    // 边框
    border: OutlineInputBorder(),    // 外边框
    enabledBorder: OutlineInputBorder(
      borderSide: BorderSide(color: Colors.grey),
    ),
    focusedBorder: OutlineInputBorder(
      borderSide: BorderSide(color: Colors.blue, width: 2),
    ),
    
    // 填充
    filled: true,
    fillColor: Colors.grey[200],
    
    // 内边距
    contentPadding: EdgeInsets.all(16),
  ),
  
  // 键盘类型
  keyboardType: TextInputType.text,
  // TextInputType.number      - 数字键盘
  // TextInputType.phone       - 电话键盘
  // TextInputType.emailAddress - 邮箱键盘
  // TextInputType.url         - URL 键盘
  
  // 输入行为
  maxLines: 1,              // 最大行数
  maxLength: 20,            // 最大字符数
  obscureText: false,       // 是否隐藏输入(密码)
  autocorrect: true,        // 自动纠错
  enableSuggestions: true,  // 显示建议
  
  // 事件回调
  onChanged: (value) {
    print('输入内容:$value');
  },
  onSubmitted: (value) {
    print('提交内容:$value');
  },
  onEditingComplete: () {
    print('编辑完成');
  },
  
  // 样式
  style: TextStyle(fontSize: 16, color: Colors.black),
  textAlign: TextAlign.start,
)

完整的登录表单示例:

class LoginForm extends StatefulWidget {
  @override
  _LoginFormState createState() => _LoginFormState();
}

class _LoginFormState extends State<LoginForm> {
  final _usernameController = TextEditingController();
  final _passwordController = TextEditingController();
  bool _obscurePassword = true;

  @override
  void dispose() {
    _usernameController.dispose();
    _passwordController.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Padding(
      padding: EdgeInsets.all(16),
      child: Column(
        children: [
          // 用户名输入框
          TextField(
            controller: _usernameController,
            decoration: InputDecoration(
              labelText: '用户名',
              hintText: '请输入用户名',
              prefixIcon: Icon(Icons.person),
              border: OutlineInputBorder(),
            ),
            keyboardType: TextInputType.text,
          ),
          
          SizedBox(height: 16),
          
          // 密码输入框
          TextField(
            controller: _passwordController,
            decoration: InputDecoration(
              labelText: '密码',
              hintText: '请输入密码',
              prefixIcon: Icon(Icons.lock),
              suffixIcon: IconButton(
                icon: Icon(
                  _obscurePassword ? Icons.visibility : Icons.visibility_off,
                ),
                onPressed: () {
                  setState(() {
                    _obscurePassword = !_obscurePassword;
                  });
                },
              ),
              border: OutlineInputBorder(),
            ),
            obscureText: _obscurePassword,
          ),
          
          SizedBox(height: 24),
          
          // 登录按钮
          ElevatedButton(
            onPressed: () {
              String username = _usernameController.text;
              String password = _passwordController.text;
              print('用户名:$username,密码:$password');
            },
            child: Text('登录'),
            style: ElevatedButton.styleFrom(
              minimumSize: Size(double.infinity, 50),
            ),
          ),
        ],
      ),
    );
  }
}

TextFormField(表单输入框)

定义: TextField 的增强版本,支持表单验证。

基础用法:

Form(
  key: _formKey,
  child: Column(
    children: [
      TextFormField(
        decoration: InputDecoration(labelText: '邮箱'),
        validator: (value) {
          if (value == null || value.isEmpty) {
            return '请输入邮箱';
          }
          if (!value.contains('@')) {
            return '请输入有效的邮箱地址';
          }
          return null;  // 验证通过
        },
      ),
      
      ElevatedButton(
        onPressed: () {
          if (_formKey.currentState!.validate()) {
            print('验证通过');
          }
        },
        child: Text('提交'),
      ),
    ],
  ),
)

完整的注册表单示例:

class RegisterForm extends StatefulWidget {
  @override
  _RegisterFormState createState() => _RegisterFormState();
}

class _RegisterFormState extends State<RegisterForm> {
  final _formKey = GlobalKey<FormState>();
  final _emailController = TextEditingController();
  final _passwordController = TextEditingController();
  final _confirmPasswordController = TextEditingController();

  @override
  Widget build(BuildContext context) {
    return Form(
      key: _formKey,
      child: Padding(
        padding: EdgeInsets.all(16),
        child: Column(
          children: [
            // 邮箱
            TextFormField(
              controller: _emailController,
              decoration: InputDecoration(
                labelText: '邮箱',
                prefixIcon: Icon(Icons.email),
                border: OutlineInputBorder(),
              ),
              keyboardType: TextInputType.emailAddress,
              validator: (value) {
                if (value == null || value.isEmpty) {
                  return '请输入邮箱';
                }
                if (!RegExp(r'^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$').hasMatch(value)) {
                  return '请输入有效的邮箱地址';
                }
                return null;
              },
            ),
            
            SizedBox(height: 16),
            
            // 密码
            TextFormField(
              controller: _passwordController,
              decoration: InputDecoration(
                labelText: '密码',
                prefixIcon: Icon(Icons.lock),
                border: OutlineInputBorder(),
              ),
              obscureText: true,
              validator: (value) {
                if (value == null || value.isEmpty) {
                  return '请输入密码';
                }
                if (value.length < 6) {
                  return '密码至少6个字符';
                }
                return null;
              },
            ),
            
            SizedBox(height: 16),
            
            // 确认密码
            TextFormField(
              controller: _confirmPasswordController,
              decoration: InputDecoration(
                labelText: '确认密码',
                prefixIcon: Icon(Icons.lock_outline),
                border: OutlineInputBorder(),
              ),
              obscureText: true,
              validator: (value) {
                if (value == null || value.isEmpty) {
                  return '请确认密码';
                }
                if (value != _passwordController.text) {
                  return '两次密码不一致';
                }
                return null;
              },
            ),
            
            SizedBox(height: 24),
            
            // 注册按钮
            ElevatedButton(
              onPressed: () {
                if (_formKey.currentState!.validate()) {
                  print('注册成功');
                  ScaffoldMessenger.of(context).showSnackBar(
                    SnackBar(content: Text('注册成功')),
                  );
                }
              },
              child: Text('注册'),
              style: ElevatedButton.styleFrom(
                minimumSize: Size(double.infinity, 50),
              ),
            ),
          ],
        ),
      ),
    );
  }

  @override
  void dispose() {
    _emailController.dispose();
    _passwordController.dispose();
    _confirmPasswordController.dispose();
    super.dispose();
  }
}

13-1. Form 表单验证

Form + FormField

定义: Form 是表单容器,配合 TextFormField 实现表单验证功能。

完整表单示例:

class LoginForm extends StatefulWidget {
  @override
  _LoginFormState createState() => _LoginFormState();
}

class _LoginFormState extends State<LoginForm> {
  final _formKey = GlobalKey<FormState>();
  final _usernameController = TextEditingController();
  final _passwordController = TextEditingController();

  @override
  Widget build(BuildContext context) {
    return Form(
      key: _formKey,
      child: Padding(
        padding: EdgeInsets.all(16),
        child: Column(
          children: [
            // 用户名输入
            TextFormField(
              controller: _usernameController,
              decoration: InputDecoration(
                labelText: '用户名',
                hintText: '请输入用户名',
                prefixIcon: Icon(Icons.person),
              ),
              validator: (value) {
                if (value == null || value.isEmpty) {
                  return '请输入用户名';
                }
                if (value.length < 3) {
                  return '用户名至少3个字符';
                }
                return null;  // 验证通过
              },
            ),
            SizedBox(height: 16),
            
            // 密码输入
            TextFormField(
              controller: _passwordController,
              obscureText: true,
              decoration: InputDecoration(
                labelText: '密码',
                hintText: '请输入密码',
                prefixIcon: Icon(Icons.lock),
              ),
              validator: (value) {
                if (value == null || value.isEmpty) {
                  return '请输入密码';
                }
                if (value.length < 6) {
                  return '密码至少6个字符';
                }
                return null;
              },
            ),
            SizedBox(height: 24),
            
            // 登录按钮
            ElevatedButton(
              onPressed: () {
                // 验证所有字段
                if (_formKey.currentState!.validate()) {
                  // 验证通过,执行登录
                  print('用户名:${_usernameController.text}');
                  print('密码:${_passwordController.text}');
                  ScaffoldMessenger.of(context).showSnackBar(
                    SnackBar(content: Text('登录成功')),
                  );
                }
              },
              child: Text('登录'),
              style: ElevatedButton.styleFrom(
                minimumSize: Size(double.infinity, 50),
              ),
            ),
          ],
        ),
      ),
    );
  }

  @override
  void dispose() {
    _usernameController.dispose();
    _passwordController.dispose();
    super.dispose();
  }
}

自定义验证规则

常用验证规则:

class Validators {
  // 必填验证
  static String? required(String? value, {String? message}) {
    if (value == null || value.isEmpty) {
      return message ?? '此字段为必填项';
    }
    return null;
  }

  // 邮箱验证
  static String? email(String? value) {
    if (value == null || value.isEmpty) return '请输入邮箱';
    
    final emailRegex = RegExp(r'^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$');
    if (!emailRegex.hasMatch(value)) {
      return '请输入有效的邮箱地址';
    }
    return null;
  }

  // 手机号验证
  static String? phone(String? value) {
    if (value == null || value.isEmpty) return '请输入手机号';
    
    final phoneRegex = RegExp(r'^1[3-9]\d{9}$');
    if (!phoneRegex.hasMatch(value)) {
      return '请输入有效的手机号';
    }
    return null;
  }

  // 长度验证
  static String? minLength(String? value, int min) {
    if (value == null || value.isEmpty) return '此字段为必填项';
    
    if (value.length < min) {
      return '长度不能少于 $min 个字符';
    }
    return null;
  }

  // 密码强度验证
  static String? password(String? value) {
    if (value == null || value.isEmpty) return '请输入密码';
    
    if (value.length < 8) {
      return '密码长度不能少于8个字符';
    }
    
    if (!RegExp(r'[A-Z]').hasMatch(value)) {
      return '密码必须包含大写字母';
    }
    
    if (!RegExp(r'[a-z]').hasMatch(value)) {
      return '密码必须包含小写字母';
    }
    
    if (!RegExp(r'[0-9]').hasMatch(value)) {
      return '密码必须包含数字';
    }
    
    return null;
  }

  // 组合验证
  static String? combine(String? value, List<String? Function(String?)> validators) {
    for (var validator in validators) {
      final result = validator(value);
      if (result != null) return result;
    }
    return null;
  }
}

// 使用示例
TextFormField(
  validator: (value) => Validators.email(value),
)

// 组合验证
TextFormField(
  validator: (value) => Validators.combine(value, [
    Validators.required,
    (v) => Validators.minLength(v, 6),
  ]),
)

Form 进阶用法

保存表单数据:

class UserForm extends StatefulWidget {
  @override
  _UserFormState createState() => _UserFormState();
}

class _UserFormState extends State<UserForm> {
  final _formKey = GlobalKey<FormState>();
  String? _name;
  String? _email;
  int? _age;

  @override
  Widget build(BuildContext context) {
    return Form(
      key: _formKey,
      child: Column(
        children: [
          TextFormField(
            decoration: InputDecoration(labelText: '姓名'),
            validator: (value) => value?.isEmpty ?? true ? '请输入姓名' : null,
            onSaved: (value) => _name = value,
          ),
          TextFormField(
            decoration: InputDecoration(labelText: '邮箱'),
            validator: Validators.email,
            onSaved: (value) => _email = value,
          ),
          TextFormField(
            decoration: InputDecoration(labelText: '年龄'),
            keyboardType: TextInputType.number,
            validator: (value) {
              if (value == null || value.isEmpty) return '请输入年龄';
              final age = int.tryParse(value);
              if (age == null || age < 0 || age > 120) {
                return '请输入有效的年龄';
              }
              return null;
            },
            onSaved: (value) => _age = int.tryParse(value ?? '0'),
          ),
          ElevatedButton(
            onPressed: () {
              if (_formKey.currentState!.validate()) {
                // 保存所有字段的值
                _formKey.currentState!.save();
                
                print('姓名:$_name');
                print('邮箱:$_email');
                print('年龄:$_age');
              }
            },
            child: Text('提交'),
          ),
        ],
      ),
    );
  }
}

重置表单:

ElevatedButton(
  onPressed: () {
    _formKey.currentState!.reset();  // 重置所有字段
  },
  child: Text('重置'),
)

13-2. 焦点管理 (FocusNode)

FocusNode 基础

定义: 管理输入框的焦点状态,控制键盘显示/隐藏。

基础用法:

class FocusDemo extends StatefulWidget {
  @override
  _FocusDemoState createState() => _FocusDemoState();
}

class _FocusDemoState extends State<FocusDemo> {
  final _usernameFocus = FocusNode();
  final _passwordFocus = FocusNode();

  @override
  void dispose() {
    _usernameFocus.dispose();
    _passwordFocus.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        TextField(
          focusNode: _usernameFocus,
          decoration: InputDecoration(labelText: '用户名'),
          textInputAction: TextInputAction.next,
          onSubmitted: (_) {
            // 回车后跳转到下一个输入框
            _passwordFocus.requestFocus();
          },
        ),
        TextField(
          focusNode: _passwordFocus,
          decoration: InputDecoration(labelText: '密码'),
          obscureText: true,
          textInputAction: TextInputAction.done,
        ),
        SizedBox(height: 20),
        Row(
          mainAxisAlignment: MainAxisAlignment.spaceAround,
          children: [
            ElevatedButton(
              onPressed: () {
                _usernameFocus.requestFocus();  // 请求焦点
              },
              child: Text('聚焦用户名'),
            ),
            ElevatedButton(
              onPressed: () {
                _usernameFocus.unfocus();  // 失去焦点
                _passwordFocus.unfocus();
              },
              child: Text('失去焦点'),
            ),
          ],
        ),
      ],
    );
  }
}

焦点监听

监听焦点变化:

class FocusListenerDemo extends StatefulWidget {
  @override
  _FocusListenerDemoState createState() => _FocusListenerDemoState();
}

class _FocusListenerDemoState extends State<FocusListenerDemo> {
  final _focusNode = FocusNode();
  bool _hasFocus = false;

  @override
  void initState() {
    super.initState();
    
    // 添加焦点监听
    _focusNode.addListener(() {
      setState(() {
        _hasFocus = _focusNode.hasFocus;
      });
      
      if (_focusNode.hasFocus) {
        print('获得焦点');
      } else {
        print('失去焦点');
      }
    });
  }

  @override
  void dispose() {
    _focusNode.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        TextField(
          focusNode: _focusNode,
          decoration: InputDecoration(
            labelText: '输入框',
            border: OutlineInputBorder(
              borderSide: BorderSide(
                color: _hasFocus ? Colors.blue : Colors.grey,
                width: _hasFocus ? 2 : 1,
              ),
            ),
          ),
        ),
        SizedBox(height: 20),
        Text(
          _hasFocus ? '输入框已聚焦' : '输入框未聚焦',
          style: TextStyle(
            color: _hasFocus ? Colors.blue : Colors.grey,
            fontSize: 16,
          ),
        ),
      ],
    );
  }
}

隐藏键盘

多种方式隐藏键盘:

// 方法1:让所有输入框失去焦点
FocusScope.of(context).unfocus();

// 方法2:请求一个空焦点
FocusScope.of(context).requestFocus(FocusNode());

// 方法3:使用 GestureDetector
GestureDetector(
  onTap: () {
    FocusScope.of(context).unfocus();  // 点击空白处隐藏键盘
  },
  child: Scaffold(
    body: Column(
      children: [
        TextField(decoration: InputDecoration(labelText: '输入框')),
      ],
    ),
  ),
)

自动聚焦

页面打开时自动聚焦:

class AutoFocusDemo extends StatefulWidget {
  @override
  _AutoFocusDemoState createState() => _AutoFocusDemoState();
}

class _AutoFocusDemoState extends State<AutoFocusDemo> {
  final _focusNode = FocusNode();

  @override
  void initState() {
    super.initState();
    // 延迟请求焦点,确保页面已构建完成
    WidgetsBinding.instance.addPostFrameCallback((_) {
      _focusNode.requestFocus();
    });
  }

  @override
  void dispose() {
    _focusNode.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return TextField(
      focusNode: _focusNode,
      decoration: InputDecoration(labelText: '自动聚焦的输入框'),
    );
  }
}

// 或者使用 autofocus 属性
TextField(
  autofocus: true,  // 更简单的方式
  decoration: InputDecoration(labelText: '自动聚焦'),
)

焦点顺序控制

完整的表单焦点流转:

class FormFocusDemo extends StatefulWidget {
  @override
  _FormFocusDemoState createState() => _FormFocusDemoState();
}

class _FormFocusDemoState extends State<FormFocusDemo> {
  final _nameFocus = FocusNode();
  final _phoneFocus = FocusNode();
  final _emailFocus = FocusNode();
  final _addressFocus = FocusNode();

  @override
  void dispose() {
    _nameFocus.dispose();
    _phoneFocus.dispose();
    _emailFocus.dispose();
    _addressFocus.dispose();
    super.dispose();
  }

  void _nextFocus(FocusNode currentFocus, FocusNode nextFocus) {
    currentFocus.unfocus();
    FocusScope.of(context).requestFocus(nextFocus);
  }

  @override
  Widget build(BuildContext context) {
    return GestureDetector(
      onTap: () => FocusScope.of(context).unfocus(),
      child: Scaffold(
        appBar: AppBar(title: Text('焦点顺序控制')),
        body: Padding(
          padding: EdgeInsets.all(16),
          child: Column(
            children: [
              TextField(
                focusNode: _nameFocus,
                decoration: InputDecoration(labelText: '姓名'),
                textInputAction: TextInputAction.next,
                onSubmitted: (_) => _nextFocus(_nameFocus, _phoneFocus),
              ),
              TextField(
                focusNode: _phoneFocus,
                decoration: InputDecoration(labelText: '手机号'),
                keyboardType: TextInputType.phone,
                textInputAction: TextInputAction.next,
                onSubmitted: (_) => _nextFocus(_phoneFocus, _emailFocus),
              ),
              TextField(
                focusNode: _emailFocus,
                decoration: InputDecoration(labelText: '邮箱'),
                keyboardType: TextInputType.emailAddress,
                textInputAction: TextInputAction.next,
                onSubmitted: (_) => _nextFocus(_emailFocus, _addressFocus),
              ),
              TextField(
                focusNode: _addressFocus,
                decoration: InputDecoration(labelText: '地址'),
                textInputAction: TextInputAction.done,
                onSubmitted: (_) {
                  _addressFocus.unfocus();
                  print('提交表单');
                },
              ),
            ],
          ),
        ),
      ),
    );
  }
}

14. 按钮组件

ElevatedButton(凸起按钮)

定义: Material Design 风格的凸起按钮,有阴影效果。

基础用法:

ElevatedButton(
  onPressed: () {
    print('按钮被点击');
  },
  child: Text('点击我'),
)

// 禁用状态
ElevatedButton(
  onPressed: null,  // 设置为 null 禁用按钮
  child: Text('禁用按钮'),
)

自定义样式:

ElevatedButton(
  onPressed: () {},
  style: ElevatedButton.styleFrom(
    backgroundColor: Colors.blue,  // 背景色
    foregroundColor: Colors.white,  // 文字颜色
    padding: EdgeInsets.symmetric(horizontal: 30, vertical: 15),
    shape: RoundedRectangleBorder(
      borderRadius: BorderRadius.circular(20),
    ),
    elevation: 5,  // 阴影高度
  ),
  child: Text('自定义按钮'),
)

带图标的按钮:

ElevatedButton.icon(
  onPressed: () {},
  icon: Icon(Icons.send),
  label: Text('发送'),
)

TextButton(文本按钮)

定义: 扁平的文本按钮,没有背景和阴影。

基础用法:

TextButton(
  onPressed: () {},
  child: Text('文本按钮'),
)

// 自定义样式
TextButton(
  onPressed: () {},
  style: TextButton.styleFrom(
    foregroundColor: Colors.blue,
    padding: EdgeInsets.all(16),
  ),
  child: Text('自定义文本按钮'),
)

OutlinedButton(轮廓按钮)

定义: 带边框的按钮,没有背景色。

基础用法:

OutlinedButton(
  onPressed: () {},
  child: Text('轮廓按钮'),
)

// 自定义样式
OutlinedButton(
  onPressed: () {},
  style: OutlinedButton.styleFrom(
    foregroundColor: Colors.blue,
    side: BorderSide(color: Colors.blue, width: 2),
    shape: RoundedRectangleBorder(
      borderRadius: BorderRadius.circular(10),
    ),
  ),
  child: Text('自定义轮廓按钮'),
)

IconButton(图标按钮)

定义: 只包含图标的圆形按钮。

基础用法:

IconButton(
  icon: Icon(Icons.favorite),
  onPressed: () {},
  color: Colors.red,
  iconSize: 30,
  tooltip: '收藏',  // 长按显示的提示
)

FloatingActionButton(悬浮按钮)

定义: 悬浮在页面上的圆形按钮,通常用于主要操作。

基础用法:

FloatingActionButton(
  onPressed: () {},
  child: Icon(Icons.add),
  tooltip: '添加',
)

// 扩展样式
FloatingActionButton.extended(
  onPressed: () {},
  icon: Icon(Icons.add),
  label: Text('添加项目'),
)

15. Scaffold 脚手架

Scaffold(页面脚手架)

定义: Material Design 的基本页面布局结构,提供 AppBar、Drawer、BottomNavigationBar 等。

完整示例:

class MyHomePage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      // 顶部导航栏
      appBar: AppBar(
        title: Text('首页'),
        actions: [
          IconButton(icon: Icon(Icons.search), onPressed: () {}),
          IconButton(icon: Icon(Icons.more_vert), onPressed: () {}),
        ],
      ),
      
      // 主体内容
      body: Center(
        child: Text('页面内容'),
      ),
      
      // 悬浮按钮
      floatingActionButton: FloatingActionButton(
        onPressed: () {},
        child: Icon(Icons.add),
      ),
      
      // 悬浮按钮位置
      floatingActionButtonLocation: FloatingActionButtonLocation.centerFloat,
      
      // 左侧抽屉菜单
      drawer: Drawer(
        child: ListView(
          children: [
            DrawerHeader(
              decoration: BoxDecoration(color: Colors.blue),
              child: Text('菜单', style: TextStyle(color: Colors.white, fontSize: 24)),
            ),
            ListTile(
              leading: Icon(Icons.home),
              title: Text('首页'),
              onTap: () {},
            ),
            ListTile(
              leading: Icon(Icons.settings),
              title: Text('设置'),
              onTap: () {},
            ),
          ],
        ),
      ),
      
      // 底部导航栏
      bottomNavigationBar: BottomNavigationBar(
        items: [
          BottomNavigationBarItem(icon: Icon(Icons.home), label: '首页'),
          BottomNavigationBarItem(icon: Icon(Icons.business), label: '商城'),
          BottomNavigationBarItem(icon: Icon(Icons.person), label: '我的'),
        ],
        currentIndex: 0,
        onTap: (index) {
          print('点击了第 $index 项');
        },
      ),
    );
  }
}

BottomNavigationBar 完整应用示例

定义: 实现底部导航栏的多页面切换,配合 IndexedStack 保持页面状态。

完整示例:

class MainPage extends StatefulWidget {
  @override
  _MainPageState createState() => _MainPageState();
}

class _MainPageState extends State<MainPage> {
  int _currentIndex = 0;
  
  // 页面列表
  final List<Widget> _pages = [
    HomePage(),
    ShopPage(),
    ProfilePage(),
  ];

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: IndexedStack(
        index: _currentIndex,  // 使用 IndexedStack 保持页面状态
        children: _pages,
      ),
      bottomNavigationBar: BottomNavigationBar(
        currentIndex: _currentIndex,
        onTap: (index) {
          setState(() {
            _currentIndex = index;
          });
        },
        items: [
          BottomNavigationBarItem(
            icon: Icon(Icons.home),
            activeIcon: Icon(Icons.home, color: Colors.blue),
            label: '首页',
          ),
          BottomNavigationBarItem(
            icon: Icon(Icons.shopping_cart),
            activeIcon: Icon(Icons.shopping_cart, color: Colors.blue),
            label: '商城',
          ),
          BottomNavigationBarItem(
            icon: Icon(Icons.person),
            activeIcon: Icon(Icons.person, color: Colors.blue),
            label: '我的',
          ),
        ],
        selectedItemColor: Colors.blue,
        unselectedItemColor: Colors.grey,
        type: BottomNavigationBarType.fixed,  // 固定类型,超过3个item时推荐
      ),
    );
  }
}

// 首页
class HomePage extends StatefulWidget {
  @override
  _HomePageState createState() => _HomePageState();
}

class _HomePageState extends State<HomePage> with AutomaticKeepAliveClientMixin {
  int _counter = 0;

  @override
  bool get wantKeepAlive => true;  // 保持状态

  @override
  Widget build(BuildContext context) {
    super.build(context);  // 必须调用
    
    return Scaffold(
      appBar: AppBar(title: Text('首页')),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            Text('首页计数:$_counter', style: TextStyle(fontSize: 24)),
            ElevatedButton(
              onPressed: () {
                setState(() {
                  _counter++;
                });
              },
              child: Text('增加'),
            ),
          ],
        ),
      ),
    );
  }
}

// 商城页面
class ShopPage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('商城')),
      body: Center(child: Text('商城页面')),
    );
  }
}

// 我的页面
class ProfilePage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('我的')),
      body: Center(child: Text('个人中心')),
    );
  }
}

IndexedStack vs PageView:

// IndexedStack:所有页面都会保持状态,切换时不重建
IndexedStack(
  index: _currentIndex,
  children: _pages,
)

// PageView:可以滑动切换,但默认不保持状态
PageView(
  controller: _pageController,
  children: _pages,
  onPageChanged: (index) {
    setState(() {
      _currentIndex = index;
    });
  },
)

SafeArea(安全区域)

定义: 自动适配刘海屏、状态栏、导航条等系统UI,确保内容不被遮挡。

基础用法:

Scaffold(
  body: SafeArea(
    child: Column(
      children: [
        Text('这段内容不会被刘海屏遮挡'),
        // ... 其他内容
      ],
    ),
  ),
)

指定安全区域位置:

SafeArea(
  top: true,     // 顶部安全区域
  bottom: true,  // 底部安全区域
  left: true,    // 左侧安全区域
  right: true,   // 右侧安全区域
  child: Container(
    color: Colors.blue,
    child: Center(child: Text('内容')),
  ),
)

部分区域不使用安全区域:

SafeArea(
  top: false,    // 顶部不留空白,例如全屏图片
  bottom: true,
  child: Column(
    children: [
      Container(
        height: 200,
        color: Colors.blue,
        child: Center(child: Text('全屏顶部图片')),
      ),
      Expanded(
        child: Center(child: Text('其他内容')),
      ),
    ],
  ),
)

SafeArea + Scaffold 组合:

// 推荐做法:在 Scaffold 内使用 SafeArea
Scaffold(
  appBar: AppBar(title: Text('标题')),
  body: SafeArea(
    child: Column(
      children: [
        // 内容不会被系统UI遮挡
      ],
    ),
  ),
)

AppBar(顶部导航栏)

详细用法:

AppBar(
  // 标题
  title: Text('标题'),
  
  // 左侧按钮(默认是返回按钮)
  leading: IconButton(
    icon: Icon(Icons.menu),
    onPressed: () {},
  ),
  
  // 右侧按钮组
  actions: [
    IconButton(icon: Icon(Icons.search), onPressed: () {}),
    IconButton(icon: Icon(Icons.shopping_cart), onPressed: () {}),
    PopupMenuButton(
      itemBuilder: (context) => [
        PopupMenuItem(child: Text('设置'), value: '设置'),
        PopupMenuItem(child: Text('关于'), value: '关于'),
      ],
      onSelected: (value) {
        print('选择了:$value');
      },
    ),
  ],
  
  // 背景色
  backgroundColor: Colors.blue,
  
  // 阴影高度
  elevation: 4,
  
  // 底部 Widget
  bottom: TabBar(
    tabs: [
      Tab(text: '推荐'),
      Tab(text: '热门'),
      Tab(text: '最新'),
    ],
  ),
)

16. 对话框与提示

AlertDialog(警告对话框)

基础用法:

void _showDialog(BuildContext context) {
  showDialog(
    context: context,
    builder: (context) {
      return AlertDialog(
        title: Text('提示'),
        content: Text('确定要删除吗?'),
        actions: [
          TextButton(
            onPressed: () {
              Navigator.pop(context);  // 关闭对话框
            },
            child: Text('取消'),
          ),
          TextButton(
            onPressed: () {
              Navigator.pop(context);
              print('确认删除');
            },
            child: Text('确定'),
          ),
        ],
      );
    },
  );
}

// 使用
ElevatedButton(
  onPressed: () => _showDialog(context),
  child: Text('显示对话框'),
)

自定义对话框:

showDialog(
  context: context,
  builder: (context) {
    return AlertDialog(
      title: Row(
        children: [
          Icon(Icons.warning, color: Colors.orange),
          SizedBox(width: 10),
          Text('警告'),
        ],
      ),
      content: Column(
        mainAxisSize: MainAxisSize.min,
        children: [
          Text('这是一个自定义对话框'),
          SizedBox(height: 10),
          TextField(
            decoration: InputDecoration(
              hintText: '请输入内容',
              border: OutlineInputBorder(),
            ),
          ),
        ],
      ),
      actions: [
        TextButton(
          onPressed: () => Navigator.pop(context),
          child: Text('取消'),
        ),
        ElevatedButton(
          onPressed: () => Navigator.pop(context),
          child: Text('确定'),
        ),
      ],
    );
  },
);

SnackBar(底部提示条)

基础用法:

void _showSnackBar(BuildContext context) {
  ScaffoldMessenger.of(context).showSnackBar(
    SnackBar(
      content: Text('操作成功'),
      duration: Duration(seconds: 2),
    ),
  );
}

// 使用
ElevatedButton(
  onPressed: () => _showSnackBar(context),
  child: Text('显示提示'),
)

带操作的 SnackBar:

ScaffoldMessenger.of(context).showSnackBar(
  SnackBar(
    content: Text('已删除'),
    action: SnackBarAction(
      label: '撤销',
      onPressed: () {
        print('撤销删除');
      },
    ),
    duration: Duration(seconds: 3),
    backgroundColor: Colors.red,
  ),
);

BottomSheet(底部弹窗)

模态底部弹窗:

void _showBottomSheet(BuildContext context) {
  showModalBottomSheet(
    context: context,
    builder: (context) {
      return Container(
        height: 200,
        child: Column(
          children: [
            ListTile(
              leading: Icon(Icons.photo),
              title: Text('拍照'),
              onTap: () {
                Navigator.pop(context);
                print('选择了拍照');
              },
            ),
            ListTile(
              leading: Icon(Icons.image),
              title: Text('从相册选择'),
              onTap: () {
                Navigator.pop(context);
                print('选择了相册');
              },
            ),
            ListTile(
              leading: Icon(Icons.cancel),
              title: Text('取消'),
              onTap: () => Navigator.pop(context),
            ),
          ],
        ),
      );
    },
  );
}

DatePicker(日期选择器)

基础用法:

Future<void> _selectDate(BuildContext context) async {
  final DateTime? picked = await showDatePicker(
    context: context,
    initialDate: DateTime.now(),
    firstDate: DateTime(2020),
    lastDate: DateTime(2030),
  );
  
  if (picked != null) {
    print('选择的日期:$picked');
  }
}

TimePicker(时间选择器)

基础用法:

Future<void> _selectTime(BuildContext context) async {
  final TimeOfDay? picked = await showTimePicker(
    context: context,
    initialTime: TimeOfDay.now(),
  );
  
  if (picked != null) {
    print('选择的时间:${picked.hour}:${picked.minute}');
  }
}

17. 交互组件

Switch(开关)

基础用法:

class SwitchDemo extends StatefulWidget {
  @override
  _SwitchDemoState createState() => _SwitchDemoState();
}

class _SwitchDemoState extends State<SwitchDemo> {
  bool _switchValue = false;

  @override
  Widget build(BuildContext context) {
    return Switch(
      value: _switchValue,
      onChanged: (value) {
        setState(() {
          _switchValue = value;
        });
      },
      activeColor: Colors.blue,
    );
  }
}

Checkbox(复选框)

基础用法:

class CheckboxDemo extends StatefulWidget {
  @override
  _CheckboxDemoState createState() => _CheckboxDemoState();
}

class _CheckboxDemoState extends State<CheckboxDemo> {
  bool _checked = false;

  @override
  Widget build(BuildContext context) {
    return CheckboxListTile(
      title: Text('同意用户协议'),
      value: _checked,
      onChanged: (value) {
        setState(() {
          _checked = value!;
        });
      },
      controlAffinity: ListTileControlAffinity.leading,  // 复选框在左侧
    );
  }
}

Radio(单选框)

基础用法:

class RadioDemo extends StatefulWidget {
  @override
  _RadioDemoState createState() => _RadioDemoState();
}

class _RadioDemoState extends State<RadioDemo> {
  String _selectedValue = '选项1';

  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        RadioListTile<String>(
          title: Text('选项1'),
          value: '选项1',
          groupValue: _selectedValue,
          onChanged: (value) {
            setState(() {
              _selectedValue = value!;
            });
          },
        ),
        RadioListTile<String>(
          title: Text('选项2'),
          value: '选项2',
          groupValue: _selectedValue,
          onChanged: (value) {
            setState(() {
              _selectedValue = value!;
            });
          },
        ),
      ],
    );
  }
}

Slider(滑块)

基础用法:

class SliderDemo extends StatefulWidget {
  @override
  _SliderDemoState createState() => _SliderDemoState();
}

class _SliderDemoState extends State<SliderDemo> {
  double _sliderValue = 50;

  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        Text('当前值:${_sliderValue.toInt()}'),
        Slider(
          value: _sliderValue,
          min: 0,
          max: 100,
          divisions: 10,  // 分段数
          label: _sliderValue.toInt().toString(),
          onChanged: (value) {
            setState(() {
              _sliderValue = value;
            });
          },
        ),
      ],
    );
  }
}

18. 手势检测

GestureDetector(手势检测器)

定义: 检测各种手势操作,如点击、双击、长按、拖动等。

基础用法:

GestureDetector(
  onTap: () {
    print('单击');
  },
  onDoubleTap: () {
    print('双击');
  },
  onLongPress: () {
    print('长按');
  },
  child: Container(
    width: 200,
    height: 200,
    color: Colors.blue,
    child: Center(child: Text('点击我')),
  ),
)

拖动示例:

class DraggableDemo extends StatefulWidget {
  @override
  _DraggableDemoState createState() => _DraggableDemoState();
}

class _DraggableDemoState extends State<DraggableDemo> {
  double _top = 100;
  double _left = 100;

  @override
  Widget build(BuildContext context) {
    return Stack(
      children: [
        Positioned(
          top: _top,
          left: _left,
          child: GestureDetector(
            onPanUpdate: (details) {
              setState(() {
                _left += details.delta.dx;
                _top += details.delta.dy;
              });
            },
            child: Container(
              width: 100,
              height: 100,
              color: Colors.blue,
              child: Center(child: Text('拖动我')),
            ),
          ),
        ),
      ],
    );
  }
}

InkWell(水波纹点击)

定义: 带水波纹效果的点击组件。

基础用法:

InkWell(
  onTap: () {
    print('点击');
  },
  child: Container(
    padding: EdgeInsets.all(16),
    child: Text('点击我会有水波纹效果'),
  ),
)

19. 异步处理

Future 与 async/await

基础用法:

// 异步函数
Future<String> fetchData() async {
  await Future.delayed(Duration(seconds: 2));  // 模拟网络延迟
  return '获取的数据';
}

// 使用
void loadData() async {
  print('开始加载');
  String data = await fetchData();
  print('数据:$data');
}

FutureBuilder(异步构建器)

定义: 根据 Future 的状态自动构建 UI。

基础用法:

class FutureBuilderDemo extends StatelessWidget {
  Future<String> fetchData() async {
    await Future.delayed(Duration(seconds: 2));
    return '加载完成的数据';
  }

  @override
  Widget build(BuildContext context) {
    return FutureBuilder<String>(
      future: fetchData(),
      builder: (context, snapshot) {
        // 加载中
        if (snapshot.connectionState == ConnectionState.waiting) {
          return Center(child: CircularProgressIndicator());
        }
        
        // 加载失败
        if (snapshot.hasError) {
          return Center(child: Text('错误:${snapshot.error}'));
        }
        
        // 加载成功
        if (snapshot.hasData) {
          return Center(child: Text('数据:${snapshot.data}'));
        }
        
        return Center(child: Text('无数据'));
      },
    );
  }
}

StreamBuilder(流构建器)

定义: 根据 Stream 的数据流自动构建 UI。

基础用法:

class StreamBuilderDemo extends StatelessWidget {
  Stream<int> counterStream() async* {
    for (int i = 0; i < 10; i++) {
      await Future.delayed(Duration(seconds: 1));
      yield i;
    }
  }

  @override
  Widget build(BuildContext context) {
    return StreamBuilder<int>(
      stream: counterStream(),
      builder: (context, snapshot) {
        if (snapshot.hasData) {
          return Text('计数:${snapshot.data}', style: TextStyle(fontSize: 48));
        }
        return CircularProgressIndicator();
      },
    );
  }
}

20. TabBar 导航

TabBar & TabBarView

定义: 实现顶部或底部标签页导航。

完整示例:

class TabBarDemo extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return DefaultTabController(
      length: 3,  // 标签数量
      child: Scaffold(
        appBar: AppBar(
          title: Text('TabBar 示例'),
          bottom: TabBar(
            tabs: [
              Tab(icon: Icon(Icons.home), text: '首页'),
              Tab(icon: Icon(Icons.business), text: '商城'),
              Tab(icon: Icon(Icons.person), text: '我的'),
            ],
          ),
        ),
        body: TabBarView(
          children: [
            Center(child: Text('首页内容', style: TextStyle(fontSize: 24))),
            Center(child: Text('商城内容', style: TextStyle(fontSize: 24))),
            Center(child: Text('我的内容', style: TextStyle(fontSize: 24))),
          ],
        ),
      ),
    );
  }
}

使用 TabController:

class TabBarControllerDemo extends StatefulWidget {
  @override
  _TabBarControllerDemoState createState() => _TabBarControllerDemoState();
}

class _TabBarControllerDemoState extends State<TabBarControllerDemo>
    with SingleTickerProviderStateMixin {
  late TabController _tabController;

  @override
  void initState() {
    super.initState();
    _tabController = TabController(length: 3, vsync: this);
    
    // 监听标签切换
    _tabController.addListener(() {
      if (!_tabController.indexIsChanging) {
        print('切换到标签 ${_tabController.index}');
      }
    });
  }

  @override
  void dispose() {
    _tabController.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('TabController 示例'),
        bottom: TabBar(
          controller: _tabController,
          tabs: [
            Tab(text: '推荐'),
            Tab(text: '热门'),
            Tab(text: '最新'),
          ],
        ),
      ),
      body: TabBarView(
        controller: _tabController,
        children: [
          ListView.builder(
            itemCount: 20,
            itemBuilder: (context, index) => ListTile(title: Text('推荐 $index')),
          ),
          ListView.builder(
            itemCount: 20,
            itemBuilder: (context, index) => ListTile(title: Text('热门 $index')),
          ),
          ListView.builder(
            itemCount: 20,
            itemBuilder: (context, index) => ListTile(title: Text('最新 $index')),
          ),
        ],
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: () {
          // 程序控制切换标签
          _tabController.animateTo((_tabController.index + 1) % 3);
        },
        child: Icon(Icons.navigate_next),
      ),
    );
  }
}

21. 卡片与列表

Card(卡片组件)

定义: Material Design 风格的卡片,带圆角和阴影。

基础用法:

Card(
  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(
  elevation: 8,  // 阴影高度
  shape: RoundedRectangleBorder(
    borderRadius: BorderRadius.circular(12),
  ),
  margin: EdgeInsets.all(16),
  child: Column(
    children: [
      ClipRRect(
        borderRadius: BorderRadius.vertical(top: Radius.circular(12)),
        child: Image.network(
          'https://picsum.photos/400/200',
          height: 200,
          width: double.infinity,
          fit: BoxFit.cover,
        ),
      ),
      Padding(
        padding: EdgeInsets.all(16),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            Text('卡片标题', style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold)),
            SizedBox(height: 8),
            Text('这是一张带图片的卡片示例'),
            SizedBox(height: 16),
            Row(
              mainAxisAlignment: MainAxisAlignment.end,
              children: [
                TextButton(onPressed: () {}, child: Text('取消')),
                SizedBox(width: 8),
                ElevatedButton(onPressed: () {}, child: Text('确定')),
              ],
            ),
          ],
        ),
      ),
    ],
  ),
)

列表中的卡片:

ListView.builder(
  padding: EdgeInsets.all(8),
  itemCount: 10,
  itemBuilder: (context, index) {
    return Card(
      margin: EdgeInsets.symmetric(vertical: 8, horizontal: 4),
      child: ListTile(
        leading: CircleAvatar(
          child: Text('${index + 1}'),
        ),
        title: Text('项目 ${index + 1}'),
        subtitle: Text('这是项目的描述信息'),
        trailing: Icon(Icons.arrow_forward_ios),
        onTap: () {
          print('点击了项目 $index');
        },
      ),
    );
  },
)

ExpansionTile(可展开列表)

定义: 可以展开和收起的列表项。

基础用法:

ExpansionTile(
  leading: Icon(Icons.folder),
  title: Text('可展开项'),
  subtitle: Text('点击展开查看更多'),
  children: [
    ListTile(
      leading: Icon(Icons.insert_drive_file),
      title: Text('子项 1'),
      onTap: () {},
    ),
    ListTile(
      leading: Icon(Icons.insert_drive_file),
      title: Text('子项 2'),
      onTap: () {},
    ),
    ListTile(
      leading: Icon(Icons.insert_drive_file),
      title: Text('子项 3'),
      onTap: () {},
    ),
  ],
)

实际应用 - 分类菜单:

class CategoryMenu extends StatelessWidget {
  final List<Map<String, dynamic>> categories = [
    {
      'title': '电子产品',
      'icon': Icons.devices,
      'items': ['手机', '电脑', '平板']
    },
    {
      'title': '服装鞋帽',
      'icon': Icons.shopping_bag,
      'items': ['男装', '女装', '童装']
    },
    {
      'title': '图书音像',
      'icon': Icons.book,
      'items': ['文学', '科技', '教育']
    },
  ];

  @override
  Widget build(BuildContext context) {
    return ListView.builder(
      itemCount: categories.length,
      itemBuilder: (context, index) {
        final category = categories[index];
        return Card(
          child: ExpansionTile(
            leading: Icon(category['icon']),
            title: Text(category['title']),
            children: (category['items'] as List<String>).map((item) {
              return ListTile(
                contentPadding: EdgeInsets.only(left: 72, right: 16),
                title: Text(item),
                onTap: () {
                  print('选择了:${category['title']} - $item');
                },
              );
            }).toList(),
          ),
        );
      },
    );
  }
}

22. 选择器组件

DropdownButton(下拉选择框)

定义: 下拉选择菜单。

基础用法:

class DropdownDemo extends StatefulWidget {
  @override
  _DropdownDemoState createState() => _DropdownDemoState();
}

class _DropdownDemoState extends State<DropdownDemo> {
  String _selectedValue = '选项1';

  @override
  Widget build(BuildContext context) {
    return DropdownButton<String>(
      value: _selectedValue,
      items: ['选项1', '选项2', '选项3', '选项4'].map((String value) {
        return DropdownMenuItem<String>(
          value: value,
          child: Text(value),
        );
      }).toList(),
      onChanged: (String? newValue) {
        setState(() {
          _selectedValue = newValue!;
        });
      },
    );
  }
}

完整示例 - 城市选择:

class CitySelector extends StatefulWidget {
  @override
  _CitySelectorState createState() => _CitySelectorState();
}

class _CitySelectorState extends State<CitySelector> {
  String _selectedCity = '北京';
  
  final List<String> _cities = [
    '北京', '上海', '广州', '深圳', '杭州', '成都', '武汉', '西安'
  ];

  @override
  Widget build(BuildContext context) {
    return Container(
      padding: EdgeInsets.symmetric(horizontal: 16),
      decoration: BoxDecoration(
        border: Border.all(color: Colors.grey),
        borderRadius: BorderRadius.circular(8),
      ),
      child: DropdownButtonHideUnderline(
        child: DropdownButton<String>(
          value: _selectedCity,
          isExpanded: true,
          icon: Icon(Icons.arrow_drop_down),
          items: _cities.map((String city) {
            return DropdownMenuItem<String>(
              value: city,
              child: Text(city),
            );
          }).toList(),
          onChanged: (String? newValue) {
            setState(() {
              _selectedCity = newValue!;
            });
            print('选择了城市:$_selectedCity');
          },
        ),
      ),
    );
  }
}

Chip 系列(标签芯片)

1. Chip(基础芯片):

Chip(
  label: Text('标签'),
  avatar: CircleAvatar(child: Text('A')),
  deleteIcon: Icon(Icons.close),
  onDeleted: () {
    print('删除标签');
  },
)

2. ChoiceChip(选择芯片):

class ChoiceChipDemo extends StatefulWidget {
  @override
  _ChoiceChipDemoState createState() => _ChoiceChipDemoState();
}

class _ChoiceChipDemoState extends State<ChoiceChipDemo> {
  int _selectedIndex = 0;
  final List<String> _options = ['全部', '待付款', '待发货', '待收货', '已完成'];

  @override
  Widget build(BuildContext context) {
    return Wrap(
      spacing: 8,
      children: List.generate(_options.length, (index) {
        return ChoiceChip(
          label: Text(_options[index]),
          selected: _selectedIndex == index,
          onSelected: (selected) {
            setState(() {
              _selectedIndex = index;
            });
          },
        );
      }),
    );
  }
}

3. FilterChip(过滤芯片):

class FilterChipDemo extends StatefulWidget {
  @override
  _FilterChipDemoState createState() => _FilterChipDemoState();
}

class _FilterChipDemoState extends State<FilterChipDemo> {
  List<String> _selectedFilters = [];
  final List<String> _filters = ['价格', '品牌', '评分', '销量', '折扣'];

  @override
  Widget build(BuildContext context) {
    return Wrap(
      spacing: 8,
      children: _filters.map((filter) {
        return FilterChip(
          label: Text(filter),
          selected: _selectedFilters.contains(filter),
          onSelected: (selected) {
            setState(() {
              if (selected) {
                _selectedFilters.add(filter);
              } else {
                _selectedFilters.remove(filter);
              }
            });
            print('已选择:$_selectedFilters');
          },
        );
      }).toList(),
    );
  }
}

23. 进度与加载

CircularProgressIndicator(圆形进度条)

基础用法:

// 不确定进度
CircularProgressIndicator()

// 确定进度
CircularProgressIndicator(
  value: 0.6,  // 0.0 到 1.0
  strokeWidth: 6,
  backgroundColor: Colors.grey[200],
  valueColor: AlwaysStoppedAnimation<Color>(Colors.blue),
)

实际应用:

class LoadingDemo extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Center(
      child: Column(
        mainAxisAlignment: MainAxisAlignment.center,
        children: [
          CircularProgressIndicator(),
          SizedBox(height: 20),
          Text('加载中...', style: TextStyle(fontSize: 16)),
        ],
      ),
    );
  }
}

LinearProgressIndicator(线性进度条)

基础用法:

// 不确定进度
LinearProgressIndicator()

// 确定进度
LinearProgressIndicator(
  value: 0.7,  // 0.0 到 1.0
  backgroundColor: Colors.grey[200],
  valueColor: AlwaysStoppedAnimation<Color>(Colors.blue),
)

下载进度示例:

class DownloadProgress extends StatefulWidget {
  @override
  _DownloadProgressState createState() => _DownloadProgressState();
}

class _DownloadProgressState extends State<DownloadProgress> {
  double _progress = 0.0;
  Timer? _timer;

  @override
  void initState() {
    super.initState();
    _startDownload();
  }

  void _startDownload() {
    _timer = Timer.periodic(Duration(milliseconds: 100), (timer) {
      setState(() {
        _progress += 0.01;
        if (_progress >= 1.0) {
          _progress = 1.0;
          timer.cancel();
        }
      });
    });
  }

  @override
  void dispose() {
    _timer?.cancel();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Column(
      mainAxisAlignment: MainAxisAlignment.center,
      children: [
        LinearProgressIndicator(value: _progress),
        SizedBox(height: 20),
        Text('${(_progress * 100).toInt()}%', style: TextStyle(fontSize: 24)),
      ],
    );
  }
}

RefreshIndicator(下拉刷新)

定义: 为可滚动组件添加下拉刷新功能。

基础用法:

class RefreshDemo extends StatefulWidget {
  @override
  _RefreshDemoState createState() => _RefreshDemoState();
}

class _RefreshDemoState extends State<RefreshDemo> {
  List<String> _items = List.generate(20, (index) => '项目 $index');

  Future<void> _onRefresh() async {
    // 模拟网络请求
    await Future.delayed(Duration(seconds: 2));
    
    setState(() {
      _items = List.generate(20, (index) => '刷新后的项目 $index');
    });
  }

  @override
  Widget build(BuildContext context) {
    return RefreshIndicator(
      onRefresh: _onRefresh,
      child: ListView.builder(
        itemCount: _items.length,
        itemBuilder: (context, index) {
          return ListTile(
            title: Text(_items[index]),
          );
        },
      ),
    );
  }
}

24. 滑动操作

Dismissible(滑动删除)

定义: 通过滑动手势删除列表项。

基础用法:

class DismissibleDemo extends StatefulWidget {
  @override
  _DismissibleDemoState createState() => _DismissibleDemoState();
}

class _DismissibleDemoState extends State<DismissibleDemo> {
  List<String> _items = List.generate(20, (index) => '项目 $index');

  @override
  Widget build(BuildContext context) {
    return ListView.builder(
      itemCount: _items.length,
      itemBuilder: (context, index) {
        final item = _items[index];
        return Dismissible(
          key: Key(item),
          background: Container(
            color: Colors.red,
            alignment: Alignment.centerRight,
            padding: EdgeInsets.only(right: 20),
            child: Icon(Icons.delete, color: Colors.white),
          ),
          direction: DismissDirection.endToStart,  // 只能从右向左滑动
          onDismissed: (direction) {
            setState(() {
              _items.removeAt(index);
            });
            ScaffoldMessenger.of(context).showSnackBar(
              SnackBar(
                content: Text('已删除 $item'),
                action: SnackBarAction(
                  label: '撤销',
                  onPressed: () {
                    setState(() {
                      _items.insert(index, item);
                    });
                  },
                ),
              ),
            );
          },
          confirmDismiss: (direction) async {
            // 确认删除对话框
            return await showDialog(
              context: context,
              builder: (context) {
                return AlertDialog(
                  title: Text('确认删除'),
                  content: Text('确定要删除 $item 吗?'),
                  actions: [
                    TextButton(
                      onPressed: () => Navigator.pop(context, false),
                      child: Text('取消'),
                    ),
                    TextButton(
                      onPressed: () => Navigator.pop(context, true),
                      child: Text('确定'),
                    ),
                  ],
                );
              },
            );
          },
          child: ListTile(
            leading: CircleAvatar(child: Text('${index + 1}')),
            title: Text(item),
            subtitle: Text('向左滑动删除'),
          ),
        );
      },
    );
  }
}

双向滑动不同操作:

Dismissible(
  key: Key(item),
  // 向右滑动的背景(归档)
  background: Container(
    color: Colors.green,
    alignment: Alignment.centerLeft,
    padding: EdgeInsets.only(left: 20),
    child: Icon(Icons.archive, color: Colors.white),
  ),
  // 向左滑动的背景(删除)
  secondaryBackground: Container(
    color: Colors.red,
    alignment: Alignment.centerRight,
    padding: EdgeInsets.only(right: 20),
    child: Icon(Icons.delete, color: Colors.white),
  ),
  onDismissed: (direction) {
    if (direction == DismissDirection.startToEnd) {
      print('归档:$item');
    } else {
      print('删除:$item');
    }
  },
  child: ListTile(title: Text(item)),
)

25. 主题与适配

ThemeData(主题配置)

定义: 配置应用的全局主题样式。

基础配置:

MaterialApp(
  theme: ThemeData(
    // 主色调
    primarySwatch: Colors.blue,
    primaryColor: Colors.blue,
    
    // 强调色
    colorScheme: ColorScheme.fromSeed(
      seedColor: Colors.blue,
      brightness: Brightness.light,
    ),
    
    // 文本主题
    textTheme: TextTheme(
      displayLarge: TextStyle(fontSize: 32, fontWeight: FontWeight.bold),
      bodyLarge: TextStyle(fontSize: 16),
      bodyMedium: TextStyle(fontSize: 14),
    ),
    
    // 按钮主题
    elevatedButtonTheme: ElevatedButtonThemeData(
      style: ElevatedButton.styleFrom(
        padding: EdgeInsets.symmetric(horizontal: 30, vertical: 15),
        shape: RoundedRectangleBorder(
          borderRadius: BorderRadius.circular(8),
        ),
      ),
    ),
    
    // AppBar 主题
    appBarTheme: AppBarTheme(
      elevation: 0,
      centerTitle: true,
      backgroundColor: Colors.blue,
      foregroundColor: Colors.white,
    ),
    
    // 卡片主题
    cardTheme: CardTheme(
      elevation: 4,
      shape: RoundedRectangleBorder(
        borderRadius: BorderRadius.circular(12),
      ),
    ),
  ),
  home: HomePage(),
)

深色模式:

class ThemeDemo extends StatefulWidget {
  @override
  _ThemeDemoState createState() => _ThemeDemoState();
}

class _ThemeDemoState extends State<ThemeDemo> {
  bool _isDarkMode = false;

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      theme: ThemeData(
        brightness: Brightness.light,
        primarySwatch: Colors.blue,
      ),
      darkTheme: ThemeData(
        brightness: Brightness.dark,
        primarySwatch: Colors.blue,
      ),
      themeMode: _isDarkMode ? ThemeMode.dark : ThemeMode.light,
      home: Scaffold(
        appBar: AppBar(title: Text('主题切换')),
        body: Center(
          child: Switch(
            value: _isDarkMode,
            onChanged: (value) {
              setState(() {
                _isDarkMode = value;
              });
            },
          ),
        ),
      ),
    );
  }
}

MediaQuery(屏幕适配)

定义: 获取设备屏幕信息,实现响应式布局。

获取屏幕信息:

@override
Widget build(BuildContext context) {
  // 获取屏幕尺寸
  final size = MediaQuery.of(context).size;
  final width = size.width;
  final height = size.height;
  
  // 获取设备像素比
  final devicePixelRatio = MediaQuery.of(context).devicePixelRatio;
  
  // 获取状态栏高度
  final padding = MediaQuery.of(context).padding;
  final statusBarHeight = padding.top;
  
  // 获取系统亮度模式
  final brightness = MediaQuery.of(context).platformBrightness;
  final isDark = brightness == Brightness.dark;
  
  return Container(
    width: width * 0.8,  // 屏幕宽度的 80%
    height: height * 0.5, // 屏幕高度的 50%
    child: Text('屏幕宽度:$width\n屏幕高度:$height'),
  );
}

响应式布局示例:

class ResponsiveLayout extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    final size = MediaQuery.of(context).size;
    final width = size.width;
    
    // 根据屏幕宽度判断布局
    if (width < 600) {
      // 手机布局
      return MobileLayout();
    } else if (width < 900) {
      // 平板布局
      return TabletLayout();
    } else {
      // 桌面布局
      return DesktopLayout();
    }
  }
}

class MobileLayout extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        Text('手机布局', style: TextStyle(fontSize: 24)),
        // 单列布局
      ],
    );
  }
}

LayoutBuilder(布局构建器):

LayoutBuilder(
  builder: (context, constraints) {
    // constraints.maxWidth 可用的最大宽度
    // constraints.maxHeight 可用的最大高度
    
    if (constraints.maxWidth > 600) {
      return Row(
        children: [
          Expanded(child: Text('左侧')),
          Expanded(child: Text('右侧')),
        ],
      );
    } else {
      return Column(
        children: [
          Text('顶部'),
          Text('底部'),
        ],
      );
    }
  },
)

26. 其他实用组件

Tooltip(提示文本)

基础用法:

Tooltip(
  message: '这是一个提示',
  child: IconButton(
    icon: Icon(Icons.info),
    onPressed: () {},
  ),
)

PopupMenuButton(弹出菜单)

详细用法:

PopupMenuButton<String>(
  icon: Icon(Icons.more_vert),
  itemBuilder: (context) => [
    PopupMenuItem(
      value: 'edit',
      child: Row(
        children: [
          Icon(Icons.edit, size: 20),
          SizedBox(width: 10),
          Text('编辑'),
        ],
      ),
    ),
    PopupMenuItem(
      value: 'share',
      child: Row(
        children: [
          Icon(Icons.share, size: 20),
          SizedBox(width: 10),
          Text('分享'),
        ],
      ),
    ),
    PopupMenuDivider(),
    PopupMenuItem(
      value: 'delete',
      child: Row(
        children: [
          Icon(Icons.delete, size: 20, color: Colors.red),
          SizedBox(width: 10),
          Text('删除', style: TextStyle(color: Colors.red)),
        ],
      ),
    ),
  ],
  onSelected: (value) {
    print('选择了:$value');
  },
)

六、滚动组件

14. 基础滚动组件

SingleChildScrollView(单子组件滚动)

定义: 当内容超出屏幕时,提供滚动功能的容器,只能包含一个子组件。

基础用法:

SingleChildScrollView(
  child: Column(
    children: [
      Container(height: 200, color: Colors.red),
      Container(height: 200, color: Colors.green),
      Container(height: 200, color: Colors.blue),
    ],
  ),
)

常用属性:

SingleChildScrollView(
  scrollDirection: Axis.vertical,  // 滚动方向
  reverse: false,                  // 是否反向滚动
  padding: EdgeInsets.all(16),     // 内边距
  physics: BouncingScrollPhysics(), // 滚动物理效果
  
  child: Column(children: [...]),
)

15. 列表组件

ListView(列表视图)

1. ListView.builder(构建器模式):

ListView.builder(
  itemCount: 100,
  itemBuilder: (context, index) {
    return ListTile(
      leading: CircleAvatar(child: Text('${index + 1}')),
      title: Text('标题 $index'),
      subtitle: Text('副标题 $index'),
      trailing: Icon(Icons.arrow_forward_ios),
      onTap: () => print('点击了第 $index 项'),
    );
  },
)

2. ListView.separated(带分隔线):

ListView.separated(
  itemCount: 20,
  itemBuilder: (context, index) {
    return ListTile(title: Text('项目 $index'));
  },
  separatorBuilder: (context, index) {
    return Divider(height: 1, color: Colors.grey[300]);
  },
)

16. 网格组件

GridView(网格视图)

1. GridView.count(固定列数):

GridView.count(
  crossAxisCount: 3,     // 每行3列
  crossAxisSpacing: 10,  // 水平间距
  mainAxisSpacing: 10,   // 垂直间距
  children: List.generate(20, (index) {
    return Container(
      color: Colors.blue,
      child: Center(child: Text('$index')),
    );
  }),
)

2. GridView.builder(构建器模式):

GridView.builder(
  gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
    crossAxisCount: 2,
    crossAxisSpacing: 10,
    mainAxisSpacing: 10,
    childAspectRatio: 1.0,  // 宽高比
  ),
  itemCount: 50,
  itemBuilder: (context, index) {
    return Card(
      child: Center(child: Text('$index')),
    );
  },
)

17. 自定义滚动容器

CustomScrollView

定义: 使用 Sliver 组件创建更灵活的滚动效果。

基础用法:

CustomScrollView(
  slivers: [
    SliverAppBar(
      expandedHeight: 200,
      pinned: true,
      flexibleSpace: FlexibleSpaceBar(
        title: Text('标题'),
        background: Image.network(url, fit: BoxFit.cover),
      ),
    ),
    SliverList(
      delegate: SliverChildBuilderDelegate(
        (context, index) => ListTile(title: Text('项 $index')),
        childCount: 20,
      ),
    ),
  ],
)

18. 整页滚动容器

PageView(页面滚动)

基础用法:

PageView(
  children: [
    Container(color: Colors.red),
    Container(color: Colors.green),
    Container(color: Colors.blue),
  ],
)

PageView.builder:

PageView.builder(
  itemCount: 10,
  itemBuilder: (context, index) {
    return Container(
      color: Colors.primaries[index % Colors.primaries.length],
      child: Center(child: Text('页面 ${index + 1}')),
    );
  },
)

七、高级特性

19. 组件通信

1. 父子组件通信

父组件向子组件传递数据(通过构造函数):

// 子组件
class ChildWidget extends StatelessWidget {
  final String title;
  final int count;
  
  const ChildWidget({
    super.key,
    required this.title,
    required this.count,
  });

  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        Text('标题:$title'),
        Text('数量:$count'),
      ],
    );
  }
}

// 父组件使用
ChildWidget(title: '商品', count: 10)

2. 子组件向父组件通信(回调函数)
// 子组件
class CounterButton extends StatelessWidget {
  final Function(int) onCountChanged;
  
  const CounterButton({super.key, required this.onCountChanged});

  @override
  Widget build(BuildContext context) {
    return ElevatedButton(
      onPressed: () => onCountChanged(1),
      child: Text('增加'),
    );
  }
}

// 父组件
class ParentWidget extends StatefulWidget {
  @override
  _ParentWidgetState createState() => _ParentWidgetState();
}

class _ParentWidgetState extends State<ParentWidget> {
  int _count = 0;

  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        Text('计数:$_count'),
        CounterButton(
          onCountChanged: (value) {
            setState(() => _count += value);
          },
        ),
      ],
    );
  }
}

3. Provider 状态管理(推荐)

安装:

flutter pub add provider

基础用法:

import 'package:provider/provider.dart';

// 1. 创建数据模型
class CounterModel extends ChangeNotifier {
  int _count = 0;
  
  int get count => _count;
  
  void increment() {
    _count++;
    notifyListeners();  // 通知更新
  }
}

// 2. 提供 Provider
void main() {
  runApp(
    ChangeNotifierProvider(
      create: (context) => CounterModel(),
      child: MyApp(),
    ),
  );
}

// 3. 使用数据
class CounterPage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            // 监听数据
            Consumer<CounterModel>(
              builder: (context, counter, child) {
                return Text('计数:${counter.count}');
              },
            ),
            ElevatedButton(
              onPressed: () {
                // 修改数据
                context.read<CounterModel>().increment();
              },
              child: Text('增加'),
            ),
          ],
        ),
      ),
    );
  }
}

八、网络与数据

20. 网络请求

Dio 工具

安装:

flutter pub add dio

基础用法:

import 'package:dio/dio.dart';

// GET 请求
Future<void> fetchData() async {
  try {
    final dio = Dio();
    final response = await dio.get('https://api.example.com/data');
    print('数据:${response.data}');
  } catch (e) {
    print('错误:$e');
  }
}

// POST 请求
Future<void> postData() async {
  final dio = Dio();
  final response = await dio.post(
    'https://api.example.com/login',
    data: {'username': 'admin', 'password': '123456'},
  );
  print(response.data);
}

封装 HTTP 工具类
class HttpUtil {
  static final HttpUtil _instance = HttpUtil._internal();
  factory HttpUtil() => _instance;
  
  late Dio dio;
  
  HttpUtil._internal() {
    dio = Dio(BaseOptions(
      baseUrl: 'https://api.example.com',
      connectTimeout: Duration(seconds: 5),
      receiveTimeout: Duration(seconds: 3),
    ));
    
    // 添加拦截器
    dio.interceptors.add(InterceptorsWrapper(
      onRequest: (options, handler) {
        print('请求:${options.uri}');
        options.headers['Authorization'] = 'Bearer token';
        handler.next(options);
      },
      onResponse: (response, handler) {
        print('响应:${response.data}');
        handler.next(response);
      },
      onError: (error, handler) {
        print('错误:${error.message}');
        handler.next(error);
      },
    ));
  }
  
  Future<Response> get(String path) async {
    return await dio.get(path);
  }
  
  Future<Response> post(String path, {dynamic data}) async {
    return await dio.post(path, data: data);
  }
}

// 使用
void main() async {
  final http = HttpUtil();
  final response = await http.get('/users');
  print(response.data);
}

21. Flutter Web 跨域问题

解决方案

方法一:后端配置 CORS

// 服务端需要添加 CORS 头
Access-Control-Allow-Origin: *
Access-Control-Allow-Methods: GET, POST, PUT, DELETE
Access-Control-Allow-Headers: Content-Type, Authorization

方法二:使用代理(开发环境)

在项目根目录创建 web/manifest.json,然后使用代理服务器。

方法三:Dio 配置

final dio = Dio();
dio.options.headers['Access-Control-Allow-Origin'] = '*';

总结

本笔记涵盖了 Flutter 的核心知识点:

  • 布局组件:Container、Column、Row、Stack、Wrap 等
  • 基础组件:Text、Image、TextField 等
  • 滚动组件:ListView、GridView、PageView 等
  • 状态管理:Provider 等
  • 网络请求:Dio 工具封装