第9讲:列表与网格:展示动态数据

掌握ListView与GridView,构建可滚动的数据展示界面。

你好,欢迎回到《Flutter入门到精通》专栏。在上一讲中,我们构建了一个完整的登录页面。现在,让我们来学习如何在Flutter中高效地展示大量数据——无论是简单的线性列表还是复杂的网格布局。

一、ListView:滚动的线性列表

ListView是Flutter中最常用的滚动Widget,用于在垂直或水平方向上排列子项。

1.1 ListView的基本用法

1.1.1 静态列表(少量数据)

对于数量固定的子项,可以直接使用ListView的默认构造函数:

dart

ListView(
  padding: EdgeInsets.all(16),
  children: <Widget>[
    ListTile(
      leading: Icon(Icons.person),
      title: Text('张三'),
      subtitle: Text('前端开发工程师'),
      trailing: Icon(Icons.arrow_forward_ios),
    ),
    ListTile(
      leading: Icon(Icons.person),
      title: Text('李四'),
      subtitle: Text('Flutter开发工程师'),
      trailing: Icon(Icons.arrow_forward_ios),
    ),
    ListTile(
      leading: Icon(Icons.person),
      title: Text('王五'),
      subtitle: Text('全栈开发工程师'),
      trailing: Icon(Icons.arrow_forward_ios),
    ),
    // ... 更多ListTile
  ],
)
1.1.2 ListTile - 列表项的标准组件

ListTile提供了标准的Material Design列表项布局:

dart

ListTile(
  leading: CircleAvatar( // 左侧部件
    backgroundImage: NetworkImage('https://example.com/avatar.jpg'),
  ),
  title: Text('标题'), // 主标题
  subtitle: Text('副标题'), // 副标题
  trailing: Icon(Icons.more_vert), // 右侧部件
  isThreeLine: true, // 是否显示三行(标题 + 两行副标题)
  dense: true, // 是否使用紧凑布局
  contentPadding: EdgeInsets.symmetric(horizontal: 16), // 内边距
  onTap: () {
    // 点击回调
    print('列表项被点击');
  },
)

1.2 ListView.builder:动态列表(大量数据)

对于大量数据或动态数据,必须使用ListView.builder,它只会构建可见的子项,性能极佳。

dart

class DynamicListViewExample extends StatelessWidget {
  final List<String> items = List.generate(100, (index) => '项目 ${index + 1}');

  @override
  Widget build(BuildContext context) {
    return ListView.builder(
      itemCount: items.length, // 列表项总数
      itemBuilder: (context, index) {
        // 为每个索引构建对应的列表项
        return Card(
          margin: EdgeInsets.symmetric(horizontal: 16, vertical: 4),
          child: ListTile(
            leading: CircleAvatar(
              child: Text('${index + 1}'),
            ),
            title: Text(items[index]),
            subtitle: Text('这是第 ${index + 1} 个项目的描述'),
            trailing: Icon(Icons.arrow_forward_ios),
            onTap: () {
              ScaffoldMessenger.of(context).showSnackBar(
                SnackBar(content: Text('你点击了: ${items[index]}'))
              );
            },
          ),
        );
      },
    );
  }
}

1.3 ListView.separated:带分隔符的列表

ListView.separated在每两个列表项之间添加一个分隔符:

dart

ListView.separated(
  itemCount: 50,
  separatorBuilder: (context, index) => Divider( // 分隔符
    color: Colors.grey[300],
    height: 1,
    indent: 16, // 起始缩进
    endIndent: 16, // 结束缩进
  ),
  itemBuilder: (context, index) {
    return ListTile(
      title: Text('消息 ${index + 1}'),
      subtitle: Text('这是第 ${index + 1} 条消息的内容...'),
      leading: Icon(Icons.message),
    );
  },
)

1.4 水平ListView

通过设置scrollDirection属性创建水平列表:

dart

