Flutter使用FutureBuilder渲染API返回数据时实现懒加载的方法咨询
Flutter 分页懒加载实现方案(原FutureBuilder全量加载改造)
改造核心思路
- 废弃原有一次性请求所有分页数据的
getPublicationAll方法,改为按页按需加载 - 在State中维护本地数据列表、当前加载页码、总页数、加载状态等本地变量,替代FutureBuilder的一次性异步状态
- 为GridView添加滚动监听器,当用户滚动到列表底部时自动触发下一页数据请求
- 加载下一页时在列表底部显示加载中提示,无更多数据时隐藏加载提示
完整改造后代码
import 'dart:convert'; import 'package:flutter/material.dart'; import 'package:statistik_malang/constants.dart'; import 'package:statistik_malang/details_screen.dart'; import 'package:statistik_malang/model_publikasi.dart'; import 'package:http/http.dart' as http; void main() { runApp(const PublikasiSecondPage()); } // 类名改为大驼峰,符合Dart代码规范 class PublikasiSecondPage extends StatefulWidget { const PublikasiSecondPage({Key? key}) : super(key: key); @override _PublikasiSecondPageState createState() => _PublikasiSecondPageState(); } class _PublikasiSecondPageState extends State<PublikasiSecondPage> { // 存储已经加载完成的所有数据 List<Publikasimod> publications = []; // 当前加载到的页码,初始为1 int currentPage = 1; // 接口返回的总页数,初始为0,第一页加载完成后赋值 int totalPages = 0; // 是否正在加载数据,防止重复请求 bool isLoading = false; // 滚动控制器,监听滚动位置 final ScrollController _scrollController = ScrollController(); @override void initState() { super.initState(); // 初始化加载第一页数据 _loadPageData(page: 1); // 添加滚动监听 _scrollController.addListener(_onScroll); } @override void dispose() { // 销毁控制器,防止内存泄漏 _scrollController.dispose(); super.dispose(); } // 滚动监听回调 void _onScroll() { // 已经滚动到底部、还有更多数据、且当前没有在加载中,才触发下一页加载 if (_scrollController.position.pixels >= _scrollController.position.maxScrollExtent * 0.9 && currentPage < totalPages && !isLoading) { _loadPageData(page: currentPage + 1); } } // 加载指定页码的数据 Future<void> _loadPageData({required int page}) async { setState(() { isLoading = true; }); try { // 替换为你的真实接口地址,注意拼接page参数 String apiURL = "link of api?page=$page"; var apiResult = await http.get(Uri.parse(apiURL)); var jsonObject = json.decode(apiResult.body); List<dynamic> responseData = (jsonObject as Map<String, dynamic>)["data"]; // 第一页加载时要拿到总页数 if (page == 1) { totalPages = responseData[0]["pages"]; publications.clear(); } // 解析当前页的10条数据 List<dynamic> currentPageList = responseData[1]; for (var item in currentPageList) { publications.add(Publikasimod.createPublikasimod(item)); } setState(() { currentPage = page; }); } catch (e) { // 加载失败提示,可以根据需求自定义 ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text("加载失败,请检查网络连接")), ); } finally { setState(() { isLoading = false; }); } } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: const Text("Daftar Publikasi Terbaru"), ), body: _buildBody(), ); } Widget _buildBody() { // 初始加载第一页时的加载状态 if (publications.isEmpty && isLoading) { return const Center(child: CircularProgressIndicator()); } // 加载完成但无数据 if (publications.isEmpty) { return const Center(child: Text("暂无数据")); } // 数据加载完成,渲染GridView return Column( crossAxisAlignment: CrossAxisAlignment.start, children: <Widget>[ Expanded( child: Padding( padding: const EdgeInsets.symmetric(horizontal: kDefaultPaddin, vertical: 20), child: GridView.builder( controller: _scrollController, // 最后多一个item,用来显示加载中或者无更多数据提示 itemCount: publications.length + 1, gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( crossAxisCount: 2, mainAxisSpacing: kDefaultPaddin, crossAxisSpacing: kDefaultPaddin, childAspectRatio: 0.55, ), itemBuilder: (context, index) { // 到了最后一个item,显示加载状态 if (index == publications.length) { return _buildLoadMoreWidget(); } // 渲染正常的书籍条目 final item = publications[index]; return ItemBuku( id: 1, title: item.title, cover: item.cover, date: item.rl_date, press: () => Navigator.push( context, MaterialPageRoute( builder: (context) => DetailsScreen(product: item), ), ), ); }, ), ), ) ], ); } // 底部加载更多组件 Widget _buildLoadMoreWidget() { if (isLoading) { return const Padding( padding: EdgeInsets.all(16.0), child: Center(child: CircularProgressIndicator(strokeWidth: 2)), ); } if (currentPage >= totalPages) { return const Padding( padding: EdgeInsets.all(16.0), child: Center(child: Text("已加载全部内容")), ); } return const SizedBox.shrink(); } } // 类名改为大驼峰,符合Dart规范 class ItemBuku extends StatelessWidget { final String title; final String date; final String cover; final Function press; final int id; const ItemBuku({ Key? key, required this.title, required this.date, required this.id, required this.cover, required this.press, }) : super(key: key); @override Widget build(BuildContext context) { return GestureDetector( onTap: () => press.call(), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Expanded( child: Container( padding: const EdgeInsets.all(kDefaultPaddin), height: 230, width: 169, decoration: BoxDecoration( borderRadius: BorderRadius.circular(16), image: DecorationImage( fit: BoxFit.fill, image: NetworkImage(cover), ), ), ), ), Padding( padding: const EdgeInsets.symmetric(vertical: kDefaultPaddin / 4), child: Text( title, style: const TextStyle(fontWeight: FontWeight.bold), ), ), Text( date, style: Theme.of(context).textTheme.caption, ) ], ), ); } }
注意事项
- 请将代码中
link of api?page=$page替换为你的真实接口地址,确保分页参数和接口要求一致 - 代码中已经按照Dart官方规范修改了类名命名(大驼峰),如果你的项目中其他地方引用了原有类名,请注意同步修改
- 滚动触发下一页的阈值设置为滚动到90%位置就触发,你可以根据实际使用体验调整
_onScroll方法中的比例值
内容的提问来源于stack exchange,提问作者lukman azhari
相关产品推荐
相关产品推荐

