Flutter中StreamBuilder始终处于ConnectionState.waiting状态,无法加载Firebase数据问题排查
我仔细看了你的代码,找出了几个导致StreamBuilder一直显示加载状态的关键问题,咱们一个个来解决:
1. 最核心的问题:从未触发数据加载
你创建了LabController实例,但从来没调用过loadLab()方法,这就导致_streamController根本没有往流里添加任何数据,StreamBuilder自然一直处于等待状态。
你需要在合适的时机(比如页面初始化时)调用加载方法,比如在StatefulWidget的initState里:
@override void initState() { super.initState(); ctrlLab.loadLab(submenu); // 触发数据加载 }
2. Stream初始化错误
在LabController的构造函数里,你写了:
stream = _streamController.stream..toList();
这里的toList()会把Stream转换成一个Future<List<ItemLab>>,直接破坏了原有的广播流特性,导致你的stream变成了一个只完成一次的Future,而不是能持续发射数据的Stream。
修复方式很简单,直接赋值即可:
LabController() { _streamController = StreamController<List<ItemLab>>.broadcast(); stream = _streamController.stream; // 移除多余的toList() }
3. Async*生成器与StreamController混用冲突
你的loadLab方法标记为async*(Stream生成器),但同时又调用_streamController.add(conteudo)手动发送数据,这两种方式是冲突的,会导致流的逻辑混乱。
建议统一使用StreamController来管理流,把loadLab改成普通的async方法:
// 修改为普通async方法,不再用async*和yield Future<void> loadLab(String submenu, [int limit]) async { // 原有逻辑保留,把yield替换为_streamController.add(...) }
4. Firestore Stream处理不当
在处理Firestore的snapshots()流时,你用了.forEach(...),这会导致代码一直阻塞监听该流,而且无法正确控制数据的发射时机。正确的做法是用await for来遍历快照事件:
// 替换原来的forEach写法 await for (var element in databaseReference .collection("lab_${pathSlashless}_url") .snapshots()) { element.docs.forEach((f) { // 原有的文档处理逻辑 }); // 每次快照更新后,把最新数据发送到流 conteudo = checkDuplicateFolder(conteudo, submenu); _streamController.add(List.from(conteudo)); // 发送副本避免意外修改 }
5. 资源释放错误
当前的closeStream方法调用_streamController.stream.drain(),这只是清空流中的现有数据,但不会关闭StreamController,容易造成内存泄漏。正确的关闭方式是:
void closeStream() { _streamController.close(); // 直接关闭控制器 }
记得在页面销毁时调用这个方法,比如在dispose里:
@override void dispose() { ctrlLab.closeStream(); super.dispose(); }
class ItemLab { final String tipo; final Reference elemento; final String pastaVideo; final Map video; final String imageUrl; ItemLab({ this.tipo, this.elemento, this.video, this.imageUrl, this.pastaVideo }); } class LabController { final databaseReference = FirebaseFirestore.instance; final _storage = FirebaseStorage.instance; final picker = ImagePicker(); bool hasMore = false; Stream<List<ItemLab>> stream; StreamController<List<ItemLab>> _streamController; LabController() { _streamController = StreamController<List<ItemLab>>.broadcast(); stream = _streamController.stream; } void closeStream() { _streamController.close(); } Future<void> loadLab(String submenu, [int limit]) async { var parts = submenu.split('/'); var pathSlashless = parts[0].trim(); var subPathSlashless = parts.sublist(1).join('/').trim(); var snapshot = await _storage.ref().child("/${submenu}"); var retorno = await snapshot.listAll(); List<ItemLab> conteudo = []; if(subPathSlashless.isEmpty || subPathSlashless == null){ retorno.prefixes.forEach((element) { conteudo.add( ItemLab( tipo: 'PASTA', elemento: element, ), ); }); } for(int i = 0; i < retorno.items.length ; i++){ var url = await retorno.items[i].getDownloadURL(); conteudo.add( ItemLab( tipo: 'FILE', elemento: retorno.items[i], imageUrl: url, ), ); if(limit != null){ if(conteudo.length > limit){ hasMore = true; _streamController.add(List.from(conteudo)); }else{ hasMore = false; print("less than 9"); } } } if(subPathSlashless.isNotEmpty){ print(subPathSlashless); List items; await for (var element in databaseReference .collection("lab_${pathSlashless}_url") .snapshots()) { element.docs.forEach((f) { if(f.data()['videos'] != null){ items == null ? items = f.data()['videos'] : items.addAll(f.data()['videos']); }; print("ITEMS :::: >>> ${items}"); }); conteudo.clear(); for(int i = 0; i < items.length; i ++){ conteudo.add( ItemLab( tipo: 'VIDEO', elemento: null, video: items[i], ), ); } _streamController.add(List.from(conteudo)); } }else{ await for (var element in databaseReference .collection("lab_${pathSlashless}_url") .snapshots()) { conteudo.clear(); element.docs.forEach((f) { if(f.data().isNotEmpty){ if(f.data().keys.contains("videos")){ conteudo.add( ItemLab( tipo: 'PASTA', pastaVideo: findFolderName(f.reference.path) ), ); }else{ conteudo.add( ItemLab( tipo: 'VIDEO', elemento: null, video: f.data(), ), ); } } }); conteudo = checkDuplicateFolder(conteudo, submenu); _streamController.add(List.from(conteudo)); } } conteudo = checkDuplicateFolder(conteudo, submenu); _streamController.add(List.from(conteudo)); } // 假设你的findFolderName和checkDuplicateFolder方法在这里实现 String findFolderName(String path) { // 你的实现逻辑 return path.split('/').last; } List<ItemLab> checkDuplicateFolder(List<ItemLab> conteudo, String submenu) { // 你的去重逻辑 return conteudo; } }
- 发送数据到流时用
List.from(conteudo)发送副本,避免后续修改原列表导致流中的数据被意外篡改; - Firestore的
snapshots()会持续监听数据变化,所以当数据库内容更新时,StreamBuilder会自动刷新列表,正好满足你的需求; - 一定要在页面销毁时关闭StreamController,防止内存泄漏。
内容的提问来源于stack exchange,提问作者user14789593

