掌握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 性能优化技巧
-
使用const构造函数:
dart
itemBuilder: (context, index) {
return const ListTile( // 使用const
leading: Icon(Icons.star),
title: Text('固定内容'),
);
}
-
为列表项添加key:
dart
itemBuilder: (context, index) {
return ProductItem(
key: ValueKey(products[index].id), // 为动态列表项添加key
product: 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');
@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中的导航与路由,让你能够在不同页面之间跳转,构建真正的多页面应用。
4790

被折叠的 条评论
为什么被折叠?



