Flutter - Runtime Key GlobalKey

2,314 阅读7分钟

Runtime其实就是对象的类型,例如 num i = 0; i的Runtime就是num。
一般情况下不设置key也会默认复用Element。

对于更改同一父级下Widget(尤其是runtimeType不同的Widget)的顺序或是增删,使用key可以更好的复用Element,提升性能。

总之框架要求同一个父节点下子节点的Key都是唯一的就可以了,GlobalKey可以保证全局是唯一的,所以GlobalKey的组件能够依附在不同的节点上。

GlobalKey的作用: 首先就是上面提到的使用相同的GlobalKey来实现复用。

ListView中Key的理解,key的作用 注意这个例子中Widget使用的是StatefullWidget :blog.csdn.net/weixin_3435…

注: 如果widget是stateless的,不加key也能够正确删除。 可能的原因大概是stageless的widget每帧都需要重新绘制,因此不管key变不变化都是重绘的,而stateful则是根据state有没有变化来重绘,这样由于key没有变所以state也没有改变。state.build(this) 就还是绘制之前的视图 但是作为开发者的我们,都应该养成添加key的习惯。

最后这个注阐述的就是颜色交换例子中Stateless和Statefull的不同之处!

###########################################

Key

abstract class Key {
  const factory Key(String value) = ValueKey<String>;

  const Key._();   //命名构造函数
}

factory 工厂模式,我们知道构建方法返回的一般都是当前类所刚构建的对象,但是加上factory关键字之后你可以修改返回的值,可以让返回的对象是之前已经创建好的,也可以返回这个类的子类对象。

新的语法特性:

  const factory Key(String value) = ValueKey<String>;

等价于:

const factory Key(String value) => new ValueKey<String>(value);
  1. LocalKey
    LocalKey 继承自 Key,在同一父级的Element之间必须是唯一的。(当然了,你要是写成不唯一也行,不过后果自负哈。。。)
    我们基本不直接使用LocalKey ,而是使用的它的子类:
    1.1) ValueKey
    我们上面使用到的Key,其实就是ValueKey。它主要是使用特定类型的值来做标识的,像是“值引用”,比如int、String等类型。我们看它源码中的 ==操作符方法:
    重载了==方法
class ValueKey<T> extends LocalKey {
  const ValueKey(this.value);
  
  final T value;

  @override
  bool operator ==(dynamic other) {
    if (other.runtimeType != runtimeType)
      return false; //先比较两个对象的runtimeType
    final ValueKey<T> typedOther = other;
    return value == typedOther.value; // 再比较‘值引用’
  }
  ...
}

1.2) ObjectKey
ValueKey是值引用,ObjectKey是对象引用。

class ObjectKey extends LocalKey {
  const ObjectKey(this.value);

  final Object value;

  @override
  bool operator ==(dynamic other) {
    if (other.runtimeType != runtimeType)
      return false;
    final ObjectKey typedOther = other;
    return identical(value, typedOther.value); // <---
  }
  ...
}

如果您有一个 Todo List 应用程序,它将会记录你需要完成的事情。我们假设每个 Todo 事情都各不相同,而你想要对每个 Todo 进行滑动删除操作。 这时候就需要使用 ValueKey!

如果你有一个生日应用,它可以记录某个人的生日,并用列表显示出来,同样的还是需要有一个滑动删除操作。 我们知道人名可能会重复,这时候你无法保证给 Key 的值每次都会不同。但是,当人名和生日组合起来的 Object 将具有唯一性。 这时候你需要使用 ObjectKey!

也就是说当ValueKey的值引用无法保证key唯一的时候,就可以使用ObjectKey对象引用。

1.3) UniqueKey
如果组合的 Object 都无法满足唯一性的时候,你想要确保每一个 Key 都具有唯一性。那么,你可以使用 UniqueKey。它将会通过该对象生成一个具有唯一性的 hash 码。 不过这样做,每次 Widget 被构建时都会去重新生成一个新的 UniqueKey,失去了一致性。也就是说你的小部件还是会改变。(还不如不用😂)

1.4) PageStorageKey
用于保存和还原比Widget生命周期更长的值。比如用于保存滚动的偏移量。每次滚动完成时,PageStorage会保存其滚动偏移量。 这样在重新创建Widget时可以恢复之前的滚动位置。类似的,在ExpansionTile中用于保存展开与闭合的状态。

  1. GlobalKey
    给一个widget设置GlobalKey
    GlobalKey 能够跨 Widget 访问状态
    GlobalKey是全局的,GlobalKey是Flutter提供的一种在整个APP中引用element的机制。如果一个widget设置了GlobalKey,那么我们便可以通过globalKey.currentWidget获得该widget对象、globalKey.currentElement来获得widget对应的element对象,如果当前widget是StatefulWidget,则可以通过globalKey.currentState来获得该widget对应的state对象。
    总之框架要求同一个父节点下子节点的Key都是唯一的就可以了,GlobalKey可以保证全局是唯一的,所以GlobalKey的组件能够依附在不同的节点上。

