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

Flutter GetX使用Get.off跳转时出现Duplicate GlobalKey异常

问题:注册后返回登录页触发Duplicate GlobalKey错误

场景复现

从LoginScreen跳转到SignUp页面完成注册后,调用Get.off(()=>LoginScreen())返回登录页时出现Duplicate GlobalKey错误,使用Get.back()则正常。技术栈为GetX框架 + Flutter 3.19.0。

错误日志

════════ Exception caught by widgets library ═══════════════════════════════════
The following assertion was thrown while finalizing the widget tree:
Duplicate GlobalKey detected in widget tree.
The following GlobalKey was specified multiple times in the widget tree. This will lead to parts of the widget tree being truncated unexpectedly, because the second time a key is seen, the previous instance is moved to the new location. The key was:

  • [LabeledGlobalKey<FormState>#207b9 _home]
    This was determined by noticing that after the widget with the above global key was moved out of its previous parent, that previous parent never updated during this frame, meaning that it either did not update at all or updated before the widget was moved, in either case implying that it still thinks that it should have a child with that global key.
    The specific parent that did not update after having one or more children forcibly removed due to GlobalKey reparenting is:
  • Column(direction: vertical, mainAxisAlignment: center, crossAxisAlignment: start, dependencies: [Directionality], renderObject: RenderFlex#14950 relayoutBoundary=up13 NEEDS-PAINT)
    A GlobalKey can only be specified on one widget at a time in the widget tree.

核心原因

LoginController中定义的loginFormKey是类级成员变量,而GetX默认以全局单例方式注入控制器(Get.put())。当跳转到注册页时,旧的LoginController实例并未被销毁,调用Get.offAll(()=>LoginScreen())会创建新的LoginScreen,但仍然复用旧的全局单例LoginController,导致同一个loginFormKey被两个Form组件绑定,触发重复键冲突。

解决方案

方案1:将GlobalKey移到LoginScreen的State中

把Form的GlobalKey从Controller转移到LoginScreen的State里,确保每个LoginScreen实例都持有独立的Key:

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

  @override
  State<LoginScreen> createState() => _LoginScreenState();
}

class _LoginScreenState extends State<LoginScreen> {
  // 每个LoginScreen实例拥有独立的FormKey
  final GlobalKey<FormState> _loginFormKey = GlobalKey<FormState>(debugLabel: '_home');
  late LoginController _controller;

  @override
  void initState() {
    super.initState();
    _controller = Get.put(LoginController());
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Form(
        key: _loginFormKey,
        child: Column(
          children: [
            TextFormField(
              controller: _controller.signinEmailTextEditingControler,
              validator: (value) => value?.isEmpty ?? true ? "请输入邮箱" : null,
            ),
            TextFormField(
              controller: _controller.signinPasswordTextEditingControler,
              obscureText: true,
              validator: (value) => value?.length ?? 0 < 6 ? "密码不少于6位" : null,
            ),
            ElevatedButton(
              onPressed: () async {
                if (_loginFormKey.currentState!.validate()) {
                  await _controller.signIn();
                }
              },
              child: const Text('登录'),
            ),
          ],
        ),
      ),
    );
  }
}

同时修改LoginController,移除loginFormKey变量:

class LoginController extends GetxController {
  final TextEditingController signinEmailTextEditingControler = TextEditingController();
  final TextEditingController signinPasswordTextEditingControler = TextEditingController();

  bool isLoading = false;
  bool showSigninError = false;

  Future<void> signIn() async {
    try {
      isLoading = true;
      showSigninError = false;
      update();
      final NetworkResponse networkResponse = await NetworkCaller().postRequest(
        Urls.login,
        body: {
          "email": signinEmailTextEditingControler.text.trim(),
          "password": signinPasswordTextEditingControler.text
        },
      );
      if (networkResponse.isSuccess) {
        Get.off(() => const MainBottomNavBar());
        signinClear();
      } else {
        if (networkResponse.statusCode == 401) {
          showSigninError = true;
          update();
          signinPasswordTextEditingControler.clear();
        } else {
          signinClear();
        }
      }
    } finally {
      isLoading = false;
      update();
    }
  }

  void signinClear() {
    signinEmailTextEditingControler.clear();
    signinPasswordTextEditingControler.clear();
  }

  @override
  void dispose() {
    signinEmailTextEditingControler.dispose();
    signinPasswordTextEditingControler.dispose();
    super.dispose();
  }
}

方案2:跳转前删除旧的LoginController实例

在注册成功后,先删除全局的LoginController单例,再跳转回登录页,确保新的LoginScreen会创建全新的控制器实例:

修改SignupController的signup方法:

if (response.isSuccess) {
  signupClear();
  // 删除旧的LoginController单例
  Get.delete<LoginController>();
  Get.offAll(() => LoginScreen());
  update();
}

方案3:为LoginController生成唯一实例标签

每次注入LoginController时使用唯一的tag,确保每个LoginScreen对应独立的控制器实例:

在LoginScreen中注入控制器:

class LoginScreen extends StatelessWidget {
  final String _controllerTag = UniqueKey().toString();

  const LoginScreen({super.key});

  @override
  Widget build(BuildContext context) {
    final LoginController _controller = Get.put(LoginController(), tag: _controllerTag);
    return Scaffold(
      body: Form(
        key: _controller.loginFormKey,
        // 表单内容
      ),
    );
  }

  @override
  void dispose() {
    // 销毁对应tag的控制器
    Get.delete<LoginController>(tag: _controllerTag);
    super.dispose();
  }
}

或者在跳转时绑定新控制器:

// SignupController中的跳转逻辑
Get.offAll(()=>LoginScreen(), binding: BindingsBuilder((){
  Get.put(LoginController(), tag: UniqueKey().toString());
}));

方案对比

  • 方案1:最符合Flutter组件设计原则,每个Widget实例管理自己的Key,无单例冲突问题,推荐使用。
  • 方案2:实现简单,适合快速修复,但需注意控制器删除时机,避免其他地方依赖旧控制器导致异常。
  • 方案3:灵活性高,但需要管理tag,增加了代码复杂度,适合需要保留控制器持有FormKey的场景。

内容的提问来源于stack exchange,提问作者MD Tangim haque

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.29 17:02:09