Flutter中如何批量监听StreamSubscription实现分页加载?
地理定位帖子系统分页加载修复方案
原代码核心问题
- 每次调用
getResults都会重新创建Stream订阅,覆盖之前的streamSubscription,导致pause/resume逻辑完全失效 - 依赖实时Stream的
skip操作实现分页不可靠,Firestore地理查询Stream会实时推送全量更新,容易导致重复加载或数据错位 - 用
Future.delayed的方式是无效的hack,无法保证代码执行顺序
基于StreamController的稳定解决方案
我们可以通过本地缓存地理查询的完整结果列表,仅订阅一次Firestore的地理Stream,后续分页操作直接读取本地缓存,既保证低读取量,又能稳定实现load more功能。
重构后的完整代码:
class GeoFlutterFirePaginatedSystem extends StatefulWidget { const GeoFlutterFirePaginatedSystem({Key? key}) : super(key: key); @override State<GeoFlutterFirePaginatedSystem> createState() => _GeoFlutterFirePaginatedSystemState(); } class _GeoFlutterFirePaginatedSystemState extends State<GeoFlutterFirePaginatedSystem> { final _firestore = FirebaseFirestore.instance; final geo = Geoflutterfire(); GeoFirePoint myLocation = GeoFirePoint(13.7738153, 100.5667812); // 缓存地理查询的完整结果 List<DocumentSnapshot> _fullResults = []; // 用于展示的分页数据 List<String> resultList = []; bool hasData = true; bool _isLoading = false; StreamSubscription<List<DocumentSnapshot>>? _geoSubscription; final int _pageSize = 10; @override void initState() { super.initState(); _initGeoStream(); } // 仅订阅一次地理查询Stream,缓存全量结果 void _initGeoStream() { final geoStream = geo.collection(collectionRef: _firestore.collection('Listings')).within( center: myLocation, radius: 1, field: 'location', strictMode: true, ); _geoSubscription = geoStream.listen((List<DocumentSnapshot> documents) { setState(() { _fullResults = documents; // 初始化加载第一页数据 if (resultList.isEmpty) { _loadNextPage(); } }); }); } // 加载下一页数据 void _loadNextPage() { if (_isLoading || !hasData) return; setState(() => _isLoading = true); final startIndex = resultList.length; final endIndex = startIndex + _pageSize; // 从缓存中截取对应页码的数据 final nextBatch = _fullResults.sublist( startIndex, endIndex > _fullResults.length ? _fullResults.length : endIndex ); if (nextBatch.isEmpty) { setState(() { hasData = false; _isLoading = false; }); return; } // 转换为展示用的数据格式 final formattedData = nextBatch.map((doc) { final data = doc.data() as Map<String, dynamic>; return data['query'].toString(); }).toList(); setState(() { resultList.addAll(formattedData); _isLoading = false; // 返回数据不足一页,标记无更多数据 if (nextBatch.length < _pageSize) { hasData = false; } }); } @override void dispose() { _geoSubscription?.cancel(); super.dispose(); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( actions: [ if (hasData) ...[ InkWell( onTap: _loadNextPage, borderRadius: BorderRadius.circular(100), child: Container( width: 100, height: 40, alignment: Alignment.center, child: _isLoading ? const CircularProgressIndicator(size: 20) : const Text("加载更多"), ), ), ] else ...[ const SizedBox( width: 100, height: 40, child: Center(child: Text("没有更多数据")), ), ] ], ), body: SingleChildScrollView( child: Padding( padding: const EdgeInsets.all(16.0), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text("结果(${resultList.length})", style: const TextStyle(fontSize: 14, color: Colors.black)), const SizedBox(height: 15), ListView.builder( physics: const NeverScrollableScrollPhysics(), shrinkWrap: true, itemCount: resultList.length, itemBuilder: (context, index) { return Container( margin: const EdgeInsets.only(bottom: 10), padding: const EdgeInsets.only(bottom: 10), decoration: const BoxDecoration( border: Border(bottom: BorderSide(width: 1, color: Colors.black)), ), child: Text(resultList[index]), ); }, ), ], ), ), ), ); } }
关键逻辑说明
- 单次订阅,低读取量:仅在初始化时订阅一次地理查询Stream,后续所有分页操作都基于本地缓存的全量数据,避免重复请求Firestore
- 本地分页稳定可靠:每次加载更多时,直接从缓存的结果列表中截取对应片段,不会因为实时更新导致数据混乱
- 状态控制严谨:用
_isLoading防止重复触发加载,用hasData准确标记是否还有更多数据 - 支持实时更新:如果地理区域内的帖子有新增/修改,缓存会自动同步最新数据,分页逻辑也会基于最新列表执行(若不需要实时更新,可在首次获取结果后取消
_geoSubscription)
内容的提问来源于stack exchange,提问作者Uraam Asif
相关产品推荐
相关产品推荐

