You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.06 23:51:01