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

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);
      });
    });
  }

问题根源

  1. 循环触发逻辑:监听器回调中直接调用fetchUsersAndPosts(false),而该方法又会再次执行fetchUsers()和fetchPosts(),触发notifyListeners(),形成无限循环。
  2. 错误的状态更新方式:PostsDataProvider的fetchPosts()在forEach循环中每次都覆盖postsMap并调用通知,不仅会多次触发监听,还会导致后获取的用户帖子覆盖之前的结果。
  3. 滥用static变量:用static变量存储状态会导致全局共享,不符合ChangeNotifier实例化状态管理的设计,容易引发状态混乱。
  4. 频繁调用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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.18 22:35:20