在小部件树中检测到重复的 GlobalKey

Posted

技术标签:

【中文标题】在小部件树中检测到重复的 GlobalKey【英文标题】:Duplicate GlobalKey detected in widget tree 【发布时间】:2018-08-28 12:00:35 【问题描述】:

Screen A 导航到Screen B 并单击“取消”按钮返回Screen A 后,我遇到了globalKey 错误。

问题似乎是Screen B 要么是

A) 未正确处理 B) 没有做本来可以做的事情

其实我也不知道:

如果我只是删除globalKey 的使用,会发生什么坏事? (以便更好地了解基础知识) 如何正确解决此问题?

StatefulWidget 文档状态:enter link description here

StatefulWidget 在从一个状态对象移动时保持相同的状态对象 如果树的创建者使用 GlobalKey 它的关键。因为一个带有 GlobalKey 的小部件最多只能用于一个 在树中的位置,使用 GlobalKey 的小部件最多有一个 关联元素。 框架利用了这个属性 从树中的一个位置移动具有全局键的小部件时 通过嫁接与该关联的(唯一)子树到另一个 小部件 从旧位置到新位置(而不是 在新位置重新创建子树)。国家对象 与 StatefulWidget 相关联的部分与其余部分一起被嫁接 子树,这意味着状态对象被重用(而不是被 重新创建)在新位置。 但是,为了有资格 嫁接,小部件必须插入到新位置 与从旧位置移除的相同动画帧

控制台错误输出:

══╡ 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<FormFieldState<String>>#3c76d]
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: start, crossAxisAlignment: center, renderObject:
RenderFlex#7595c relayoutBoundary=up1 NEEDS-PAINT)
A GlobalKey can only be specified on one widget at a time in the widget tree.

所以这部分错误输出:

前一个父级在此帧期间从未更新,这意味着它 要么根本没有更新,要么在移动小部件之前更新

让我觉得我的旧有状态小部件有机会做某事(重新定位自身或释放某些东西以正确处理。

这似乎在framework.dart assert(_children.contains(child)) 上失败了:

  @override
  void forgetChild(Element child) 
    assert(_children.contains(child));
    assert(!_forgottenChildren.contains(child));
    _forgottenChildren.add(child);
  

【问题讨论】:

您可能希望使用popAndPushNamed() 或类似名称而不是仅使用pushNamed() 在页面之间导航以避免页面被多次添加 感谢您的回复。这里有两件事 1) 幸运的是,我刚开始使用 fluro (github.com/goposse/fluro),但我还不知道 pop 如何与该库一起工作。 2) 如果此错误暗示 Screen A 是仍然存在,有没有办法我可以使用那个实例? (我认为错误都来自Screen A。可能是因为有Screen A 的原始实例,它正在尝试创建一个新实例 这也是我的假设。不过我不知道Fluro。我认为您需要 top pop 活动路线才能返回到前一个路线,或者当您推送新路线时,替换前一个路线,而不是仅添加一个顶部的另一个。 popAndPushNamed() 是 Fl​​utter 路由器如何做到的,不知道 Fluro。 @GunterZochbauer 是的,就是这样!我不知道小部件处理与“弹出”一条路线有关。不过,我想这是有道理的。我不再懒惰,而是查看了 Fluro 的文档:github.com/goposse/fluro/blob/master/lib/src/… 并设置replace: true 触发了对dispose() 的调用,所以我们都很好。谢谢! 您可以添加一个简短的 Fluro 代码示例作为答案并接受它 【参考方案1】:

就我而言,它喜欢热重载错误。只需重新启动调试对我有用。

【讨论】:

Flutter 专业提示:如果您遇到错误,请不要假设您确实有错误。先重启。【参考方案2】:

从 key 变量中移除 static 和 final 类型所以 if

static final GlobalKey<FormState> _abcKey = GlobalKey<FormState>();

改成

GlobalKey<FormState> _abcKey = GlobalKey<FormState>();

【讨论】:

这就是答案!! 正确答案!!!【参考方案3】:

感谢 Gunter 的评论,我确定这是因为屏幕没有得到妥善处理。

Flutter 的 pushReplacement 调用 Route.dispose 最终将处理屏幕。

我仍然不确定这会起作用:

小部件必须插入到同一动画中的新位置 框架

我不确定这种诡计会在什么情况下受益。不过,我的问题解决了。我只需要调用 pop 或 replace 即可。

以下是可用选项:

使用pushAB 和只是从Navigator.popB 使用pushReplacementAB 和从BA

我最近开始使用Fluro 进行路由,还有更多方法可以处理这些情况(注意可选参数replace):

使用router.navigateTo(context, route, replace: false)ABNavigator.popB 使用router.navigateTo(context, route, replace: true)ABBA相同(关键是replace: true

【讨论】:

正是我想要的。谢谢。【参考方案4】:

确保您没有Form 父级和Form 子级具有相同的key

【讨论】:

【参考方案5】:

解决这个问题的最佳方法,对我有用:

class _HomeScreenState extends State<HomeScreen> 
   GlobalKey<FormState> _homeKey = GlobalKey<FormState>(debugLabel: '_homeScreenkey');
   @override
   Widget build(BuildContext context) 
     return Container(
       key: _homeKey,
     );
   

【讨论】:

debugLabel: '_homeScreenkey' 是什么,请解释一下您在这里实际做了什么。 debugLabel - 它用于为应用中的多个 GlobalKey 定义一个单独的label(a.k.a. name, id)。【参考方案6】:

我也有这个问题。 我有一个四屏底部选项卡式应用程序和一个“注销”方法。 但是,该注销方法正在调用 pushReplacementNamed。 这阻止了持有全局键的类(不同于注销函数)调用 dispose。

解决方案是用 popAndPushNamed 更改 pushReplacementNamed 以返回我的“登录”屏幕。

【讨论】:

我有类似的设置,但使用了 GetX 包。需要 Get.offAllNamed() 来路由进出身份验证屏幕,以确保处理所有先前的路由。 github.com/jonataslaw/getx/blob/master/documentation/en_US/…【参考方案7】:

在我的例子中,我想使用 static GlobalKey&lt;ScaffoldState&gt; _scaffoldKey,但是当我多次使用同一个小部件时,它给出了这个重复的错误。

我想给它一个唯一的字符串并且仍然使用这个支架状态。 所以我最终使用了:

static GlobalObjectKey<ScaffoldState> _scaffoldKey

initState:

_scaffoldKey = new GlobalObjectKey<ScaffoldState>(id);

编辑: 事实上,我很傻。我只是简单地删除了static 并再次将其设为GlobalKey :)

【讨论】:

【参考方案8】:

请使用 SingleChildScrollview:

如果你使用 bloc pettern 然后使用带有广播的 strem

代码在这里:

        body: Container(
        decoration: BoxDecoration(
          image: DecorationImage(
            image: AssetImage('assets/images/abcd.jpg'),
            fit: BoxFit.cover,
          ),
        ),
        child: Container(child:Form(key: _key,
          child: Padding(
            padding: EdgeInsets.symmetric(vertical: 100.0, horizontal: 20.0),
              child: SingleChildScrollView(child:Column(
                children: <Widget>[
                  Padding(
                    padding: const EdgeInsets.all(10.0),
                    child: Image.asset('assets/images/logo.png', height: 80, width:80,),
                  ),
                  email(),
                  Padding(
                    padding: EdgeInsets.all(5.0),
                  ),
                  password(),
                  row(context),
                ],
              ),
            ),
          ),
        ),
      ),
      ),
      resizeToAvoidBottomPadding: false,
    );
  

