Flutter基于sqflite如何实现列表分页懒加载(每次加载10条)
Flutter 结合sqflite实现列表分页懒加载方案
核心改造思路
传统全量查询1000条数据会造成页面启动卡顿、内存占用过高,我们通过数据库层分页查询+页面层滚动监听的组合实现需求:启动时加载10条,滚动靠近当前列表底部时自动追加加载下一批10条,直到所有数据加载完成。
第一步:改造DatabaseHelper,新增分页查询能力
你现有的queryAllRows方法会一次性拉取全表所有数据,完全不适合大数据量场景,需要在帮助类中新增支持偏移量、条数限制的查询方法:
// 新增分页查询方法 Future<List<Map<String, dynamic>>> queryPagedRows(int offset, int limit) async { Database db = await instance.database; // 必须按主键id排序,保证分页数据顺序稳定,避免重复/漏查 return await db.query( table, orderBy: '$colId ASC', limit: limit, offset: offset, ); }
附带修正你现有代码的一处逻辑bug:原
deleteEmp方法的参数错误,sqflite的delete方法第一个参数需要传入表名,而非拼好的SQL语句,修正后的写法:Future<int> deleteEmp(int id) async { Database db = await instance.database; return await db.delete(table, where: '$colId = ?', whereArgs: [id]); }
第二步:页面层实现懒加载状态管控
在_firstScreen的State类中,替换原有零散的状态变量,加入滚动控制器、加载锁、分页偏移量等必要状态:
- 新增状态定义
// 固定每页加载10条,匹配需求 static const int _pageSize = 10; // 滚动控制器,用于监听列表滚动位置 final ScrollController _scrollController = ScrollController(); // 加载锁:防止重复触发请求造成数据重复加载 bool _isLoading = false; // 当前已加载数据的偏移位置 int _loadedOffset = 0; // 数据库总数据量,用于判断是否加载完全部数据 int _totalDataCount = 0; List<Emp> empList = []; - 初始化与资源释放
把原来写在build方法里的懒加载判断逻辑移到initState,避免build触发时重复初始化:@override void initState() { super.initState(); // 首次进入页面加载初始数据 _initData(); // 绑定滚动监听 _scrollController.addListener(_scrollListener); } @override void dispose() { // 销毁时释放控制器,避免内存泄漏 _scrollController.dispose(); super.dispose(); } - 初始数据加载逻辑
启动时先查询数据库总条数,再加载第一页10条数据:Future<void> _initData() async { if (_isLoading) return; setState(() => _isLoading = true); // 查询全表总条数 _totalDataCount = await dbHelper.queryRowCount(); // 加载第一页10条数据 final firstPageRows = await dbHelper.queryPagedRows(0, _pageSize); empList.clear(); empList.addAll(firstPageRows.map((row) => Emp.fromMap(row))); _loadedOffset = empList.length; setState(() => _isLoading = false); } - 滚动监听逻辑
当列表滚动到距离底部200像素位置时,触发下一页加载,同时做好边界判断:void _scrollListener() { // 边界拦截:正在加载、已经加载完全部数据时不触发请求 if (_isLoading || _loadedOffset >= _totalDataCount) return; // 滚动位置距离底部不足200px时触发加载(阈值可根据实际UI调整) if (_scrollController.position.pixels >= _scrollController.position.maxScrollExtent - 200) { _loadNextPage(); } } - 下一页加载逻辑
从当前已加载的偏移位置开始查询10条新数据,追加到原有列表中,不要清空原有数据:Future<void> _loadNextPage() async { setState(() => _isLoading = true); // 按当前偏移量查询下一批数据 final nextPageRows = await dbHelper.queryPagedRows(_loadedOffset, _pageSize); empList.addAll(nextPageRows.map((row) => Emp.fromMap(row))); _loadedOffset = empList.length; setState(() => _isLoading = false); }
第三步:适配ListView渲染逻辑
改造原有getListView方法,绑定滚动控制器,在列表底部追加加载提示位:
ListView getListView() { return ListView.builder( controller: _scrollController, // itemCount在原有列表长度基础上+1,给底部加载提示预留位置 itemCount: empList.length + 1, itemBuilder: (BuildContext context, int position) { // 渲染列表底部的提示区域 if (position == empList.length) { // 已经加载完全部数据时不渲染提示 if (_loadedOffset >= _totalDataCount) { return SizedBox.shrink(); } // 加载中显示转圈提示 return const Padding( padding: EdgeInsets.all(16.0), child: Center(child: CircularProgressIndicator()), ); } // 正常渲染员工列表项 final emp = empList[position]; final _byteImage = Base64Decoder().convert(emp.path); return Card( color: Colors.white, elevation: 2.0, child: ListTile( leading: CircleAvatar( backgroundColor: Colors.white, child: Image.memory(_byteImage, fit: BoxFit.fill), ), title: Text(emp.name), subtitle: Text(emp.age), ), ); }, ); }
原有代码的其他适配调整
- 把原来的
updateListView方法替换为重置逻辑,给刷新按钮、新增员工后的列表刷新使用:void updateListView() async { _loadedOffset = 0; empList.clear(); await _initData(); } - 修正新增员工按钮的逻辑:原代码在
Navigator.push之后直接调用updateListView,由于路由跳转是阻塞的,这行代码根本不会执行。调整为等从新增页面返回后再触发刷新:onPressed: () async { debugPrint('add pressed'); // 等路由返回后再刷新列表 await Navigator.push(context, MaterialPageRoute(builder: (context) { return secondScreeen(); })); // 返回页面后重置加载第一页数据 updateListView(); }
内容的提问来源于stack exchange,提问作者Natsu Dragnal
相关产品推荐
相关产品推荐

