Skip to content

第12章:Flutter 常用组件进阶

12.1 列表组件(ListView)

ListView.builder

ListView.builder 是 Flutter 中最常用的列表组件,适合显示大量数据,具有性能优化功能。

基本使用

dart
ListView.builder(
  itemCount: items.length, // 列表项数量
  itemBuilder: (context, index) {
    return ListTile(
      title: Text(items[index].title),
      subtitle: Text(items[index].subtitle),
      leading: Icon(items[index].icon),
      trailing: Icon(Icons.arrow_forward),
      onTap: () {
        // 点击事件
        print('Tapped on item $index');
      },
    );
  },
);

列表项点击事件

dart
ListView.builder(
  itemCount: items.length,
  itemBuilder: (context, index) {
    return GestureDetector(
      onTap: () {
        Navigator.push(
          context,
          MaterialPageRoute(
            builder: (context) => DetailPage(item: items[index]),
          ),
        );
      },
      child: ListTile(
        title: Text(items[index].title),
        subtitle: Text(items[index].subtitle),
      ),
    );
  },
);

列表分割线

方法 1:使用 Divider

dart
ListView.separated(
  itemCount: items.length,
  separatorBuilder: (context, index) => Divider(),
  itemBuilder: (context, index) {
    return ListTile(
      title: Text(items[index].title),
    );
  },
);

方法 2:自定义分割线

dart
ListView.builder(
  itemCount: items.length,
  itemBuilder: (context, index) {
    return Column(
      children: [
        ListTile(
          title: Text(items[index].title),
        ),
        if (index < items.length - 1)
          Container(
            height: 1,
            color: Colors.grey[200],
            margin: EdgeInsets.symmetric(horizontal: 16),
          ),
      ],
    );
  },
);

下拉刷新(RefreshIndicator)

dart
RefreshIndicator(
  onRefresh: _refreshData, // 刷新回调
  child: ListView.builder(
    itemCount: items.length,
    itemBuilder: (context, index) {
      return ListTile(
        title: Text(items[index].title),
      );
    },
  ),
);

// 刷新数据的方法
Future<void> _refreshData() async {
  // 模拟网络请求
  await Future.delayed(Duration(seconds: 2));
  setState(() {
    // 更新数据
    items = [...items];
  });
}

列表上拉加载更多

dart
ListView.builder(
  itemCount: items.length + 1, // 增加一个加载项
  itemBuilder: (context, index) {
    if (index == items.length) {
      // 显示加载指示器
      return _isLoading
          ? Padding(
              padding: const EdgeInsets.all(16.0),
              child: Center(child: CircularProgressIndicator()),
            )
          : Padding(
              padding: const EdgeInsets.all(16.0),
              child: Center(child: Text('No more data')),
            );
    }
    return ListTile(
      title: Text(items[index].title),
    );
  },
  controller: _scrollController,
);

// 滚动控制器
final _scrollController = ScrollController();

@override
void initState() {
  super.initState();
  _scrollController.addListener(() {
    if (_scrollController.position.pixels == _scrollController.position.maxScrollExtent) {
      _loadMoreData();
    }
  });
}

// 加载更多数据
void _loadMoreData() {
  if (!_isLoading) {
    setState(() {
      _isLoading = true;
    });
    // 模拟网络请求
    Future.delayed(Duration(seconds: 2), () {
      setState(() {
        items.addAll(newItems);
        _isLoading = false;
      });
    });
  }
}

12.2 网格组件(GridView)

GridView 用于实现网格布局,如商品列表、图片画廊等。

基本使用

dart
GridView.count(
  crossAxisCount: 2, // 列数
  mainAxisSpacing: 10, // 行间距
  crossAxisSpacing: 10, // 列间距
  padding: EdgeInsets.all(10), // 内边距
  children: List.generate(
    items.length,
    (index) {
      return Card(
        child: Column(
          children: [
            Image.network(
              items[index].imageUrl,
              height: 100,
              width: double.infinity,
              fit: BoxFit.cover,
            ),
            Padding(
              padding: const EdgeInsets.all(8.0),
              child: Text(items[index].title),
            ),
          ],
        ),
      );
    },
  ),
);

GridView.builder

dart
GridView.builder(
  gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
    crossAxisCount: 2,
    mainAxisSpacing: 10,
    crossAxisSpacing: 10,
    childAspectRatio: 0.8, // 子项宽高比
  ),
  itemCount: items.length,
  itemBuilder: (context, index) {
    return Card(
      child: Column(
        children: [
          Image.network(items[index].imageUrl),
          Padding(
            padding: const EdgeInsets.all(8.0),
            child: Text(items[index].title),
          ),
        ],
      ),
    );
  },
);

