什么是 ListView?
ListView 是 Flutter 中用于显示可滚动列表的组件,类似于 HTML 中的滚动容器。
用途:
- 显示长列表
- 聊天消息列表
- 商品列表
- 新闻列表
- 任何需要滚动的内容
ListView 的类型
1. ListView – 基础列表
ListView(
children: [
ListTile(title: Text('项目 1')),
ListTile(title: Text('项目 2')),
ListTile(title: Text('项目 3')),
],
)
2. ListView.builder – 动态列表(推荐)
ListView.builder(
itemCount: 100,
itemBuilder: (context, index) {
return ListTile(
title: Text('项目 $index'),
);
},
)
3. ListView.separated – 带分隔符的列表
ListView.separated(
itemCount: 20,
itemBuilder: (context, index) {
return ListTile(
title: Text('项目 $index'),
);
},
separatorBuilder: (context, index) {
return Divider(); // 分隔线
},
)
4. ListView.custom – 自定义列表
ListView.custom(
childrenDelegate: SliverChildBuilderDelegate(
(context, index) {
return ListTile(title: Text('项目 $index'));
},
childCount: 20,
),
)
常用属性
1. 滚动方向 scrollDirection
// 垂直滚动(默认)
ListView(
scrollDirection: Axis.vertical,
children: [...],
)
// 水平滚动
ListView(
scrollDirection: Axis.horizontal,
children: [...],
)
2. 反向滚动 reverse
ListView(
reverse: true, // 从底部开始
children: [...],
)
3. 内边距 padding
ListView(
padding: EdgeInsets.all(10),
children: [...],
)
4. 物理效果 physics
// 总是可滚动
ListView(
physics: AlwaysScrollableScrollPhysics(),
children: [...],
)
// 永不滚动
ListView(
physics: NeverScrollableScrollPhysics(),
children: [...],
)
// 弹性滚动(iOS 风格)
ListView(
physics: BouncingScrollPhysics(),
children: [...],
)
// 夹紧滚动(Android 风格)
ListView(
physics: ClampingScrollPhysics(),
children: [...],
)
ListTile 列表项
基础用法
ListTile(
leading: Icon(Icons.person), // 左侧图标
title: Text('标题'), // 标题
subtitle: Text('副标题'), // 副标题
trailing: Icon(Icons.chevron_right), // 右侧图标
onTap: () {
print('点击了列表项');
},
)
完整属性
ListTile(
leading: CircleAvatar(
child: Icon(Icons.person),
),
title: Text('张三'),
subtitle: Text('Flutter 开发工程师'),
trailing: Icon(Icons.chevron_right),
isThreeLine: false, // 是否三行
dense: false, // 是否紧凑
enabled: true, // 是否启用
selected: false, // 是否选中
onTap: () {},
onLongPress: () {},
)
实战案例
案例1:简单列表
ListView.builder(
itemCount: 20,
itemBuilder: (context, index) {
return ListTile(
leading: CircleAvatar(
child: Text('${index + 1}'),
),
title: Text('列表项 ${index + 1}'),
subtitle: Text('这是副标题'),
trailing: Icon(Icons.chevron_right),
onTap: () {
print('点击了第 ${index + 1} 项');
},
);
},
)
案例2:联系人列表
class Contact {
final String name;
final String phone;
final String avatar;
Contact({required this.name, required this.phone, required this.avatar});
}
class ContactList extends StatelessWidget {
final List<Contact> contacts = [
Contact(name: '张三', phone: '138****0001', avatar: 'https://example.com/1.jpg'),
Contact(name: '李四', phone: '138****0002', avatar: 'https://example.com/2.jpg'),
Contact(name: '王五', phone: '138****0003', avatar: 'https://example.com/3.jpg'),
];
Widget build(BuildContext context) {
return ListView.builder(
itemCount: contacts.length,
itemBuilder: (context, index) {
final contact = contacts[index];
return ListTile(
leading: CircleAvatar(
backgroundImage: NetworkImage(contact.avatar),
),
title: Text(contact.name),
subtitle: Text(contact.phone),
trailing: IconButton(
icon: Icon(Icons.phone),
onPressed: () {
print('拨打 ${contact.phone}');
},
),
onTap: () {
print('查看 ${contact.name} 的详情');
},
);
},
);
}
}
案例3:聊天列表
class ChatItem {
final String name;
final String message;
final String time;
final int unread;
ChatItem({
required this.name,
required this.message,
required this.time,
this.unread = 0,
});
}
class ChatList extends StatelessWidget {
final List<ChatItem> chats = [
ChatItem(name: '张三', message: '你好啊', time: '10:30', unread: 2),
ChatItem(name: '李四', message: '在吗?', time: '09:15', unread: 0),
ChatItem(name: '王五', message: '晚上一起吃饭', time: '昨天', unread: 1),
];
Widget build(BuildContext context) {
return ListView.builder(
itemCount: chats.length,
itemBuilder: (context, index) {
final chat = chats[index];
return ListTile(
leading: CircleAvatar(
child: Text(chat.name[0]),
),
title: Row(
children: [
Expanded(child: Text(chat.name)),
Text(
chat.time,
style: TextStyle(fontSize: 12, color: Colors.grey),
),
],
),
subtitle: Row(
children: [
Expanded(
child: Text(
chat.message,
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
),
if (chat.unread > 0)
Container(
padding: EdgeInsets.all(4),
decoration: BoxDecoration(
color: Colors.red,
shape: BoxShape.circle,
),
constraints: BoxConstraints(minWidth: 20, minHeight: 20),
child: Center(
child: Text(
'${chat.unread}',
style: TextStyle(color: Colors.white, fontSize: 12),
),
),
),
],
),
onTap: () {
print('打开与 ${chat.name} 的聊天');
},
);
},
);
}
}
案例4:商品列表
class Product {
final String name;
final String image;
final double price;
final String description;
Product({
required this.name,
required this.image,
required this.price,
required this.description,
});
}
class ProductList extends StatelessWidget {
final List<Product> products = [
Product(
name: 'iPhone 15 Pro',
image: 'https://example.com/iphone.jpg',
price: 7999,
description: '最新款 iPhone',
),
// 更多商品…
];
Widget build(BuildContext context) {
return ListView.builder(
itemCount: products.length,
itemBuilder: (context, index) {
final product = products[index];
return Card(
margin: EdgeInsets.all(10),
child: Padding(
padding: EdgeInsets.all(10),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
ClipRRect(
borderRadius: BorderRadius.circular(8),
child: Image.network(
product.image,
width: 100,
height: 100,
fit: BoxFit.cover,
),
),
SizedBox(width: 10),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
product.name,
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
),
),
SizedBox(height: 5),
Text(
product.description,
style: TextStyle(color: Colors.grey),
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
SizedBox(height: 10),
Row(
children: [
Text(
'¥${product.price}',
style: TextStyle(
fontSize: 20,
fontWeight: FontWeight.bold,
color: Colors.red,
),
),
Spacer(),
ElevatedButton(
onPressed: () {},
child: Text('购买'),
),
],
),
],
),
),
],
),
),
);
},
);
}
}
案例5:水平滚动列表
Container(
height: 200,
child: ListView.builder(
scrollDirection: Axis.horizontal,
itemCount: 10,
itemBuilder: (context, index) {
return Container(
width: 150,
margin: EdgeInsets.all(10),
decoration: BoxDecoration(
color: Colors.blue,
borderRadius: BorderRadius.circular(10),
),
child: Center(
child: Text(
'卡片 $index',
style: TextStyle(color: Colors.white, fontSize: 18),
),
),
);
},
),
)
案例6:分组列表
class GroupedList extends StatelessWidget {
final Map<String, List<String>> groups = {
'A': ['Alice', 'Amy', 'Andrew'],
'B': ['Bob', 'Betty', 'Brian'],
'C': ['Charlie', 'Cathy'],
};
Widget build(BuildContext context) {
return ListView.builder(
itemCount: _getTotalCount(),
itemBuilder: (context, index) {
final item = _getItem(index);
if (item['type'] == 'header') {
return Container(
padding: EdgeInsets.all(10),
color: Colors.grey[300],
child: Text(
item['data'],
style: TextStyle(
fontWeight: FontWeight.bold,
fontSize: 16,
),
),
);
} else {
return ListTile(
title: Text(item['data']),
);
}
},
);
}
int _getTotalCount() {
int count = 0;
groups.forEach((key, value) {
count += 1 + value.length; // 1 个标题 + n 个项目
});
return count;
}
Map<String, dynamic> _getItem(int index) {
int currentIndex = 0;
for (var entry in groups.entries) {
if (currentIndex == index) {
return {'type': 'header', 'data': entry.key};
}
currentIndex++;
for (var item in entry.value) {
if (currentIndex == index) {
return {'type': 'item', 'data': item};
}
currentIndex++;
}
}
return {'type': 'item', 'data': ''};
}
}
案例7:下拉刷新
class RefreshableList extends StatefulWidget {
_RefreshableListState createState() => _RefreshableListState();
}
class _RefreshableListState extends State<RefreshableList> {
List<String> items = List.generate(20, (index) => '项目 $index');
Future<void> _onRefresh() async {
await Future.delayed(Duration(seconds: 2));
setState(() {
items = List.generate(20, (index) => '新项目 $index');
});
}
Widget build(BuildContext context) {
return RefreshIndicator(
onRefresh: _onRefresh,
child: ListView.builder(
itemCount: items.length,
itemBuilder: (context, index) {
return ListTile(
title: Text(items[index]),
);
},
),
);
}
}
案例8:滑动删除
class DismissibleList extends StatefulWidget {
_DismissibleListState createState() => _DismissibleListState();
}
class _DismissibleListState extends State<DismissibleList> {
List<String> items = List.generate(20, (index) => '项目 $index');
Widget build(BuildContext context) {
return ListView.builder(
itemCount: items.length,
itemBuilder: (context, index) {
final item = items[index];
return Dismissible(
key: Key(item),
background: Container(
color: Colors.red,
alignment: Alignment.centerRight,
padding: EdgeInsets.only(right: 20),
child: Icon(Icons.delete, color: Colors.white),
),
direction: DismissDirection.endToStart,
onDismissed: (direction) {
setState(() {
items.removeAt(index);
});
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('$item 已删除')),
);
},
child: ListTile(
title: Text(item),
),
);
},
);
}
}
案例9:加载更多
class LoadMoreList extends StatefulWidget {
_LoadMoreListState createState() => _LoadMoreListState();
}
class _LoadMoreListState extends State<LoadMoreList> {
List<String> items = List.generate(20, (index) => '项目 $index');
bool isLoading = false;
ScrollController _scrollController = ScrollController();
void initState() {
super.initState();
_scrollController.addListener(_onScroll);
}
void _onScroll() {
if (_scrollController.position.pixels ==
_scrollController.position.maxScrollExtent) {
_loadMore();
}
}
Future<void> _loadMore() async {
if (isLoading) return;
setState(() {
isLoading = true;
});
await Future.delayed(Duration(seconds: 2));
setState(() {
int currentLength = items.length;
items.addAll(
List.generate(10, (index) => '项目 ${currentLength + index}'),
);
isLoading = false;
});
}
Widget build(BuildContext context) {
return ListView.builder(
controller: _scrollController,
itemCount: items.length + 1,
itemBuilder: (context, index) {
if (index == items.length) {
return isLoading
? Center(
child: Padding(
padding: EdgeInsets.all(20),
child: CircularProgressIndicator(),
),
)
: SizedBox.shrink();
}
return ListTile(
title: Text(items[index]),
);
},
);
}
void dispose() {
_scrollController.dispose();
super.dispose();
}
}
案例10:搜索列表
class SearchableList extends StatefulWidget {
_SearchableListState createState() => _SearchableListState();
}
class _SearchableListState extends State<SearchableList> {
List<String> allItems = List.generate(100, (index) => '项目 $index');
List<String> filteredItems = [];
TextEditingController _searchController = TextEditingController();
void initState() {
super.initState();
filteredItems = allItems;
_searchController.addListener(_onSearchChanged);
}
void _onSearchChanged() {
setState(() {
filteredItems = allItems
.where((item) =>
item.toLowerCase().contains(_searchController.text.toLowerCase()))
.toList();
});
}
Widget build(BuildContext context) {
return Column(
children: [
Padding(
padding: EdgeInsets.all(10),
child: TextField(
controller: _searchController,
decoration: InputDecoration(
hintText: '搜索…',
prefixIcon: Icon(Icons.search),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(10),
),
),
),
),
Expanded(
child: ListView.builder(
itemCount: filteredItems.length,
itemBuilder: (context, index) {
return ListTile(
title: Text(filteredItems[index]),
);
},
),
),
],
);
}
void dispose() {
_searchController.dispose();
super.dispose();
}
}
完整示例
import 'package:flutter/material.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
Widget build(BuildContext context) {
return MaterialApp(
home: ListViewDemo(),
);
}
}
class ListViewDemo extends StatelessWidget {
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('ListView 示例')),
body: ListView.separated(
itemCount: 20,
itemBuilder: (context, index) {
return ListTile(
leading: CircleAvatar(
child: Text('${index + 1}'),
),
title: Text('列表项 ${index + 1}'),
subtitle: Text('这是副标题'),
trailing: Icon(Icons.chevron_right),
onTap: () {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('点击了第 ${index + 1} 项')),
);
},
);
},
separatorBuilder: (context, index) {
return Divider(height: 1);
},
),
);
}
}
常见问题
1. ListView 在 Column 中不显示?
// ❌ 错误
Column(
children: [
ListView(...), // 会报错
],
)
// ✅ 解决1:使用 Expanded
Column(
children: [
Expanded(
child: ListView(...),
),
],
)
// ✅ 解决2:使用 shrinkWrap
Column(
children: [
ListView(
shrinkWrap: true,
physics: NeverScrollableScrollPhysics(),
children: [...],
),
],
)
2. 如何监听滚动?
ScrollController _controller = ScrollController();
void initState() {
super.initState();
_controller.addListener(() {
print('滚动位置: ${_controller.position.pixels}');
});
}
ListView(
controller: _controller,
children: [...],
)
3. 如何滚动到指定位置?
_controller.animateTo(
100.0,
duration: Duration(milliseconds: 500),
curve: Curves.easeInOut,
);
属性速查表
| children | 子组件列表 |
| scrollDirection | 滚动方向 |
| reverse | 是否反向 |
| controller | 滚动控制器 |
| physics | 滚动物理效果 |
| padding | 内边距 |
| shrinkWrap | 是否根据子组件调整大小 |
ListView.builder 参数
| itemCount | 项目数量 |
| itemBuilder | 项目构建器 |
ListTile 属性
| leading | 左侧组件 |
| title | 标题 |
| subtitle | 副标题 |
| trailing | 右侧组件 |
| onTap | 点击回调 |
| onLongPress | 长按回调 |
总结
ListView 的核心要点:
记住:
- 大量数据用 ListView.builder
- 在 Column 中使用需要 Expanded 或 shrinkWrap
- 使用 ListTile 快速构建列表项
- 善用 ScrollController 监听和控制滚动
ListView 是最常用的滚动组件,掌握它能构建各种列表界面!
网硕互联帮助中心





评论前必须登录!
注册