SizedBox(
  height: 120, // 必须指定高度
  child: ListView.builder(
    scrollDirection: Axis.horizontal, // 水平滚动
    itemCount: 10,
    itemBuilder: (context, index) {
      return Container(
        width: 100,
        margin: EdgeInsets.all(8),
        decoration: BoxDecoration(
          color: Colors.blue[50],
          borderRadius: BorderRadius.circular(12),
        ),
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            Icon(Icons.star, size: 40, color: Colors.amber),
            SizedBox(height: 8),
            Text('功能 ${index + 1}'),
          ],
        ),
      );
    },
  ),
)

二、GridView:网格布局

GridView用于在二维网格中排列子项,非常适合展示图片库、产品网格等。

2.1 GridView.count:固定列数的网格

指定网格的列数,自动计算每个项的宽度:

dart

GridView.count(
  crossAxisCount: 3, // 交叉轴上的列数(横屏时的行数)
  crossAxisSpacing: 8, // 水平间距
  mainAxisSpacing: 8, // 垂直间距
  padding: EdgeInsets.all(16),
  children: List.generate(20, (index) {
    return Container(
      decoration: BoxDecoration(
        color: Colors.primaries[index % Colors.primaries.length],
        borderRadius: BorderRadius.circular(8),
      ),
      child: Center(
        child: Text(
          '${index + 1}',
          style: TextStyle(color: Colors.white, fontSize: 18),
        ),
      ),
    );
  }),
)

2.2 GridView.builder:动态网格

与ListView.builder类似,用于大量数据的网格:

dart

class DynamicGridViewExample extends StatelessWidget {
  final List<Product> products = List.generate(50, (index) => Product(
    id: index,
    name: '产品 ${index + 1}',
    price: (index + 1) * 10.0,
    imageUrl: 'https://picsum.photos/200/200?random=$index',
  ));

  @override
  Widget build(BuildContext context) {
    return GridView.builder(
      gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
        crossAxisCount: 2, // 两列网格
        crossAxisSpacing: 8,
        mainAxisSpacing: 8,
        childAspectRatio: 0.8, // 子项的宽高比
      ),
      padding: EdgeInsets.all(16),
      itemCount: products.length,
      itemBuilder: (context, index) {
        return _buildProductItem(products[index]);
      },
    );
  }

  Widget _buildProductItem(Product product) {
    return Card(
      elevation: 2,
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          // 产品图片
          Expanded(
            child: Container(
              width: double.infinity,
              decoration: BoxDecoration(
                borderRadius: BorderRadius.vertical(top: Radius.circular(4)),
                image: DecorationImage(
                  image: NetworkImage(product.imageUrl),
                  fit: BoxFit.cover,
                ),
              ),
            ),
          ),
          // 产品信息
          Padding(
            padding: EdgeInsets.all(8),
            child: Column(
              crossAxisAlignment: CrossAxisAlignment.start,
              children: [
                Text(
                  product.name,
                  style: TextStyle(
                    fontWeight: FontWeight.bold,
                    fontSize: 14,
                  ),
                  maxLines: 1,
                  overflow: TextOverflow.ellipsis,
                ),
                SizedBox(height: 4),
                Text(
                  '¥${product.price.toStringAsFixed(2)}',
                  style: TextStyle(
                    color: Colors.red,
                    fontWeight: FontWeight.bold,
                    fontSize: 16,
                  ),
                ),
              ],
            ),
          ),
        ],
      ),
    );
  }
}

class Product {
  final int id;
  final String name;
  final double price;
  final String imageUrl;

  Product({
    required this.id,
    required this.name,
    required this.price,
    required this.imageUrl,
  });
}

2.3 GridView.extended:自定义网格布局

提供最大的灵活性,可以自定义每个网格项的尺寸:

dart

GridView.extent(
  maxCrossAxisExtent: 150, // 每个网格项的最大宽度
  crossAxisSpacing: 8,
  mainAxisSpacing: 8,
  padding: EdgeInsets.all(16),
  children: List.generate(15, (index) {
    return Container(
      decoration: BoxDecoration(
        gradient: LinearGradient(
          colors: [Colors.blue, Colors.purple],
          begin: Alignment.topLeft,
          end: Alignment.bottomRight,
        ),
        borderRadius: BorderRadius.circular(12),
      ),
      child: Center(
        child: Icon(
          Icons.favorite,
          color: Colors.white,
          size: 40,
        ),
      ),
    );
  }),
)

