【问题标题】:How to use `GlobalKey` to maintain widgets' states when changing parents?更改父母时如何使用`GlobalKey`维护小部件的状态?
【发布时间】:2019-11-15 15:05:00
【问题描述】:

在 Emily Fortuna 的文章(和视频)中,她提到:

GlobalKeys 有两个用途:它们允许小部件更改父级 在您的应用程序中的任何位置都不会丢失状态,或者它们可以用于 在完全不同的部分访问有关另一个小部件的信息 的小部件树。第一个场景的一个例子可能是如果你 想在两个不同的屏幕上显示相同的小部件,但持有 在所有相同的状态下,您都需要使用 GlobalKey。

她的文章包含一个名为“Using GlobalKey to ReuseWidget”的应用程序的 gif 演示,但没有提供源代码(可能是因为它太琐碎了)。您还可以在此处观看快速视频演示,从 8:30 开始:https://youtu.be/kn0EOS-ZiIc?t=510

如何实现她的演示?我在哪里定义 GlobalKey 变量以及如何/在哪里使用它?基本上例如,我想显示一个每秒计数的计数器,并在许多不同的屏幕上显示它。 GlobalKey 可以帮助我吗?

【问题讨论】:

    标签: flutter


    【解决方案1】:

    使用GlobalKey 在树周围移动小部件的最常见用例是有条件地将“子”包装到另一个小部件中,如下所示:

    Widget build(context) {
      if (foo) {
        return Foo(child: child);
      }
      return child;
    }
    

    使用这样的代码,您会很快注意到如果child 是有状态的,切换foo 将使child 失去其状态,这通常是意料之外的。

    为了解决这个问题,我们要让我们的小部件有状态,创建一个GlobalKey,并将child 包装成一个KeyedSubtree

    这是一个例子:

    class Example extends StatefulWidget {
      const Example({Key key, this.foo, this.child}) : super(key: key);
    
      final Widget child;
      final bool foo;
    
      @override
      _ExampleState createState() => _ExampleState();
    }
    
    class _ExampleState extends State<Example> {
      final key = GlobalKey();
    
      @override
      Widget build(BuildContext context) {
        final child = KeyedSubtree(key: key, child: widget.child);
    
        if (widget.foo) {
          return Foo(child: child);
        }
        return child;
      }
    }
    

    【讨论】:

    • 感谢您指出 KeyedSubtree 小部件。下次我会用它!
    • 为什么 Example 是 StatefulWidget。如果 'key' 成员设置为 final,它不能是一个无状态的小部件吗?
    【解决方案2】:

    我不建议将 GlobalKey 用于此任务。

    您应该传递数据,而不是小部件,而不是小部件状态。例如,如果您想要在演示中使用SwitchSlider,最好将实际的booleandouble 传递到这两个小部件后面。对于更复杂的数据,您应该查看ProviderInheritedWidget 等。

    自该视频发布以来,情况发生了变化。 Saed 的回答(我奖励了 50 分)可能是视频中的做法,但它在最近的 Flutter 版本中不再有效。基本上目前没有使用 GlobalKey 轻松实现演示的好方法

    但是……

    如果你能保证,两个小部件永远不会同时出现在屏幕上,或者更准确地说,它们永远不会同时插入到同一帧上的小部件树中,那么您可以尝试使用GlobalKey 在布局的不同部分使用相同的小部件。

    请注意,这是一个非常严格的限制。例如,当滑动到另一个屏幕时,通常会有一个过渡动画,两个屏幕同时渲染。那不行。所以在这个演示中,我插入了一个“空白页”来防止滑动时出现这种情况。

    如何:

    因此,如果您想要相同的小部件出现在非常不同的屏幕上(希望它们彼此相距很远),您可以使用 GlobalKey 来做到这一点,基本上只需 3 行代码。

    首先,声明一个可以从两个屏幕访问的变量:

    final _key = GlobalKey();
    

    然后,在您的小部件中,有一个构造函数,该构造函数接受一个键并将其传递给父类:

    Foo(key) : super(key: key);
    

    最后,每当您使用小部件时,将相同的键变量传递给它:

    return Container(
      color: Colors.green[100],
      child: Foo(_key),
    );
    

    完整来源:

    import 'package:flutter/material.dart';
    
    void main() {
      runApp(MaterialApp(home: MyApp()));
    }
    
    class MyApp extends StatelessWidget {
      final _key = GlobalKey();
    
      @override
      Widget build(BuildContext context) {
        return Scaffold(
          appBar: AppBar(title: Text("Global Key Demo")),
          body: PageView.builder(
            itemCount: 3,
            itemBuilder: (context, index) {
              switch (index) {
                case 0:
                  return Container(
                    color: Colors.green[100],
                    child: Foo(_key),
                  );
                  break;
                case 1:
                  return Container(
                    color: Colors.blue[100],
                    child: Text("Blank Page"),
                  );
                  break;
                case 2:
                  return Container(
                    color: Colors.red[100],
                    child: Foo(_key),
                  );
                  break;
                default:
                  throw "404";
              }
            },
          ),
        );
      }
    }
    
    class Foo extends StatefulWidget {
      @override
      _FooState createState() => _FooState();
    
      Foo(key) : super(key: key);
    }
    
    class _FooState extends State<Foo> {
      bool _switchValue = false;
      double _sliderValue = 0.5;
    
      @override
      Widget build(BuildContext context) {
        return Column(
          children: [
            Switch(
              value: _switchValue,
              onChanged: (v) {
                setState(() => _switchValue = v);
              },
            ),
            Slider(
              value: _sliderValue,
              onChanged: (v) {
                setState(() => _sliderValue = v);
              },
            )
          ],
        );
      }
    }
    

    【讨论】:

    • 您好 user1032613,我只是想澄清一下,这不是我的方法,而是 Emily Fortuna 的方法。我只是试图解释它是如何实现的,而不是我完全同意你的观点
    • @SaedNabil 感谢您的澄清。我相信可能是这种情况,因为她的视频清楚地显示了两个小部件出现在同一帧上。但是,您是否有任何支持证据表明 Emily 使用了您提到的确切方法,可能是她的博客或 GitHub 等?
    • 嗨,我想区分概念/方法和实现,我不知道她的实现,但显然这是我对她的方法的实现我不认为艾米丽也推荐这种方法由于她对颤振非常了解,我相信她在一般解释键的上下文中提到了使用全局键访问/修改小部件状态的可能性/能力,当时状态管理并不清晰且更具争议性除了现在的主题之外,我不知道可能使用 Globalkey 的任何不同方式
    • @SaedNabil 谢谢,我已经相应地编辑了我的答案。另外,我想指出的是,这个答案使用了不同的方法,以防你一眼就错过了它。这个答案没有使用currentState 访问另一个小部件的状态(从而区分“主小部件”和“克隆”),而是让 Flutter 将不同的小部件实例链接到单个状态。
    • 谢谢,我知道在小部件树中移动小部件的概念,但以前从未尝试过实现它,因为在现实生活中的应用程序中,状态应该移到一个集中的类中,以便业务逻辑可以操作,然​​后相应地更新 ui,当您有仍然需要在内部保持其状态的动画小部件时,它可能会很有用,很高兴看到概念证明,所以您所做的是重新发现 GlobalKeys 的“古老拼图”的一部分,它可以是您自己在回答中提到的谨慎和限制使用:)
    【解决方案3】:

    更新:这是处理状态管理的旧方法,不再推荐,请参阅我的 cmets 并查看下面的 user1032613's answer

    全局键可用于从小部件树中的任何位置访问有状态小部件的状态

    import 'package:flutter/material.dart';
    
    main() {
      runApp(MaterialApp(
        theme: ThemeData(
          primarySwatch: Colors.indigo,
        ),
        home: App(),
      ));
    }
    
    class App extends StatefulWidget {
      @override
      State<App> createState() => _AppState();
    }
    
    class _AppState extends State<App> {
      GlobalKey<_CounterState> _counterState;
    
      @override
      void initState() {
        super.initState();
        _counterState = GlobalKey();
      }
    
      @override
      Widget build(BuildContext context) {
        return Scaffold(
          appBar: AppBar(),
          body: Center(
              child: Column(
            children: <Widget>[
              Counter(
                key: _counterState,
              ),
            ],
          )),
          floatingActionButton: FloatingActionButton(
            child: Icon(Icons.navigate_next),
            onPressed: () {
              Navigator.of(context).push(
                MaterialPageRoute(builder: (context) {
                  return Page1(_counterState);
                }),
              );
            },
          ),
        );
      }
    }
    
    class Counter extends StatefulWidget {
      const Counter({
        Key key,
      }) : super(key: key);
    
      @override
      _CounterState createState() => _CounterState();
    }
    
    class _CounterState extends State<Counter> {
      int count;
    
      @override
      void initState() {
        super.initState();
        count = 0;
      }
    
      @override
      Widget build(BuildContext context) {
        return Row(
          children: <Widget>[
            IconButton(
              icon: Icon(Icons.add),
              onPressed: () {
                setState(() {
                  count++;
                });
              },
            ),
            Text(count.toString()),
          ],
        );
      }
    }
    class Page1 extends StatefulWidget {
      final GlobalKey<_CounterState> counterKey;
      Page1( this.counterKey);
      @override
      _Page1State createState() => _Page1State();
    }
    
    class _Page1State extends State<Page1> {
      @override
      Widget build(BuildContext context) {
        return Scaffold(
          appBar: AppBar(),
          body: Center(
            child: Row(
              children: <Widget>[
                IconButton(
                  icon: Icon(Icons.add),
                  onPressed: () {
                    setState(() {
                      widget.counterKey.currentState.count++;
                      print(widget.counterKey.currentState.count);
                    });
                  },
                ),
                Text(
                  widget.counterKey.currentState.count.toString(),
                  style: TextStyle(fontSize: 50),
                ),
              ],
            ),
          ),
        );
      }
    }
    

    【讨论】:

    • 当 _CounterState 从单独的包中导入时,如何将 _CounterState 设置为创建全局键 _counterState 时的类型?
    • @BrandonPillay 你可以使用final counterKey;而不是最终的 GlobalKey<_counterstate> counterKey;因此可以推断,如果您坚持使用类型,则需要将 _CounterState 从私有转换为全局 CounterState,以便您可以从另一个包中导入它。
    • 嘿@SaedNabil,我想知道build()Counter 何时被触发。我们只做widget.counterKey.currentState.count++;。当我们点击返回时,Counterbuild() 不应该被触发,并且值应该没有在 UI 中更新,仅作为状态变量 count。那么,build() 何时触发 Counter 小部件?
    • @Harsh 嘿,很遗憾 Flutter 团队可能已经改变了导航器的行为,现在它不会自动重建,但这是可以理解的,因为状态管理是 Flutter 中一个正在发展的活动,我不得不向您承认,不再推荐这种方式来管理您的应用程序状态,如果您是高级开发人员,请使用继承的小部件强制重建,如果不使用更人性化的方法,即提供程序包来管理您的状态@ 987654323@
    猜你喜欢
    • 1970-01-01
    • 2021-12-04
    • 2019-04-26
    • 2021-07-25
    • 2021-06-27
    • 2021-10-06
    • 1970-01-01
    • 1970-01-01
    • 2019-10-06
    相关资源
    最近更新 更多