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

Flutter应用导航时触发setState报错及首页过渡黑屏问题求助

解决Flutter中setState构建时调用错误及首页卡顿问题

我在Flutter首页编写了showData函数用于展示数据,但遇到两个核心问题:

  • 导航至首页或切换页面时,触发setState() or markNeedsBuild() called during build错误
  • 首页过渡完成后出现明显卡顿甚至黑屏现象

原代码:

void showData() async {
  final homeLoadingProvider =
      Provider.of<HomeLoadingProvider>(context, listen: false);
  homeLoadingProvider.setLoading(true); // Set loading to true initially

  final processedLists = Set<String>();
  // Check if there is any data in the 'GroceryLists' node initially
  await databaseReference.child('GroceryLists').get().then((snapshot) async {
    if (snapshot.value == null) {
      homeLoadingProvider.setLoading(false);
      return; // Return early if there's no data
    }
  });
  // Get the current user
  User? user = FirebaseAuth.instance.currentUser;
  //print('user : ${user}');

  // Check if the user is logged in and get their UID
  if (user != null) {
    String currentUid = user.uid;
    //print('currentUid : ${currentUid}');
    databaseReference
        .child('GroceryLists')
        .onChildAdded
        .listen((event) async {
      try {
        var groceryListKey = event.snapshot.key;
        var value = event.snapshot.value;

        if (value is Map<dynamic, dynamic>) {
          Map<String, dynamic> groceryListValue =
              Map<String, dynamic>.from(value);
          String? listName = groceryListValue['listName'] as String?;
          String? listId = groceryListValue['id'] as String?;
          String? userId =
              groceryListValue['Members'][currentUid]['userId'] as String?;
          // print('userId inside Members node : ${userId}');

          if (listName != null &&
              listId != null &&
              userId == currentUid && // Check if userId matches currentUid
              !processedLists.contains(listName)) {
            processedLists.add(listName);
            var itemValueMap = await fetchItemsForList(groceryListKey!);
            //print('itemValueMap : ${itemValueMap}');
            var colorID = groceryListValue['colorID'] ?? "";

            List<String> simpleItems = [];
            List<bool> isCheckedItems = [];

            if (itemValueMap.isNotEmpty) {
              itemValueMap.forEach((key, itemValue) {
                if (itemValue is Map<dynamic, dynamic> &&
                    itemValue['category'] is Map) {
                  var categoryItems = itemValue['category']['item'] as List?;
                  var categoryName =
                      itemValue['category']['categoryName'] as String?;

                  if (categoryItems != null && categoryName != "Done") {
                    for (var item in categoryItems) {
                      if (item is Map) {
                        String? itemName = item['itemName'] as String?;

                        if (itemName != null) {
                          simpleItems.add(itemName);

                          // Check if the item is marked as done in "Done" category
                          var doneCategory = itemValueMap.entries.firstWhere(
                              (entry) => (entry.value['category']
                                      ['categoryName'] ==
                                  "Done"),
                              orElse: () => MapEntry(
                                  null, {}) // returning dummy MapEntry here
                              );

                          bool isDone = false;
                          if (doneCategory.key != null) {
                            var doneItems = doneCategory.value['category']
                                ['item'] as List?;
                            var doneItem = doneItems?.firstWhere(
                                (doneItem) =>
                                    doneItem['itemName'] == itemName,
                                orElse: () => null);
                            isDone = doneItem != null &&
                                doneItem['isDone'] == true;
                          }

                          isCheckedItems.add(isDone);
                        }
                      }
                    }
                  }
                }
              });
              String? createdAtString =
                  groceryListValue['createdAt'] as String?;
              DateTime? createdAt = createdAtString != null
                  ? DateTime.parse(createdAtString)
                  : DateTime.now();
              if (simpleItems.length == isCheckedItems.length) {
                groceryLists.add(GroceryList(
                  listName: listName,
                  items: simpleItems,
                  colorID: colorID,
                  id: listId,
                  isChecked: isCheckedItems,
                  createdAt: createdAt, // Add this line
                ));
                groceryLists.sort((a, b) => b.createdAt.compareTo(
                    a.createdAt)); // sorting in descending order of createdAt
                setState(() {});
              }
            }
          }
        }
        // Set isLoading to false when the processing for this child is done.
        homeLoadingProvider.setLoading(false);
      } catch (e) {
        print("Error while processing grocery list data: $e");
        homeLoadingProvider.setLoading(false);
      }
    });
  } else {
    print('user has not logged in till now');
  }
}

