ChangeNotifier为何陷入无限更新循环?
解决ChangeNotifier无限通知导致的循环重载问题
问题背景
我有两个继承自ChangeNotifier的数据提供者类,每个类中都有获取数据的函数,函数末尾会调用notifyListeners()通知页面数据变更。但目前监听器会被无限触发,导致重载循环、加载指示器无法消失、屏幕冻结。
数据提供者代码
class UsersDataProvider extends ChangeNotifier { UsersDataProvider() : super(); static Map<int, QueryDocumentSnapshot<Object?>> usersMap = {}; Future<void> fetchUsers() async { final userRef = FirebaseFirestore.instance.collection('users'); final QuerySnapshot result = await userRef.get(); final docs = result.docs.asMap(); usersMap = docs; print(usersMap.length); notifyListeners(); } } class PostsDataProvider extends ChangeNotifier { PostsDataProvider() : super(); static Map<int, QueryDocumentSnapshot<Object?>> postsMap = {}; Future<void> fetchPosts() async { UsersDataProvider.usersMap.forEach((index, resultValue) async { final postsRef = FirebaseFirestore.instance .collection('users') .doc(resultValue.id) .collection('posts'); final QuerySnapshot postsResult = await postsRef.get(); final postDocs = postsResult.docs.asMap(); postsMap = postDocs; print('Post map: ${postsMap.length}'); notifyListeners(); }); } }
添加监听器与重载数据代码
Future<void> fetchUsersAndPosts(bool initial) async { if (!initial) { setState(() { postsLoading = true; }); usersDataProvider.fetchUsers(); postsDataProvider.fetchPosts(); } if (initial) { usersDataProvider.addListener(() { print('changed'); setState(() { fetchUsersAndPosts(false); }); }); } if (initial) { postsDataProvider.addListener(() { setState(() { fetchUsersAndPosts(false); }); }); } UsersDataProvider.usersMap.forEach((index, value) async { List<Post> posts = []; PostsDataProvider.postsMap.forEach((index, value) { final post = Post.fromJson(value.data() as Map<String, dynamic>); posts.add(post); setState(() {}); if (posts.length == PostsDataProvider.postsMap.length) { setState(() { postsList = posts; postsList.sort((a, b) { return b.date.compareTo(a.date); }); postsLoading = false; }); } }); final profileInfo = ProfileInfoObject.fromJson(value.data() as Map<String, dynamic>); Profile profile = Profile(profileInfo, postsList.where((p) => p.uid == value.id).toList()); UserSearchResult user = (UserSearchResult(profile, value.id)); if (usersList.where((u) => u.uid == user.uid).toList().isEmpty) { setState(() { usersList.add(user); }); } }); setState(() { postsList.sort((a, b) { return b.date.compareTo(a.date); }); }); }
问题根源
- 循环触发逻辑:监听器回调中直接调用
fetchUsersAndPosts(false),而该方法又会再次执行fetchUsers()和fetchPosts(),触发notifyListeners(),形成无限循环。 - 错误的状态更新方式:
PostsDataProvider的fetchPosts()在forEach循环中每次都覆盖postsMap并调用通知,不仅会多次触发监听,还会导致后获取的用户帖子覆盖之前的结果。 - 滥用static变量:用static变量存储状态会导致全局共享,不符合
ChangeNotifier实例化状态管理的设计,容易引发状态混乱。 - 频繁调用setState:在数据遍历过程中多次调用
setState,加剧UI重载压力,同时逻辑上依赖循环内的状态更新,容易出错。
修复方案
1. 修正数据提供者类
去掉static变量,改用实例变量存储状态,同时优化fetchPosts的逻辑,一次性收集所有用户帖子后再更新状态:
class UsersDataProvider extends ChangeNotifier { Map<int, QueryDocumentSnapshot<Object?>> usersMap = {}; Future<void> fetchUsers() async { final userRef = FirebaseFirestore.instance.collection('users'); final QuerySnapshot result = await userRef.get(); usersMap = result.docs.asMap(); print(usersMap.length); notifyListeners(); } } class PostsDataProvider extends ChangeNotifier { // 按用户ID存储帖子,避免覆盖 Map<String, List<QueryDocumentSnapshot<Object?>>> postsMap = {}; Future<void> fetchPosts(List<QueryDocumentSnapshot<Object?>> users) async { final tempPostsMap = <String, List<QueryDocumentSnapshot<Object?>>>{}; for (final userDoc in users) { final postsRef = FirebaseFirestore.instance .collection('users') .doc(userDoc.id) .collection('posts'); final QuerySnapshot postsResult = await postsRef.get(); tempPostsMap[userDoc.id] = postsResult.docs; } postsMap = tempPostsMap; print('Post map: ${postsMap.length}'); notifyListeners(); } }
2. 重构数据获取与UI更新逻辑
移除监听器中的循环触发逻辑,让监听器仅负责UI刷新;同时统一处理数据,减少不必要的setState调用:
Future<void> fetchUsersAndPosts(bool initial) async { if (initial) { // 初始化时仅添加监听器,负责UI刷新 usersDataProvider.addListener(() => setState(() {})); postsDataProvider.addListener(() => setState(() {})); // 初始加载数据 setState(() => postsLoading = true); await usersDataProvider.fetchUsers(); await postsDataProvider.fetchPosts(usersDataProvider.usersMap.values.toList()); setState(() => postsLoading = false); } else { // 刷新时仅重新获取数据 setState(() => postsLoading = true); await usersDataProvider.fetchUsers(); await postsDataProvider.fetchPosts(usersDataProvider.usersMap.values.toList()); setState(() => postsLoading = false); } // 统一处理数据生成用户列表和帖子列表 final tempUsersList = <UserSearchResult>[]; final tempPostsList = <Post>[]; for (final userDoc in usersDataProvider.usersMap.values) { final profileInfo = ProfileInfoObject.fromJson(userDoc.data() as Map<String, dynamic>); final userPosts = postsDataProvider.postsMap[userDoc.id] ?? []; final posts = userPosts.map((doc) => Post.fromJson(doc.data() as Map<String, dynamic>)).toList(); tempPostsList.addAll(posts); final profile = Profile(profileInfo, posts); tempUsersList.add(UserSearchResult(profile, userDoc.id)); } // 排序后一次性更新UI tempPostsList.sort((a, b) => b.date.compareTo(a.date)); setState(() { usersList = tempUsersList; postsList = tempPostsList; }); }
关键注意事项
- 避免在
ChangeNotifier中使用static变量,确保每个实例状态独立。 - 监听器仅负责触发UI刷新,不要在回调中再次发起数据请求,防止循环触发。
- 数据获取完成后统一更新状态并调用
notifyListeners(),减少不必要的通知次数。 - 尽量在数据处理完成后一次性调用
setState,避免频繁UI重载影响性能。
内容的提问来源于stack exchange,提问作者Globe
相关产品推荐
相关产品推荐