三、高级特性与性能优化

3.1 下拉刷新与上拉加载

使用RefreshIndicator和ScrollController实现经典的列表交互:

dart

class RefreshableListView extends StatefulWidget {
  const RefreshableListView({super.key});

  @override
  State<RefreshableListView> createState() => _RefreshableListViewState();
}

class _RefreshableListViewState extends State<RefreshableListView> {
  final List<String> _items = List.generate(20, (index) => '初始项目 ${index + 1}');
  final ScrollController _scrollController = ScrollController();
  bool _isLoading = false;

  @override
  void initState() {
    super.initState();
    _scrollController.addListener(_scrollListener);
  }

  void _scrollListener() {
    if (_scrollController.position.pixels == 
        _scrollController.position.maxScrollExtent) {
      _loadMoreItems();
    }
  }

  Future<void> _loadMoreItems() async {
    if (_isLoading) return;
    
    setState(() {
      _isLoading = true;
    });

    // 模拟网络请求
    await Future.delayed(Duration(seconds: 2));

    setState(() {
      final newItems = List.generate(10, (index) 
        => '加载更多项目 ${_items.length + index + 1}');
      _items.addAll(newItems);
      _isLoading = false;
    });
  }

  Future<void> _refreshItems() async {
    // 模拟刷新请求
    await Future.delayed(Duration(seconds: 2));
    
    setState(() {
      _items.clear();
      _items.addAll(List.generate(20, (index) => '刷新项目 ${index + 1}'));
    });
  }

  @override
  Widget build(BuildContext context) {
    return RefreshIndicator(
      onRefresh: _refreshItems,
      child: ListView.builder(
        controller: _scrollController,
        itemCount: _items.length + (_isLoading ? 1 : 0),
        itemBuilder: (context, index) {
          if (index == _items.length) {
            // 加载更多指示器
            return Center(
              child: Padding(
                padding: EdgeInsets.all(16),
                child: CircularProgressIndicator(),
              ),
            );
          }
          
          return ListTile(
            leading: CircleAvatar(child: Text('${index + 1}')),
            title: Text(_items[index]),
            subtitle: Text('最后更新: ${DateTime.now()}'),
          );
        },
      ),
    );
  }

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

3.2 性能优化技巧

  1. 使用const构造函数

dart

itemBuilder: (context, index) {
  return const ListTile( // 使用const
    leading: Icon(Icons.star),
    title: Text('固定内容'),
  );
}
  1. 为列表项添加key

dart

itemBuilder: (context, index) {
  return ProductItem(
    key: ValueKey(products[index].id), // 为动态列表项添加key
    product: products[index],
  );
}
  1. 保持build方法纯净

dart

// 错误做法:在build方法中创建列表数据
Widget build(BuildContext context) {
  final items = List.generate(1000, (index) => 'Item $index'); // 每次重建都会重新生成
  return ListView.builder(/* ... */);
}

// 正确做法:将数据初始化放在State中
class _MyListState extends State<MyList> {
  final List<String> items = List.generate(1000, (index) => 'Item $index');
  