Future<Map<dynamic, dynamic>> fetchItemsForList(String groceryListKey) async {
  var itemsSnapshot = await databaseReference
      .child('GroceryLists/$groceryListKey/items')
      .get();
  if (itemsSnapshot.value is Map<dynamic, dynamic>) {
    return itemsSnapshot.value as Map<dynamic, dynamic>;
  }
  return {};
}

问题原因分析

  1. setState构建时调用错误:

    • 如果showData在Widget的build方法中直接调用,内部的setState或Provider状态更新会触发于Widget构建过程中,违反Flutter构建规则。
    • onChildAdded回调可能在Widget未完成构建时触发,导致状态更新操作干扰构建流程。
  2. 首页卡顿/黑屏:

    • 数据解析、遍历、排序等复杂逻辑全部在UI线程执行,阻塞渲染流程。
    • 每处理一个列表项就调用一次setState,导致Widget频繁重建,加重UI线程负担。
    • 未管理Firebase监听的订阅,可能导致重复处理数据和内存泄漏。

解决方案

  1. 避免构建期间执行状态更新:

    • 在initState中调用showData,或通过WidgetsBinding.instance.addPostFrameCallback延迟到构建完成后执行。
    • 所有状态更新操作(setState、Provider状态变更)都通过addPostFrameCallback延迟执行。
  2. 优化数据处理,释放UI线程:

    • 将复杂数据解析逻辑移到后台线程,使用compute函数处理。
    • 批量处理数据后一次性调用setState,避免频繁重建。
  3. 管理Firebase监听订阅:

    • 保存onChildAdded的StreamSubscription,在Widget销毁时取消订阅,防止内存泄漏。
    • 调整加载状态逻辑,所有数据处理完成后再更新loading状态,避免状态频繁切换。

修改后的代码

late StreamSubscription _groceryListSubscription;
final Set<String> processedLists = {};

@override
void initState() {
  super.initState();
  // 延迟到构建完成后调用,避免干扰build流程
  WidgetsBinding.instance.addPostFrameCallback((_) {
    showData();
  });
}

@override
void dispose() {
  // 取消Firebase监听,防止内存泄漏
  _groceryListSubscription.cancel();
  super.dispose();
}

void showData() async {
  final homeLoadingProvider = Provider.of<HomeLoadingProvider>(context, listen: false);
  // 延迟设置loading状态
  WidgetsBinding.instance.addPostFrameCallback((_) {
    homeLoadingProvider.setLoading(true);
  });

  final snapshot = await databaseReference.child('GroceryLists').get();
  if (snapshot.value == null) {
    WidgetsBinding.instance.addPostFrameCallback((_) {
      homeLoadingProvider.setLoading(false);
    });
    return;
  }

  final User? user = FirebaseAuth.instance.currentUser;
  if (user == null) {
    print('用户未登录');
    WidgetsBinding.instance.addPostFrameCallback((_) {
      homeLoadingProvider.setLoading(false);
    });
    return;
  }

  final String currentUid = user.uid;
  // 保存订阅对象,方便后续取消
  _groceryListSubscription = databaseReference
      .child('GroceryLists')
      .onChildAdded
      .listen((event) async {
    try {
      final groceryListKey = event.snapshot.key;
      final value = event.snapshot.value;

      if (value is Map<dynamic, dynamic>) {
        final groceryListValue = Map<String, dynamic>.from(value);
        final String? listName = groceryListValue['listName'] as String?;
        final String? listId = groceryListValue['id'] as String?;
        final String? userId = groceryListValue['Members'][currentUid]['userId'] as String?;

        if (listName != null && listId != null && userId == currentUid && !processedLists.contains(listName)) {
          processedLists.add(listName);
          final itemValueMap = await fetchItemsForList(groceryListKey!);
          final colorID = groceryListValue['colorID'] ?? "";
          final String? createdAtString = groceryListValue['createdAt'] as String?;
          final DateTime createdAt = createdAtString != null ? DateTime.parse(createdAtString) : DateTime.now();

          // 后台线程处理数据解析,避免阻塞UI
          final processedListData = await compute(processGroceryListItems, {
            'itemValueMap': itemValueMap,
            'listName': listName,
            'colorID': colorID,
            'listId': listId,
            'createdAt': createdAt,
          });

          if (processedListData != null) {
            // 延迟更新UI
            WidgetsBinding.instance.addPostFrameCallback((_) {
              groceryLists.add(processedListData);
              groceryLists.sort((a, b) => b.createdAt.compareTo(a.createdAt));
              setState(() {});
            });
          }
        }
      }

      // 所有数据处理完成后更新加载状态
      WidgetsBinding.instance.addPostFrameCallback((_) {
        homeLoadingProvider.setLoading(false);
      });
    } catch (e) {
      print("处理购物列表数据出错: $e");
      WidgetsBinding.instance.addPostFrameCallback((_) {
        homeLoadingProvider.setLoading(false);
      });
    }
  });
}

