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

为何使用isLoaded标志仍触发'null check operator used on a null value'错误?

问题:红屏报错null check operator used on a null value(触发变量为listOfExercises)

一直出现临时红屏,提示错误信息null check operator used on a null value,已通过isLoaded标志控制页面渲染时机,但问题仍存在,触发错误的变量为listOfExercises,相关代码如下:

class _ListOfExercisesState extends State<ListOfExercises>
with SingleTickerProviderStateMixin {
late bool isLoaded = false;
List<Exercise>? exercises;
List<List<Exercise>>? listOfExercises;
late TabController _tabController;

@override
void initState() {
_tabController = TabController(length: 10, vsync: this);
getData();
super.initState();
}

 @override
Widget build(BuildContext context) {
SizeConfig().init(context);

return Scaffold(
  appBar: AppBar(
    title: Text(capitalize(widget.muscleName),
        style: const TextStyle(color: Colors.black)),
    centerTitle: true,
  ),
  body: Visibility(
    visible: isLoaded,
    replacement: const Center(child: CircularProgressIndicator()),
    child: Column(
      children: [
        Align(
          alignment: Alignment.centerLeft,
          child: TabBar(
            isScrollable: true,
            controller: _tabController,
            labelColor: Colors.black,
            unselectedLabelColor: Colors.grey,
            indicator: CircleTabIndicator(color: Colors.black, radius: 4),
            tabs: listOfExercises!
                .map((e) => Tab(text: e.first.equipment))
                .toList(),
          ),
        ),
        Expanded(
          child: SizedBox(
            width: double.maxFinite,
            height: double.infinity,
            child: TabBarView(
                controller: _tabController,
                children: buildTabViews(listOfExercises!)),
              ),
            )
          ],
        ),
      ),
    );
  }

  List<Widget> buildTabViews(List<List<Exercise>> test) {
    return test.map((e) {
      return ListView.builder(
        itemCount: e.length,
        itemBuilder: (context, index) => Card(
      margin: const EdgeInsets.all(6.0),
      shape:
          RoundedRectangleBorder(borderRadius: BorderRadius.circular(7.0)),
      child: ListTile(
        contentPadding: const EdgeInsets.all(8.0),
        shape: RoundedRectangleBorder(
            borderRadius: BorderRadius.circular(7.0)),
        onTap: () {
          showModalBottomSheet(
              shape: const RoundedRectangleBorder(
                  borderRadius:
                      BorderRadius.vertical(top: Radius.circular(25.0))),
              isScrollControlled: true,
              context: context,
              builder: (context) => buildBottomSheet(
                  SizeConfig.screenWidth,
                  SizeConfig.screenHeight,
                  context,
                  e[index].name,
                  e[index].gifUrl,
                  e[index].target,
                  e[index].equipment));
        },
        title: Text(
          e[index].name,
          style: const TextStyle(
              color: Colors.white,
              fontSize: 18,
              fontWeight: FontWeight.w800),
          textAlign: TextAlign.center,
        ),
        leading: ClipRRect(
          borderRadius: BorderRadius.circular(5),
          child: Image.network(
            e[index].gifUrl,
            loadingBuilder: (context, child, loadingProgress) {
              if (loadingProgress == null) return child;
              return const ClipOval(
                child: CircularProgressIndicator(
                  color: Colors.white,
                ),
              );
            },
          ),
        ),
        tileColor: Colors.blue,
      ),
    ),
  );
  }).toList();
 }

getData() async {
exercises = await ServicesExerciseDB().getExercises(widget.muscleName);

listOfExercises = exercisesByEquipment(exercises!);

if (listOfExercises != null) {
  setState(() {
    isLoaded = true;
  });
}
}

//divide the list of exercises list of lists separated by the type of equipment used
  List<List<Exercise>> exercisesByEquipment(List<Exercise> exercises) {
    Map<String, List<Exercise>> groupedExercises = {};

// Group exercises based on equipment
for (Exercise exercise in exercises) {
  if (!groupedExercises.containsKey(exercise.equipment)) {
    groupedExercises[exercise.equipment] = [];
  }
  groupedExercises[exercise.equipment]!.add(exercise);
}

List<List<Exercise>> separatedLists = groupedExercises.values.toList();
return separatedLists;
}

  String capitalize(String text) {
    if (text.isEmpty) return text; // Return empty string if input is empty
    return text[0].toUpperCase() + text.substring(1);
  }
}

问题排查

  1. TabController长度与实际tabs数量不匹配
    initState中固定将_tabController的length设为10,但实际TabBar的tabs数量由listOfExercises的长度动态决定。当两者长度不一致时,会导致Flutter框架内部状态异常,间接触发listOfExercises的空断言报错。

  2. exercises变量的空风险未处理
    getData中直接使用exercises!调用exercisesByEquipment,若ServicesExerciseDB().getExercises返回null,此处会直接抛出空指针异常,导致listOfExercises未被赋值,后续逻辑中断。

  3. 空断言的强依赖风险
    虽然isLoaded为true时listOfExercises理论上不为null,但Flutter的build可能因外部触发(如窗口 resize、父组件重建)重复执行,若listOfExercises在某个异常场景下被置空,!断言会直接触发报错。

解决方案

1. 动态初始化TabController

在获取到listOfExercises后再初始化TabController,确保长度与tabs数量一致:

getData() async {
  exercises = await ServicesExerciseDB().getExercises(widget.muscleName);
  // 处理数据为空的情况
  if (exercises == null || exercises!.isEmpty) {
    setState(() {
      // 可添加错误状态提示逻辑
    });
    return;
  }
  listOfExercises = exercisesByEquipment(exercises!);
  if (listOfExercises != null && listOfExercises!.isNotEmpty) {
    setState(() {
      // 动态设置TabController长度
      _tabController = TabController(length: listOfExercises!.length, vsync: this);
      isLoaded = true;
    });
  }
}

// 记得在dispose中释放控制器
@override
void dispose() {
  _tabController.dispose();
  super.dispose();
}

2. 替换空断言,增加空安全兜底

将listOfExercises!替换为空合并运算符,避免因意外空值触发报错:

// TabBar的tabs部分
tabs: (listOfExercises ?? []).map((e) => Tab(text: e.first.equipment)).toList(),
// TabBarView的children部分
children: buildTabViews(listOfExercises ?? []),

3. 增强exercisesByEquipment的鲁棒性

虽然当前方法不会返回null,但可明确返回非空列表,避免后续空检查:

List<List<Exercise>> exercisesByEquipment(List<Exercise> exercises) {
  Map<String, List<Exercise>> groupedExercises = {};
  for (Exercise exercise in exercises) {
    groupedExercises.putIfAbsent(exercise.equipment, () => []);
    groupedExercises[exercise.equipment]!.add(exercise);
  }
  return groupedExercises.values.toList();
}

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.22 03:47:20