  @override
  Widget build(BuildContext context) {
    return ListView.builder(/* ... */);
  }
}

四、综合实战:构建一个新闻列表应用

让我们创建一个完整的新闻列表应用,综合运用各种列表技术:

dart

class NewsApp extends StatelessWidget {
  final List<NewsArticle> articles = [
    NewsArticle(
      title: 'Flutter 3.0 正式发布',
      summary: 'Flutter 3.0 带来了对macOS和Linux的稳定支持,以及众多新特性...',
      author: '技术社区',
      publishTime: '2024-01-15',
      imageUrl: 'https://picsum.photos/400/200?random=1',
      category: '技术',
    ),
    NewsArticle(
      title: 'Dart语言的新特性',
      summary: 'Dart 3.0 引入了健全的空安全和其他语言改进...',
      author: '开发者周刊',
      publishTime: '2024-01-14',
      imageUrl: 'https://picsum.photos/400/200?random=2',
      category: '编程',
    ),
    // 可以添加更多新闻...
  ];

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('新闻列表'),
        actions: [
          IconButton(
            icon: Icon(Icons.search),
            onPressed: () {},
          ),
        ],
      ),
      body: ListView.separated(
        padding: EdgeInsets.all(16),
        separatorBuilder: (context, index) => SizedBox(height: 16),
        itemCount: articles.length,
        itemBuilder: (context, index) {
          return _buildNewsCard(articles[index]);
        },
      ),
    );
  }

  Widget _buildNewsCard(NewsArticle article) {
    return Card(
      elevation: 2,
      child: InkWell(
        onTap: () {
          // 跳转到新闻详情页
        },
        child: Padding(
          padding: EdgeInsets.all(16),
          child: Row(
            crossAxisAlignment: CrossAxisAlignment.start,
            children: [
              // 新闻图片
              Container(
                width: 80,
                height: 80,
                decoration: BoxDecoration(
                  borderRadius: BorderRadius.circular(8),
                  image: DecorationImage(
                    image: NetworkImage(article.imageUrl),
                    fit: BoxFit.cover,
                  ),
                ),
              ),
              SizedBox(width: 16),
              // 新闻内容
              Expanded(
                child: Column(
                  crossAxisAlignment: CrossAxisAlignment.start,
                  children: [
                    // 分类标签
                    Container(
                      padding: EdgeInsets.symmetric(horizontal: 8, vertical: 2),
                      decoration: BoxDecoration(
                        color: Colors.blue[50],
                        borderRadius: BorderRadius.circular(4),
                      ),
                      child: Text(
                        article.category,
                        style: TextStyle(
                          color: Colors.blue[700],
                          fontSize: 12,
                          fontWeight: FontWeight.w500,
                        ),
                      ),
                    ),
                    SizedBox(height: 8),
                    // 标题
                    Text(
                      article.title,
                      style: TextStyle(
                        fontWeight: FontWeight.bold,
                        fontSize: 16,
                      ),
                      maxLines: 2,
                      overflow: TextOverflow.ellipsis,
                    ),
                    SizedBox(height: 4),
                    // 摘要
                    Text(
                      article.summary,
                      style: TextStyle(
                        color: Colors.grey[600],
                        fontSize: 14,
                      ),
                      maxLines: 2,
                      overflow: TextOverflow.ellipsis,
                    ),
                    SizedBox(height: 8),
                    // 作者和时间
                    Row(
                      children: [
                        Text(
                          article.author,
                          style: TextStyle(
                            color: Colors.grey[500],
                            fontSize: 12,
                          ),
                        ),
                        SizedBox(width: 8),
                        Text(
                          '•',
                          style: TextStyle(color: Colors.grey[400]),
                        ),
                        SizedBox(width: 8),
                        Text(
                          article.publishTime,
                          style: TextStyle(
                            color: Colors.grey[500],
                            fontSize: 12,
                          ),
                        ),
                      ],
                    ),
                  ],
                ),
              ),
            ],
          ),
        ),
      ),
    );
  }
}

class NewsArticle {
  final String title;
  final String summary;
  final String author;
  final String publishTime;
  final String imageUrl;
  final String category;

  NewsArticle({
    required this.title,
    required this.summary,
    required this.author,
    required this.publishTime,
    required this.imageUrl,
    required this.category,
  });
}

结语

恭喜!通过本讲的学习,你已经掌握了Flutter中展示动态数据的核心技能。ListView和GridView是构建大多数应用的基石,理解它们的各种用法和性能优化技巧至关重要。

记住这些关键点:

  • 少量数据:使用ListView/GridView的默认构造函数

  • 大量数据:必须使用.builder构造函数

  • 需要分隔符:使用.separated构造函数

  • 性能优化:使用const、添加key、避免在build中创建数据

在下一讲中,我们将学习Flutter中的导航与路由,让你能够在不同页面之间跳转,构建真正的多页面应用。

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

移动端开发者

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值