Flutter中StreamBuilder+GoogleMap用setState致UI卡顿求解决方案
问题
Flutter应用集成GoogleMap后,通过StreamBuilder从Firebase Firestore的beets集合获取数据,遍历文档时调用异步方法fetchUserData获取用户信息,生成自定义Marker后用setState添加到markers集合。但每次调用setState都会导致UI严重卡顿,移除setState又无法在数据变更时更新地图Marker,不使用setState时应用流畅度正常,求最优解决方案。
核心问题分析
- 在StreamBuilder的
builder回调里使用forEach+async/await,每个Marker生成完成后都调用一次setState,频繁触发UI刷新,直接引发卡顿。 - 每次Stream推送新数据时,未清空旧的
markers集合,导致重复添加相同Marker,进一步恶化性能。 - 异步操作直接放在
builder中,违背了Flutter Widget构建的同步原则,会导致builder多次无意义重建。
最优解决方案
1. 批量异步处理Marker生成,一次性更新状态
把所有Marker的异步生成逻辑合并成一个批量任务,完成后一次性调用状态更新,避免多次刷新UI。
2. 清理旧数据,避免重复添加
每次Stream获取新数据时,先清空旧的markers集合,防止重复Marker积累。
3. 缓存用户数据,减少Firestore请求
对fetchUserData的结果进行缓存,避免相同用户信息重复请求Firestore,提升加载速度。
优化后的代码
批量生成Marker的方法
Future<Set<Marker>> generateMarkers(List<QueryDocumentSnapshot> docs) async { final Set<Marker> newMarkers = {}; // 遍历所有文档,批量生成Marker for (final docSnapshot in docs) { if (docSnapshot.data() is Map<String, dynamic>) { final beetData = docSnapshot.data() as Map<String, dynamic>; final geoPoint = beetData['coordinates'] as GeoPoint; final coordinates = LatLng(geoPoint.latitude, geoPoint.longitude); final email = beetData['email'] as String; // 优先从缓存取用户数据 UserModel? user = _userCache[email]; if (user == null) { user = await fetchUserData(email); if (user != null) { _userCache[email] = user; } } if (user != null) { final markerUser = await MarkerIcon.downloadResizePictureCircle( user.urlImage, size: 190, addBorder: true, borderColor: AppColors.whiteColor, borderSize: 24, ); newMarkers.add(Marker( markerId: MarkerId(docSnapshot.id), onTap: () => openInfoModalUser(context, user), position: coordinates, icon: markerUser, )); } } } return newMarkers; }
修改后的StreamBuilder
// 类中添加缓存变量 final Map<String, UserModel> _userCache = {}; Set<Marker> markers = {}; StreamBuilder<QuerySnapshot>( stream: FirebaseFirestore.instance .collection(CustomCollectionPath.beets) .where('email', isNotEqualTo: currentUser.email) .snapshots(), builder: (context, snapshot) { if (snapshot.hasData) { // 用FutureBuilder处理批量异步任务 return FutureBuilder<Set<Marker>>( future: generateMarkers(snapshot.data!.docs), builder: (context, markerSnapshot) { if (markerSnapshot.hasData) { // 一次性更新所有Marker markers = markerSnapshot.data!; return GoogleMap( onMapCreated: _onMapCreated, myLocationEnabled: true, buildingsEnabled: true, myLocationButtonEnabled: false, zoomControlsEnabled: false, markers: markers, mapToolbarEnabled: false, initialCameraPosition: _startPosition, ); } else if (markerSnapshot.hasError) { return Text('生成Marker时出错'); } else { // 显示加载状态,避免地图闪烁 return GoogleMap( onMapCreated: _onMapCreated, myLocationEnabled: true, buildingsEnabled: true, myLocationButtonEnabled: false, zoomControlsEnabled: false, markers: markers, // 保留旧Marker直到新的生成完成 mapToolbarEnabled: false, initialCameraPosition: _startPosition, ); } }, ); } else if (snapshot.hasError) { return Text('获取数据失败'); } else { return CircularProgressIndicator(); } }, ),
带缓存的fetchUserData
Future<UserModel?> fetchUserData(String email) async { // 先检查缓存 if (_userCache.containsKey(email)) { return _userCache[email]; } try { final userSnapshot = await FirebaseFirestore.instance .collection(CustomCollectionPath.userInfo) .doc(email) .get(); if (userSnapshot.exists) { final userData = userSnapshot.data() as Map<String, dynamic>; final user = UserModel( id: Random().nextInt(999999), name: userData['name'], urlImage: userData['imageUrl'], roleDescription: 'Role', description: userData['description'], icon: Icons.abc, color: Colors.red, ); // 存入缓存 _userCache[email] = user; return user; } else { return null; } } catch (e) { print('获取用户数据错误: $e'); return null; } }
额外优化建议
- 对Marker的图标进行本地缓存,避免每次都下载和裁剪图片,可以使用
cached_network_image库配合本地缓存实现。 - 如果
beets集合数据量较大,考虑分页加载Marker,避免一次性生成过多Marker导致内存占用过高。 - 可以使用
ValueNotifier或ChangeNotifier来管理markers状态,替代setState,进一步优化状态更新的效率。
内容的提问来源于stack exchange,提问作者joisberg
相关产品推荐
相关产品推荐

