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

WidgetsBinding.addPostFrameCallback失效,Flutter启动时获取未渲染Widget位置崩溃

问题解决方案

核心问题分析

启动时触发空检查异常的根本原因是:你尝试在Widget尚未完成渲染时,通过GlobalKey获取其BuildContext。即使使用了addPostFrameCallback,如果回调时机不对(比如在Widget还未被添加到树中时调用),依然会拿到null的currentContext。同时你当前的代码并没有实际调用getOffset来计算位置,而是用了硬编码的坐标,这也偏离了原本的需求。

关键修复步骤

  1. 给getOffset增加空安全校验,避免直接使用!强制解包导致崩溃
  2. 在正确的时机计算偏移量:第一次渲染完成后计算初始位置,添加目标占位符后延迟计算其位置
  3. 用状态变量存储偏移量,替代硬编码坐标
  4. 避免使用indexOf获取索引(容易因重复Widget导致错误),改用asMap()遍历

修改后的完整代码

import 'package:flutter/material.dart';

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Animation Proof of Concept',
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(seedColor: Colors.black),
        useMaterial3: true,
      ),
      home: const MyHomePage(),
    );
  }
}

class MyHomePage extends StatefulWidget {
  const MyHomePage({super.key});

  @override
  State<MyHomePage> createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  final Container xo =
      Container(color: Colors.white, width: 65, height: 65, child: const Text("Xo"));
  final Container vo =
      Container(color: Colors.white, width: 65, height: 65, child: const Text("Vo"));

  List<Container> letters = [];
  List<Widget> targets = [];
  Map<int, GlobalKey> targetKeys = {};
  Map<int, GlobalKey> homeKeys = {};
  List<bool> isMovedList = [];
  // 存储每个单词的初始位置和目标位置
  List<Offset?> homeOffsets = [];
  List<Offset?> targetOffsets = [];

  @override
  void initState() {
    super.initState();
    letters = [vo];
    isMovedList = List.generate(letters.length, (_) => false);
    
    // 初始化GlobalKey
    targetKeys = {
      for (int i = 0; i < letters.length; i++)
        i: GlobalKey(debugLabel: "targetKey$i")
    };
    homeKeys = {
      for (int i = 0; i < letters.length; i++)
        i: GlobalKey(debugLabel: "homeKey$i")
    };
    
    // 初始化偏移量列表
    homeOffsets = List.generate(letters.length, (_) => null);
    targetOffsets = List.generate(letters.length, (_) => null);

    // 第一次渲染完成后计算初始位置(此时home区域的Widget已渲染)
    WidgetsBinding.instance.addPostFrameCallback((_) {
      setState(() {
        for (int i = 0; i < letters.length; i++) {
          homeOffsets[i] = getOffset(homeKeys[i]!);
        }
      });
    });
  }

  // 安全获取组件偏移量
  Offset? getOffset(GlobalKey key) {
    final BuildContext? context = key.currentContext;
    if (context == null) return null;
    
    final RenderBox? renderBox = context.findRenderObject() as RenderBox?;
    if (renderBox == null) return null;
    
    return renderBox.localToGlobal(Offset.zero);
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      backgroundColor: Colors.purple,
      body: Stack(
        children: [
          Column(
            children: [
              Expanded(
                child: Row(
                  mainAxisAlignment: MainAxisAlignment.center,
                  children: targets,
                ),
              ),
              Expanded(
                child: Row(
                  mainAxisAlignment: MainAxisAlignment.center,
                  children: letters.asMap().entries.map((entry) {
                    final index = entry.key;
                    return Opacity(
                      key: homeKeys[index],
                      opacity: 0.2,
                      child: entry.value,
                    );
                  }).toList(),
                ),
              )
            ],
          ),
          // 遍历单词并渲染可拖动的组件
          ...letters.asMap().entries.map((entry) {
            final index = entry.key;
            final letter = entry.value;
            
            // 根据状态选择当前要使用的偏移量
            final currentOffset = isMovedList[index] 
                ? targetOffsets[index] ?? Offset.zero 
                : homeOffsets[index] ?? Offset.zero;

            return AnimatedPositioned(
              left: currentOffset.dx,
              top: currentOffset.dy,
              duration: const Duration(milliseconds: 250),
              child: GestureDetector(
                onTap: () {
                  setState(() {
                    isMovedList[index] = !isMovedList[index];
                    
                    if (isMovedList[index]) {
                      // 添加目标占位符
                      targets.add(Container(
                        key: targetKeys[index],
                        color: Colors.red,
                        width: 65,
                        height: 65,
                        child: Center(
                          child: Text(
                            "T$index",
                            style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 20),
                          ),
                        ),
                      ));
                      // 等待占位符渲染完成后计算其位置
                      WidgetsBinding.instance.addPostFrameCallback((_) {
                        setState(() {
                          targetOffsets[index] = getOffset(targetKeys[index]!);
                        });
                      });
                    } else {
                      // 移除目标占位符
                      targets.removeWhere((element) => element.key == targetKeys[index]);
                    }
                  });
                },
                child: letter,
              ),
            );
          }),
        ],
      ),
    );
  }
}

关键改动说明

  1. 空安全的getOffset:增加了两层空判断,避免currentContext或RenderBox为null时崩溃
  2. 初始位置计算:在initState的addPostFrameCallback中计算home区域的位置,确保Widget已完成渲染
  3. 目标位置延迟计算:添加目标占位符后,再次使用addPostFrameCallback等待下一帧渲染完成,再获取其位置
  4. 使用asMap()遍历:直接获取每个元素的索引,避免indexOf因重复Widget导致的索引错误
  5. 动态偏移量:将偏移量存入状态变量,AnimatedPositioned根据状态动态切换位置,替代硬编码坐标

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.18 16:32:03