12.3 滚动组件(SingleChildScrollView)

SingleChildScrollView 用于解决页面溢出问题,实现滚动功能。

基本使用

dart
SingleChildScrollView(
  child: Column(
    children: [
      // 大量子组件
      for (int i = 0; i < 50; i++)
        Padding(
          padding: const EdgeInsets.all(8.0),
          child: Container(
            height: 50,
            color: Colors.blue,
            child: Center(child: Text('Item $i')),
          ),
        ),
    ],
  ),
);

水平滚动

dart
SingleChildScrollView(
  scrollDirection: Axis.horizontal,
  child: Row(
    children: [
      for (int i = 0; i < 20; i++)
        Padding(
          padding: const EdgeInsets.all(8.0),
          child: Container(
            width: 100,
            height: 100,
            color: Colors.blue,
            child: Center(child: Text('Item $i')),
          ),
        ),
    ],
  ),
);

嵌套滚动

dart
SingleChildScrollView(
  child: Column(
    children: [
      Container(
        height: 200,
        color: Colors.red,
      ),
      SingleChildScrollView(
        scrollDirection: Axis.horizontal,
        child: Row(
          children: [
            for (int i = 0; i < 10; i++)
              Container(
                width: 100,
                height: 100,
                color: Colors.blue,
                margin: EdgeInsets.all(10),
              ),
          ],
        ),
      ),
      Container(
        height: 500,
        color: Colors.green,
      ),
    ],
  ),
);

12.4 对话框组件(AlertDialog)

普通对话框

dart
void _showAlertDialog() {
  showDialog(
    context: context,
    builder: (BuildContext context) {
      return AlertDialog(
        title: Text('Alert Dialog'),
        content: Text('This is an alert dialog.'),
        actions: [
          TextButton(
            onPressed: () {
              Navigator.of(context).pop();
            },
            child: Text('Cancel'),
          ),
          TextButton(
            onPressed: () {
              // 执行操作
              Navigator.of(context).pop();
            },
            child: Text('OK'),
          ),
        ],
      );
    },
  );
}

输入对话框

dart
void _showInputDialog() {
  TextEditingController _controller = TextEditingController();
  
  showDialog(
    context: context,
    builder: (BuildContext context) {
      return AlertDialog(
        title: Text('Input Dialog'),
        content: TextField(
          controller: _controller,
          decoration: InputDecoration(labelText: 'Enter your name'),
        ),
        actions: [
          TextButton(
            onPressed: () {
              Navigator.of(context).pop();
            },
            child: Text('Cancel'),
          ),
          TextButton(
            onPressed: () {
              // 获取输入内容
              String name = _controller.text;
              print('Name: $name');
              Navigator.of(context).pop();
            },
            child: Text('OK'),
          ),
        ],
      );
    },
  );
}

12.5 底部弹窗(showModalBottomSheet)

dart
void _showBottomSheet() {
  showModalBottomSheet(
    context: context,
    builder: (BuildContext context) {
      return Container(
        padding: EdgeInsets.all(16),
        child: Column(
          mainAxisSize: MainAxisSize.min,
          children: [
            ListTile(
              leading: Icon(Icons.share),
              title: Text('Share'),
              onTap: () {
                Navigator.of(context).pop();
                // 执行分享操作
              },
            ),
            ListTile(
              leading: Icon(Icons.copy),
              title: Text('Copy'),
              onTap: () {
                Navigator.of(context).pop();
                // 执行复制操作
              },
            ),
            ListTile(
              leading: Icon(Icons.delete),
              title: Text('Delete'),
              onTap: () {
                Navigator.of(context).pop();
                // 执行删除操作
              },
            ),
          ],
        ),
      );
    },
  );
}

自定义底部弹窗

dart
void _showCustomBottomSheet() {
  showModalBottomSheet(
    context: context,
    isScrollControlled: true, // 全屏模式
    builder: (BuildContext context) {
      return DraggableScrollableSheet(
        initialChildSize: 0.5, // 初始高度
        maxChildSize: 0.8, // 最大高度
        minChildSize: 0.3, // 最小高度
        builder: (BuildContext context, ScrollController scrollController) {
          return Container(
            color: Colors.white,
            child: ListView.builder(
              controller: scrollController,
              itemCount: 20,
              itemBuilder: (context, index) {
                return ListTile(
                  title: Text('Item $index'),
                );
              },
            ),
          );
        },
      );
    },
  );
}