// 后台线程处理函数,必须为顶层或静态函数
GroceryList? processGroceryListItems(Map<String, dynamic> params) {
  final itemValueMap = params['itemValueMap'] as Map<dynamic, dynamic>;
  final listName = params['listName'] as String;
  final colorID = params['colorID'] as String;
  final listId = params['listId'] as String;
  final createdAt = params['createdAt'] as DateTime;

  final List<String> simpleItems = [];
  final List<bool> isCheckedItems = [];

  if (itemValueMap.isNotEmpty) {
    itemValueMap.forEach((key, itemValue) {
      if (itemValue is Map<dynamic, dynamic> && itemValue['category'] is Map) {
        final categoryItems = itemValue['category']['item'] as List?;
        final categoryName = itemValue['category']['categoryName'] as String?;

        if (categoryItems != null && categoryName != "Done") {
          for (var item in categoryItems) {
            if (item is Map) {
              final String? itemName = item['itemName'] as String?;
              if (itemName != null) {
                simpleItems.add(itemName);

                // 检查是否在Done分类中标记为完成
                bool isDone = false;
                final doneCategoryEntry = itemValueMap.entries.firstWhere(
                      (entry) => entry.value['category']['categoryName'] == "Done",
                  orElse: () => MapEntry(null, {}),
                );

                if (doneCategoryEntry.key != null) {
                  final doneItems = doneCategoryEntry.value['category']['item'] as List?;
                  final doneItem = doneItems?.firstWhere(
                        (doneItem) => doneItem['itemName'] == itemName,
                    orElse: () => null,
                  );
                  isDone = doneItem != null && doneItem['isDone'] == true;
                }

                isCheckedItems.add(isDone);
              }
            }
          }
        }
      }
    });
  }

  if (simpleItems.length == isCheckedItems.length) {
    return GroceryList(
      listName: listName,
      items: simpleItems,
      colorID: colorID,
      id: listId,
      isChecked: isCheckedItems,
      createdAt: createdAt,
    );
  }
  return null;
}

Future<Map<dynamic, dynamic>> fetchItemsForList(String groceryListKey) async {
  final itemsSnapshot = await databaseReference
      .child('GroceryLists/$groceryListKey/items')
      .get();
  if (itemsSnapshot.value is Map<dynamic, dynamic>) {
    return itemsSnapshot.value as Map<dynamic, dynamic>;
  }
  return {};
}

额外优化建议

  • 改用FutureBuilder或Provider管理数据状态,减少手动调用setState的次数。
  • 优化Firebase查询逻辑,直接查询当前用户的列表,避免遍历所有数据再过滤。
  • 实现分页加载,避免一次性加载大量数据导致UI阻塞。

内容的提问来源于stack exchange,提问作者Muhammad Adil

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.08 15:38:12