注意:使用GlobalKey开销较大,如果有其他可选方案,应尽量避免使用它。另外同一个GlobalKey在整个widget树中必须是唯一的,不能重复。

GlobalKey的几个应用场景:
2.1) 无Context跳转

Navigator.of(context).push(MaterialPageRoute(builder: (context){
          return DemoPage();
        }));

在日常的项目开发中,我们一般push一个新页面是用上面的方法的,利用Navigator.of(context)来进行push或者pop操作。
Navigator.of(context)就是从当前element一级一级往父级寻找NavigatorState对象!
缺点:这种情况是必须传context的,目的是为了利用Navigator.of(context)来获取到NavigatorState对象,然后才能进行push或者pop操作。 那如果我要实现在项目的任何地方都可以push一个新页面的话,而这个地方有可能获取不到context,所以这个时候,就需要实现无context跳转。
定义MatirialApp对象时,指定navigatorKey。
然后在任何地方,使用navigatorKey.currentState获取全局的NavigatorState对象,就可以实现无Context跳转。

2.2) GlobalKey 能够跨 Widget 访问状态,从外部获取widget的state对象,并调用state内部方法!

class SwitcherScreenState extends State<SwitcherScreen> {
  bool isActive = false;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Center(
        child: Switch.adaptive(
            value: isActive,
            onChanged: (bool currentStatus) {
              isActive = currentStatus;  //内部改变switch状态
              setState(() {});  
            }),
      ),
    );
  }

  //从外部更改switch状态
  changeState() {
    isActive = !isActive;
    setState(() {});
  }  
}

SwitcherScreen就是上面SwitcherScreenState的widget, 从外部获取widget的state对象!

class _ScreenState extends State<Screen> {
  final GlobalKey<SwitcherScreenState> key = GlobalKey<SwitcherScreenState>();

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: SwitcherScreen(
        key: key,
      ),
      floatingActionButton: FloatingActionButton(onPressed: () {
        key.currentState.changeState(); //从外部更改switch状态
      }),
    );
  }
}

2.3)利用GlobalKey持有的BuildContext。比如常见的使用就是获取Widget的宽高信息,通过BuildContext可以在其中获取RenderObject或Size,从而拿到宽高信息。这里就不贴代码了,有需要可以看此处示例。github.com/simplezhli/…

2.4) 还有一个场景是,过渡动画,当两个页面都是相同的Widget时,也可以使用GlobalKey。
思考一个场景,A页面是一个商品列表有许多商品图片(大概就单列这样),B页面是一个商品详情页(有商品大图),当用户在A页面点击一个其中详情,可能会出现一个过渡动画,A页面的商品图片慢慢放大然后下面的介绍文字也会跟着出现,然后就这样平滑的过渡到B页面。 此时A页面和B页面都其实共用了一个商品图片的组件,B页面没必要重复创建这个组件可以直接把A页面的组件“借”过来。

引申:在Widget树中获取State对象 book.flutterchina.club/chapter3/fl…

Scaffold(
  appBar: AppBar(
    title: Text("子树中获取State对象"),
  ),
  body: Center(
    child: Builder(builder: (context) {
      return RaisedButton(
        onPressed: () {
          // 查找父级最近的Scaffold对应的ScaffoldState对象
          ScaffoldState _state = context.findAncestorStateOfType<ScaffoldState>();
          //调用ScaffoldState的showSnackBar来弹出SnackBar
          _state.showSnackBar(
            SnackBar(
              content: Text("我是SnackBar"),
            ),
          );
        },
        child: Text("显示SnackBar"),
      );
    }),
  ),
);

RaisedButton的press方法中,查找父级最近的Scaffold对应的ScaffoldState对象.

 ScaffoldState _state = context.findAncestorStateOfType<ScaffoldState>();

通常来说,StatefulWidget的状态State是私有的(不应该向外部暴露),所以我们可以在Widget中定义一个static of()静态方法来获取该Widget的state对象。

class AWidget extends StatefulWidget{
   static AWidgetState of(BuildContext context) {
    return context.findAncestorStateOfType<AWidgetState>();
  }
}

class AWidgetState extends State<AWidget>{

}

//然后在AWidget中的子widget中,调用AWidget.of(context)获取AWidgetState对象