第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}');@overrideWidget 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',));@overrideWidget 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});@overrideState<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;@overridevoid 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}'));});}@overrideWidget 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()}'),);},),);}@overridevoid dispose() {_scrollController.dispose();super.dispose();}
}3.2 性能优化技巧
使用const构造函数:
dart
itemBuilder: (context, index) {return const ListTile( // 使用constleading: Icon(Icons.star),title: Text('固定内容'),);
}为列表项添加key:
dart
itemBuilder: (context, index) {return ProductItem(key: ValueKey(products[index].id), // 为动态列表项添加keyproduct: products[index],);
}保持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');@overrideWidget 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: '编程',),// 可以添加更多新闻...];@overrideWidget 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中的导航与路由,让你能够在不同页面之间跳转,构建真正的多页面应用。