12.6 实操案例:实现动态列表、下拉刷新、上拉加载、对话框交互

目标

创建一个应用,实现动态列表展示、下拉刷新、上拉加载更多功能,并添加对话框交互。

步骤 1:创建数据模型

dart
class Item {
  final int id;
  final String title;
  final String subtitle;
  final String imageUrl;

  Item({
    required this.id,
    required this.title,
    required this.subtitle,
    required this.imageUrl,
  });
}

步骤 2:创建主页面

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

class ListDemoPage extends StatefulWidget {
  @override
  _ListDemoPageState createState() => _ListDemoPageState();
}

class _ListDemoPageState extends State<ListDemoPage> {
  List<Item> _items = [];
  bool _isLoading = false;
  bool _isLoadingMore = false;
  int _page = 1;
  final _scrollController = ScrollController();

  @override
  void initState() {
    super.initState();
    _loadData();
    _scrollController.addListener(() {
      if (_scrollController.position.pixels == _scrollController.position.maxScrollExtent) {
        _loadMoreData();
      }
    });
  }

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

  Future<void> _loadData() async {
    setState(() {
      _isLoading = true;
    });
    
    // 模拟网络请求
    await Future.delayed(Duration(seconds: 2));
    
    setState(() {
      _items = List.generate(10, (index) => Item(
        id: index + 1,
        title: 'Item ${index + 1}',
        subtitle: 'This is item ${index + 1}',
        imageUrl: 'https://picsum.photos/200/300?random=${index + 1}',
      ));
      _isLoading = false;
      _page = 1;
    });
  }

  Future<void> _loadMoreData() async {
    if (!_isLoadingMore) {
      setState(() {
        _isLoadingMore = true;
      });
      
      // 模拟网络请求
      await Future.delayed(Duration(seconds: 2));
      
      setState(() {
        _page++;
        _items.addAll(List.generate(10, (index) => Item(
          id: (_page - 1) * 10 + index + 1,
          title: 'Item ${(_page - 1) * 10 + index + 1}',
          subtitle: 'This is item ${(_page - 1) * 10 + index + 1}',
          imageUrl: 'https://picsum.photos/200/300?random=${(_page - 1) * 10 + index + 1}',
        )));
        _isLoadingMore = false;
      });
    }
  }

  void _showItemDialog(Item item) {
    showDialog(
      context: context,
      builder: (BuildContext context) {
        return AlertDialog(
          title: Text(item.title),
          content: Column(
            mainAxisSize: MainAxisSize.min,
            children: [
              Image.network(item.imageUrl, height: 200),
              SizedBox(height: 10),
              Text(item.subtitle),
            ],
          ),
          actions: [
            TextButton(
              onPressed: () {
                Navigator.of(context).pop();
              },
              child: Text('Cancel'),
            ),
            TextButton(
              onPressed: () {
                Navigator.of(context).pop();
                // 执行操作
                ScaffoldMessenger.of(context).showSnackBar(
                  SnackBar(content: Text('Action performed on ${item.title}')),
                );
              },
              child: Text('OK'),
            ),
          ],
        );
      },
    );
  }