bloc pettern 代码在这里:

    final _email = StreamController<String>.broadcast();
    final _password = StreamController<String>.broadcast();

    Stream<String> get email => _email.stream.transform(validateEmail);
    Stream<String> get password=> _password.stream.transform(validatepassword);

    Function(String) get changeEmail=> _email.sink.add;
    Function(String) get changePassword => _password.sink.add;

    dispose()

      _email.close();
      _password.close();
    



final bloc=Bloc();

【讨论】:

【参考方案9】:

我也有类似的错误。我的回答是,在我更新 Flutter 之后,一些小部件不再具有子属性或子属性。就我而言,它是 CircleAvatar。构建最初不会出错,但在应用程序中来回导航时会失败。

*请查看所有需要孩子的小部件,然后查看更新的文档并确保您的参数仍然正确。

【讨论】:

【参考方案10】:

我遇到了类似的问题。 就我而言,问题是我在屏幕 B 的 dispose 方法中有一个函数,它无法正确执行。我刚刚删除它并解决了问题。

因此请确保您的 dispose 方法在所有屏幕中都正确执行。

【讨论】:

【参考方案11】:

这发生在我身上,我所做的是使用我制作的扩展将整个视图封闭到导航器中

Widget addNavigator() => Navigator(
    onGenerateRoute: (_) => MaterialPageRoute(
      builder: (context2) => Builder(
        builder: (context) => this,
      ),
    ),
  );

【讨论】:

【参考方案12】:

我也遇到了这个错误。类中有一个静态块对象,我删除了修复错误的静态关键字。

无论如何都应该使用 BlocProvider 添加事件。

【讨论】:

【参考方案13】:

我在 StatelessWidget 类上遇到了类似的问题,将其转换为 StatefulWidget 并且错误消失了。

【讨论】:

【参考方案14】:

我不确定为什么还没有人提到这一点,但就我而言,我只是将一个小部件从有状态更改为无状态。要修复错误,您必须重新启动应用程序,而不是进行热重载。

【讨论】:

以上是关于在小部件树中检测到重复的 GlobalKey的主要内容,如果未能解决你的问题,请参考以下文章

在小部件树中检测到 Flutter Duplicate GlobalKey

Flutter:检测任何在屏幕上不可见但在小部件树中的小部件的重建

如何使用 Flutter Web 处理来自提供商的全局密钥?

动画列表多个小部件使用相同的 GlobalKey

另一个异常被抛出:多个小部件使用相同的 GlobalKey

如何在小部件测试中测试特定的 ProviderNotFoundException