✅ 功能概览
- 分页加载(offset 模式)
- 删除单项
- 删除后补全数据
- 若删除后当前页为空,自动回退并重新加载
📦 完整代码(可直接运行)
import 'package:flutter/material.dart'
void main() => runApp(const MyApp())
class MyApp extends StatelessWidget {
const MyApp({super.key})
@override
Widget build(BuildContext context) {
return const MaterialApp(
title: 'Offset Paging Demo',
home: PaginatedListPage(),
)
}
}
class PaginatedListPage extends StatefulWidget {
const PaginatedListPage({super.key})
@override
State<PaginatedListPage> createState() => _PaginatedListPageState()
}
class _PaginatedListPageState extends State<PaginatedListPage> {
final List<Item> _items = []
int _offset = 0
final int _pageSize = 20
bool _isLoading = false
bool _hasMore = true
final FakeServer _fakeServer = FakeServer()
@override
void initState() {
super.initState()
_loadPage()
}
Future<void> _loadPage({bool isRefresh = false}) async {
if (_isLoading) return
setState(() => _isLoading = true)
if (isRefresh) {
_offset = 0
_items.clear()
_hasMore = true
}
final newItems = await fetchItems(_offset, _pageSize)
setState(() {
_items.addAll(newItems)
_offset += newItems.length
_isLoading = false
if (newItems.length < _pageSize) {
_hasMore = false
}
})
}
Future<void> _deleteItem(String id) async {
final success = await deleteItemApi(id)
if (!mounted || !success) return
setState(() {
_items.removeWhere((item) => item.id == id)
_offset--
})
// 如果不足 pageSize,尝试补1条
if (_hasMore && _items.length % _pageSize != 0) {
final patch = await fetchItems(_offset, 1)
if (patch.isNotEmpty) {
setState(() {
_items.add(patch.first)
_offset++
})
} else {
_hasMore = false
}
}
// 当前页数据全删光了
if (_items.isEmpty && _offset > 0) {
final newOffset = (_offset - _pageSize).clamp(0, _fakeServer.count())
_offset = newOffset
await _loadPage(isRefresh: true)
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('分页 + 删除(Offset 模式)')),
body: NotificationListener<ScrollNotification>(
onNotification: (scrollInfo) {
if (!_isLoading &&
_hasMore &&
scrollInfo.metrics.pixels == scrollInfo.metrics.maxScrollExtent) {
_loadPage()
}
return false
},
child: ListView.builder(
itemCount: _items.length + (_hasMore ? 1 : 0),
itemBuilder: (context, index) {
if (index == _items.length) {
return const Padding(
padding: EdgeInsets.all(16),
child: Center(child: CircularProgressIndicator()),
)
}
final item = _items[index]
return ListTile(
title: Text(item.title),
trailing: IconButton(
icon: const Icon(Icons.delete),
onPressed: () => _deleteItem(item.id),
),
)
},
),
),
)
}
// ================================
// API 接口(模拟后端)
// ================================
Future<List<Item>> fetchItems(int offset, int limit) async {
await Future.delayed(const Duration(milliseconds: 400))
return _fakeServer.getItemsByOffset(offset, limit)
.map((e) => Item(e.id, e.title))
.toList()
}
Future<bool> deleteItemApi(String id) async {
await Future.delayed(const Duration(milliseconds: 300))
return _fakeServer.deleteItem(id)
}
}
// ================================
// 数据结构
// ================================
class Item {
final String id
final String title
Item(this.id, this.title)
}
// ================================
// 模拟后端(FakeServer)
// ================================
class FakeServer {
final List<_RawItem> _db = List.generate(
53, (i) {
return _RawItem(id: '${i + 1}', title: 'Item
})
List<_RawItem> getItemsByOffset(int offset, int limit) {
if (offset >= _db.length) return []
final end = (offset + limit).clamp(0, _db.length)
return _db.sublist(offset, end)
}
bool deleteItem(String id) {
final index = _db.indexWhere((e) => e.id == id)
if (index == -1) return false
_db.removeAt(index)
return true
}
int count() => _db.length
}
class _RawItem {
final String id
final String title
_RawItem({required this.id, required this.title})
}
🧠 总结优势
| 功能点 | 是否支持 | 说明 |
|---|
| 分页加载 | ✅ | 使用 offset 避免分页错乱 |
| 删除数据 | ✅ | 删除后自动更新 UI |
| 删除补齐 | ✅ | 若不足 pageSize,则尝试补1条 |
| 删除整页 | ✅ | 自动回退上一页并刷新 |