  void _showBottomSheet(Item item) {
    showModalBottomSheet(
      context: context,
      builder: (BuildContext context) {
        return Container(
          padding: EdgeInsets.all(16),
          child: Column(
            mainAxisSize: MainAxisSize.min,
            children: [
              ListTile(
                leading: Icon(Icons.share),
                title: Text('Share ${item.title}'),
                onTap: () {
                  Navigator.of(context).pop();
                  ScaffoldMessenger.of(context).showSnackBar(
                    SnackBar(content: Text('Shared ${item.title}')),
                  );
                },
              ),
              ListTile(
                leading: Icon(Icons.copy),
                title: Text('Copy ${item.title}'),
                onTap: () {
                  Navigator.of(context).pop();
                  ScaffoldMessenger.of(context).showSnackBar(
                    SnackBar(content: Text('Copied ${item.title}')),
                  );
                },
              ),
              ListTile(
                leading: Icon(Icons.delete),
                title: Text('Delete ${item.title}'),
                onTap: () {
                  Navigator.of(context).pop();
                  setState(() {
                    _items.remove(item);
                  });
                  ScaffoldMessenger.of(context).showSnackBar(
                    SnackBar(content: Text('Deleted ${item.title}')),
                  );
                },
              ),
            ],
          ),
        );
      },
    );
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('List Demo'),
      ),
      body: _isLoading
          ? Center(child: CircularProgressIndicator())
          : RefreshIndicator(
              onRefresh: _loadData,
              child: ListView.builder(
                controller: _scrollController,
                itemCount: _items.length + 1,
                itemBuilder: (context, index) {
                  if (index == _items.length) {
                    return _isLoadingMore
                        ? Padding(
                            padding: const EdgeInsets.all(16.0),
                            child: Center(child: CircularProgressIndicator()),
                          )
                        : Padding(
                            padding: const EdgeInsets.all(16.0),
                            child: Center(child: Text('No more data')),
                          );
                  }
                  final item = _items[index];
                  return GestureDetector(
                    onTap: () => _showItemDialog(item),
                    onLongPress: () => _showBottomSheet(item),
                    child: Card(
                      margin: EdgeInsets.symmetric(horizontal: 16, vertical: 8),
                      child: Padding(
                        padding: const EdgeInsets.all(16.0),
                        child: Row(
                          children: [
                            Image.network(
                              item.imageUrl,
                              width: 80,
                              height: 80,
                              fit: BoxFit.cover,
                            ),
                            SizedBox(width: 16),
                            Expanded(
                              child: Column(
                                crossAxisAlignment: CrossAxisAlignment.start,
                                children: [
                                  Text(item.title, style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold)),
                                  SizedBox(height: 8),
                                  Text(item.subtitle, style: TextStyle(color: Colors.grey)),
                                ],
                              ),
                            ),
                            Icon(Icons.arrow_forward),
                          ],
                        ),
                      ),
                    ),
                  );
                },
              ),
            ),
    );
  }
}

步骤 3:运行应用

  1. 启动模拟器或连接真机
  2. 运行项目
  3. 测试下拉刷新功能
  4. 测试上拉加载更多功能
  5. 点击列表项测试对话框功能
  6. 长按列表项测试底部弹窗功能

12.7 常用组件最佳实践

1. 列表组件

  • 使用 ListView.builder:对于大量数据,使用 ListView.builder 提高性能
  • 合理设置 itemCount:确保 itemCount 与数据长度一致
  • 避免嵌套滚动:尽量避免在 ListView 中嵌套其他滚动组件
  • 使用 key:为列表项提供唯一的 key,提高性能

2. 网格组件

  • 选择合适的构造器:根据数据量选择 GridView.countGridView.builder
  • 合理设置间距:通过 mainAxisSpacingcrossAxisSpacing 控制间距
  • 设置合适的宽高比:通过 childAspectRatio 控制子项比例

3. 滚动组件

  • 避免过度使用:只在必要时使用 SingleChildScrollView
  • 设置 physics:根据需要设置滚动行为
  • 处理键盘:在包含输入框时,使用 SingleChildScrollView 避免键盘遮挡

4. 对话框组件

  • 保持简洁:对话框内容不要过于复杂
  • 提供明确的操作:确保用户知道如何关闭对话框
  • 处理返回键:默认情况下,点击返回键会关闭对话框

5. 底部弹窗

  • 使用 isScrollControlled:需要更大空间时设置为 true
  • 使用 DraggableScrollableSheet:实现可拖动的底部弹窗
  • 保持内容简洁:底部弹窗内容应该清晰明了

12.8 小结

本章介绍了 Flutter 的常用组件进阶,包括列表组件、网格组件、滚动组件、对话框组件、底部弹窗等内容。这些组件是 Flutter 开发中高频使用的,掌握它们对于构建功能丰富的应用至关重要。

我们学习了:

  • ListView:实现列表展示,支持下拉刷新和上拉加载
  • GridView:实现网格布局,适合展示商品、图片等
  • SingleChildScrollView:解决页面溢出问题,实现滚动功能
  • AlertDialog:实现对话框交互,包括普通对话框和输入对话框
  • showModalBottomSheet:实现底部弹窗,提供快捷操作

通过实操案例,我们实现了一个包含动态列表、下拉刷新、上拉加载、对话框交互和底部弹窗的应用,体验了这些组件的完整使用流程。在实际开发中,你应该根据应用的需求选择合适的组件,并遵循最佳实践,提升应用的用户体验。

在接下来的章节中,我们将进入综合实战部分,通过实际项目来巩固所学的知识。

© 2026 编程马·菜鸟教程 版权所有