← Frontend / Flutter

05_列表与滚动

Flutter 列表组件和滚动机制

ListView、GridView、下拉刷新、上拉加载更多。


ListView

基础列表(少量固定项)

ListView(
  padding: const EdgeInsets.all(8),
  children: [
    ListTile(
      leading: const Icon(Icons.inbox),
      title: const Text('收件箱'),
      subtitle: const Text('3 条未读'),
      trailing: const Icon(Icons.chevron_right),
      onTap: () {},
    ),
    const Divider(),              // 分隔线
    ListTile(
      leading: const CircleAvatar(child: Text('A')),
      title: const Text('Alice'),
      subtitle: const Text('最近消息内容'),
      trailing: const Text('12:00', style: TextStyle(color: Colors.grey)),
    ),
  ],
)

ListView.builder(虚拟列表,推荐)

按需渲染,只构建可见区域的 Item,适合长列表(类似 vue-virtual-scroller):

ListView.builder(
  itemCount: items.length,
  itemBuilder: (context, index) {
    final item = items[index];
    return Card(
      margin: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
      child: ListTile(
        title: Text(item.title),
        subtitle: Text(item.subtitle),
        onTap: () {},
      ),
    );
  },
)

ListView.separated(带分隔符)

ListView.separated(
  itemCount: items.length,
  separatorBuilder: (context, index) => const Divider(height: 1),
  itemBuilder: (context, index) => ListTile(title: Text(items[index])),
)

GridView

// 固定列数
GridView.builder(
  gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
    crossAxisCount: 2,           // 列数
    crossAxisSpacing: 8,         // 列间距
    mainAxisSpacing: 8,          // 行间距
    childAspectRatio: 0.75,      // 宽高比
  ),
  itemCount: products.length,
  itemBuilder: (context, index) => ProductCard(product: products[index]),
)

// 自适应列数(类似 CSS grid auto-fill)
GridView.builder(
  gridDelegate: const SliverGridDelegateWithMaxCrossAxisExtent(
    maxCrossAxisExtent: 200,     // 每格最大宽度,自动计算列数
    crossAxisSpacing: 8,
    mainAxisSpacing: 8,
  ),
  itemCount: 20,
  itemBuilder: (context, index) => Card(
    child: Center(child: Text('$index')),
  ),
)

下拉刷新

RefreshIndicator(
  onRefresh: () async {
    await loadData();  // 刷新数据
  },
  child: ListView.builder(
    itemCount: items.length,
    itemBuilder: (context, index) => ListTile(title: Text(items[index])),
  ),
)

上拉加载更多

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

  @override
  State<InfiniteList> createState() => _InfiniteListState();
}

class _InfiniteListState extends State<InfiniteList> {
  final ScrollController _scrollController = ScrollController();
  final List<String> _items = List.generate(20, (i) => '第 $i 条');
  bool _isLoading = false;

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

  void _onScroll() {
    // 距底部 200px 时触发加载
    if (_scrollController.position.pixels >=
        _scrollController.position.maxScrollExtent - 200) {
      _loadMore();
    }
  }

  Future<void> _loadMore() async {
    if (_isLoading) return;
    setState(() => _isLoading = true);
    await Future.delayed(const Duration(seconds: 1));  // 模拟请求
    if (mounted) {
      setState(() {
        _items.addAll(List.generate(10, (i) => '更多第 ${_items.length + i} 条'));
        _isLoading = false;
      });
    }
  }

  @override
  void dispose() {
    _scrollController.dispose();  // 必须释放!
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return ListView.builder(
      controller: _scrollController,
      itemCount: _items.length + (_isLoading ? 1 : 0),
      itemBuilder: (context, index) {
        if (index == _items.length) {
          // 底部 loading 指示器
          return const Padding(
            padding: EdgeInsets.all(16),
            child: Center(child: CircularProgressIndicator()),
          );
        }
        return ListTile(title: Text(_items[index]));
      },
    );
  }
}

CustomScrollView + Sliver(高级滚动)

当需要在同一个可滚动区域混合多种布局时使用:

CustomScrollView(
  slivers: [
    // 可折叠顶部图片 + 标题
    SliverAppBar(
      expandedHeight: 250,
      pinned: true,
      flexibleSpace: FlexibleSpaceBar(
        title: const Text('商品列表'),
        background: Image.network('https://example.com/banner.jpg', fit: BoxFit.cover),
      ),
    ),

    // 横向分类标签(固定高度)
    SliverToBoxAdapter(
      child: SizedBox(
        height: 60,
        child: ListView(
          scrollDirection: Axis.horizontal,
          children: ['全部', '电子', '服装', '食品']
              .map((tag) => Padding(
                    padding: const EdgeInsets.all(8),
                    child: Chip(label: Text(tag)),
                  ))
              .toList(),
        ),
      ),
    ),

    // 商品网格
    SliverGrid(
      gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
        crossAxisCount: 2,
        childAspectRatio: 0.75,
        crossAxisSpacing: 8,
        mainAxisSpacing: 8,
      ),
      delegate: SliverChildBuilderDelegate(
        (context, index) => ProductCard(product: products[index]),
        childCount: products.length,
      ),
    ),
  ],
)