Flutter中的Key

在Flutter 中,Key 是 几乎所有 widget 都具有的属性。为什么 widget 具有 Key 呢?Key的作用是什么?

什么是 Key

Key是Widget、Element 和 SemanticNodes 的标识符。 Key 是Widget、Element 和 SemanticNodes的唯一标识。例如对于 Widget 在 Widget 树中改变了位置,Key 可以帮助它们保留状态。相对于无状态的Widget,Key对于有状态的 Widget 作用更大。

Key的使用场景

在添加、删除或重排同一类型的 widget 集合时,Key 可以让这些 widget 保持状态,并且在 widget 树中处于相同的级别。

例子:两个颜色块单击按钮时交换两者位置。

两种实现方式:

第一种实现:widget 是无状态的,色值保存在 widget 本身中。当点击 FloatingActionButton,色块会交换位置。

Dart 复制代码
import 'dart:math';

import 'package:flutter/material.dart';

//色块widget是无状态的
class StatelessColorTitles extends StatelessWidget {
  //色值保存在本身控件中
  var r = Random().nextInt(256);
  var g = Random().nextInt(256);
  var b = Random().nextInt(256);

  @override
  Widget build(BuildContext context) {
    return Container(
      height: 100,
      width: 100,
      color: Color.fromRGBO(r, g, b, 1),
    );
  }
}

class PositionTiles extends StatefulWidget {
  const PositionTiles({Key? key}) : super(key: key);

  @override
  State<StatefulWidget> createState() {
    return _PositionTilesState();
  }
}

class _PositionTilesState extends State<PositionTiles> {
  List<Widget> titles = [];

  @override
  void initState() {
    super.initState();
    titles = [StatelessColorTitles(), StatelessColorTitles()];
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Center(
        child: Row(
          mainAxisAlignment: MainAxisAlignment.center,
          children: titles,
        ),
      ),
      floatingActionButton: FloatingActionButton(
        child: const Icon(Icons.sentiment_very_satisfied),
        onPressed: swapTitles,
      ),
    );
  }

  void swapTitles() {
    setState(() {
      titles.insert(1, titles.removeAt(0));
    });
  }
}

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

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(primarySwatch: Colors.blue),
      home: const PositionTiles(),
    );
  }
}

下面是第二种实现:色块 widget 有状态,色值保存在状态中。为了正确交换平铺位置,我们需要向有状态的 widget 添加 key 参数。

Dart 复制代码
import 'dart:math';

import 'package:flutter/material.dart';

class _StatefulColorTilesState2 extends State<StatefulColorTiles2> {
  //色值保存在本身控件中
  var r = Random().nextInt(256);
  var g = Random().nextInt(256);
  var b = Random().nextInt(256);

  @override
  void initState() {
    super.initState();
  }

  @override
  Widget build(BuildContext context) {
    return Container(
      height: 100,
      width: 100,
      color: Color.fromRGBO(r, g, b, 1),
    );
  }
}

class StatefulColorTiles2 extends StatefulWidget {
  const StatefulColorTiles2({Key? key}) : super(key: key);

  @override
  State<StatefulWidget> createState() {
    return _StatefulColorTilesState2();
  }
}

class PositionTiles2 extends StatefulWidget {
  const PositionTiles2({Key? key}) : super(key: key);

  @override
  State<StatefulWidget> createState() {
    return _PositionTilesState2();
  }
}

class _PositionTilesState2 extends State<PositionTiles2> {
  List<Widget> titles = [];

  @override
  void initState() {
    super.initState();
    titles = [
      //添加了key参数,若不添加则点击按钮色块不会交互
      StatefulColorTiles2(key: UniqueKey()),
      StatefulColorTiles2(key: UniqueKey())
    ];
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Center(
          child: Row(
        mainAxisAlignment: MainAxisAlignment.center,
        children: titles,
      )),
      floatingActionButton: FloatingActionButton(
        child: Icon(Icons.sentiment_very_satisfied),
        onPressed: swapTitles,
      ),
    );
  }

  void swapTitles() {
    setState(() {
      titles.insert(1, titles.removeAt(0));
    });
  }
}

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

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
        title: "FLutter Demo about key",
        theme: ThemeData(primarySwatch: Colors.blue),
        home: const PositionTiles2());
  }
}

widget 有状态时,才需要设置 key。如果是无状态的 widget 则可以不需要设置 key。

原理

渲染 widget 时,Flutter 在构建 widget 树的同时,还会构建其对应的Element树。Element树持有 widget 树中 widget 的信息及其子 widget 的引用。在修改和重新渲染的过程中,Flutter 查找Element树查看其是否改变,以便在元素未改变时可以复用旧元素。

注意:

1.widget 树封装配置信息,Element树相当于实例对象。widget 类似于 json串,Element树类似于 json 解析后的 bean。

  1. widget 类型 和 key 值 ,在没用 key 的情况下,类型相同表示新旧 widget 可复用。

#Widget

Dart 复制代码
  /// Whether the `newWidget` can be used to update an [Element] that currently
  /// has the `oldWidget` as its configuration.
  ///
  /// An element that uses a given widget as its configuration can be updated to
  /// use another widget as its configuration if, and only if, the two widgets
  /// have [runtimeType] and [key] properties that are [operator==].
  ///
  /// If the widgets have no key (their key is null), then they are considered a
  /// match if they have the same type, even if their children are completely
  /// different.
  static bool canUpdate(Widget oldWidget, Widget newWidget) {
    return oldWidget.runtimeType == newWidget.runtimeType
        && oldWidget.key == newWidget.key;
  }

在上面实现方式一的例子中:原色块是 一 和 二, 交换后 oldWidget 与 newWidget 比较,因为 一 和 二 是同类型 StatelessColorTiles ,则表示原来在Element树中的 Element(一)元素在交换后是可以继续供 Element(二)元素 复用。

键类型

Key 是抽象类,有两个重要的子类 LocalKey和 GlobalKey。

Key

Dart 复制代码
/// A [Key] is an identifier for [Widget]s, [Element]s and [SemanticsNode]s.
///
/// A new widget will only be used to update an existing element if its key is
/// the same as the key of the current widget associated with the element.
///
/// {@youtube 560 315 https://www.youtube.com/watch?v=kn0EOS-ZiIc}
///
/// Keys must be unique amongst the [Element]s with the same parent.
///
/// Subclasses of [Key] should either subclass [LocalKey] or [GlobalKey].
///
/// See also:
///
///  * [Widget.key], which discusses how widgets use keys.
@immutable
abstract class Key {
  /// Construct a [ValueKey<String>] with the given [String].
  ///
  /// This is the simplest way to create keys.
  const factory Key(String value) = ValueKey<String>;

  /// Default constructor, used by subclasses.
  ///
  /// Useful so that subclasses can call us, because the [Key.new] factory
  /// constructor shadows the implicit constructor.
  @protected
  const Key.empty();
}

Localkey

LocalKey 是 Key 的子类,用于标识局部范围内的 Widgets。它有两个具体实现:ValueKey 和 ObjectKey。

Dart 复制代码
/// A key that is not a [GlobalKey].
///
/// Keys must be unique amongst the [Element]s with the same parent. By
/// contrast, [GlobalKey]s must be unique across the entire app.
///
/// See also:
///
///  * [Widget.key], which discusses how widgets use keys.
abstract class LocalKey extends Key {
  /// Abstract const constructor. This constructor enables subclasses to provide
  /// const constructors so that they can be used in const expressions.
  const LocalKey() : super.empty();
}

ValueKey

ValueKey 是简单的值(如字符串、数字)来标识 Widget。它通常用于具有唯一标识的对象。

Dart 复制代码
/// A key that uses a value of a particular type to identify itself.
///
/// A [ValueKey<T>] is equal to another [ValueKey<T>] if, and only if, their
/// values are [operator==].
///
/// This class can be subclassed to create value keys that will not be equal to
/// other value keys that happen to use the same value. If the subclass is
/// private, this results in a value key type that cannot collide with keys from
/// other sources, which could be useful, for example, if the keys are being
/// used as fallbacks in the same scope as keys supplied from another widget.
///
/// See also:
///
///  * [Widget.key], which discusses how widgets use keys.
class ValueKey<T> extends LocalKey {
  /// Creates a key that delegates its [operator==] to the given value.
  const ValueKey(this.value);

  /// The value to which this key delegates its [operator==]
  final T value;

  @override
  bool operator ==(Object other) {
    if (other.runtimeType != runtimeType) {
      return false;
    }
    return other is ValueKey<T>
        && other.value == value;
  }

  @override
  int get hashCode => Object.hash(runtimeType, value);

  @override
  String toString() {
    final String valueString = T == String ? "<'$value'>" : '<$value>';
    // The crazy on the next line is a workaround for
    // https://github.com/dart-lang/sdk/issues/33297
    if (runtimeType == _TypeLiteral<ValueKey<T>>().type) {
      return '[$valueString]';
    }
    return '[$T $valueString]';
  }
}
Dart 复制代码
Container(
  alignment: Alignment.centerLeft,
  width: 100,
  height: 70,
  key: const ValueKey("TipItem")
)

ObjectKey

ObjectKey 使用对象作为标识符。这对于需要使用对象来唯一标识 Widget 的情况非常有用。

ObjectKey跟ValueKey类似,只是value的类型从T变成了Object,operator方法也有变化。

identical对比是否是相等或者说相同的时候,它其实属于对比引用或者说指针是否相等,类似于Java中对比内存地址。

Dart 复制代码
/// A key that takes its identity from the object used as its value.
///
/// Used to tie the identity of a widget to the identity of an object used to
/// generate that widget.
///
/// See also:
///
///  * [Key], the base class for all keys.
///  * The discussion at [Widget.key] for more information about how widgets use
///    keys.
class ObjectKey extends LocalKey {
  /// Creates a key that uses [identical] on [value] for its [operator==].
  const ObjectKey(this.value);

  /// The object whose identity is used by this key's [operator==].
  final Object? value;

  @override
  bool operator ==(Object other) {
    if (other.runtimeType != runtimeType) {
      return false;
    }
    return other is ObjectKey
        && identical(other.value, value);
  }

  @override
  int get hashCode => Object.hash(runtimeType, identityHashCode(value));

  @override
  String toString() {
    if (runtimeType == ObjectKey) {
      return '[${describeIdentity(value)}]';
    }
    return '[${objectRuntimeType(this, 'ObjectKey')} ${describeIdentity(value)}]';
  }
}
Dart 复制代码
class Student {
  final String name;
  final int age;

  Student(this.name, this.age);

  @override
  bool operator ==(Object other) {
    return identical(this, other) ||
        other is Student &&
            runtimeType == other.runtimeType &&
            name == other.name &&
            age == other.age;
  }

  @override
  int get hashCode => name.hashCode ^ age.hashCode;
}
Dart 复制代码
Container(
alignment: Alignment.centerLeft,
width: 100,
height: 70,
key:  ObjectKey(Student("Tom", 22)),
color: Colors.amber)

UniqueKey

UniqueKey:通过该对象可以生成一个具有唯一性的 hash 码。又因为每次 Widget 构建时都会重新生成一个新的 UniqueKey,失去了使用意义。(所以UniqueKey使用价值不高)。UniqueKey是一个独一无二的Key,比如UniqueKey()和UniqueKey()是不相等的。

Dart 复制代码
/// A key that is only equal to itself.
///
/// This cannot be created with a const constructor because that implies that
/// all instantiated keys would be the same instance and therefore not be unique.
class UniqueKey extends LocalKey {
  /// Creates a key that is equal only to itself.
  ///
  /// The key cannot be created with a const constructor because that implies
  /// that all instantiated keys would be the same instance and therefore not
  /// be unique.
  // ignore: prefer_const_constructors_in_immutables , never use const for this class
  UniqueKey();

  @override
  String toString() => '[#${shortHash(this)}]';
}

PageStorageKey

PageStorageKey可以用于滑动列表,在列表页面通过某一个 Item的点击 跳转到了一个新的页面,当返回之前的列表页面时,列表的滑动的距离回到了顶部。此时给 Sliver 设置一个 PageStorageKey,就能够保持 Sliver 的滚动状态。

Dart 复制代码
/// A [Key] that can be used to persist the widget state in storage after the
/// destruction and will be restored when recreated.
///
/// Each key with its value plus the ancestor chain of other [PageStorageKey]s
/// need to be unique within the widget's closest ancestor [PageStorage]. To
/// make it possible for a saved value to be found when a widget is recreated,
/// the key's value must not be objects whose identity will change each time the
/// widget is created.
///
/// See also:
///
///  * [PageStorage], which manages the data storage for widgets using
///    [PageStorageKey]s.
class PageStorageKey<T> extends ValueKey<T> {
  /// Creates a [ValueKey] that defines where [PageStorage] values will be saved.
  const PageStorageKey(super.value);
}

PageView + BottomNavigationBar 或者 TabBarView + TabBar 的时候发现当切换到另一页面的时候, 前一个页面就会被销毁, 再返回前一页时, 页面会被重建, 随之数据会重新加载, 控件会重新渲染 带来了极不好的用户体验。可以使用使用PageStorage在页面切换时保存状态。

Dart 复制代码
import 'package:flutter/material.dart';

class Page1 extends StatefulWidget {
  @override
  State<StatefulWidget> createState() {
    return _Page1State();
  }
}

class Page1Params {
  int counter = 0;
}

class _Page1State extends State<Page1> {
  Page1Params _params = Page1Params();

  @override
  Widget build(BuildContext context) {
    return Container(
      padding: EdgeInsets.all(20.0),
      child: Column(
        mainAxisAlignment: MainAxisAlignment.start,
        children: <Widget>[
          Text('${_params.counter}',
              style: TextStyle(
                fontSize: 50,
              )),
          Row(
            mainAxisAlignment: MainAxisAlignment.center,
            children: <Widget>[
              IconButton(
                  onPressed: () {
                    setState(() {
                      _params.counter--;
                    });
                  },
                  icon: Icon(Icons.remove, size: 32.0)),
              IconButton(
                  onPressed: () {
                    setState(() {
                      _params.counter++;
                    });
                  },
                  icon: Icon(Icons.add, size: 32.0)),
            ],
          )
        ],
      ),
    );
  }
}

class Page2 extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Container();
  }
}

class TabInfo {
  String label;
  Widget widget;

  TabInfo(this.label, this.widget);
}

class MyCustomApp extends StatelessWidget {
  final List<TabInfo> _tabs = [
    TabInfo("FIRST", Page1()),
    TabInfo("SECOND", Page2())
  ];

  @override
  Widget build(BuildContext context) {
    return DefaultTabController(
      length: _tabs.length,
      child: Scaffold(
        appBar: AppBar(
          title: Text('Tab Controller'),
          bottom: PreferredSize(
            child: TabBar(
              isScrollable: true,
              tabs: _tabs.map((TabInfo tab) {
                return Tab(text: tab.label);
              }).toList(),
            ),
            preferredSize: Size.fromHeight(50.0),
          ),
        ),
        body: TabBarView(
          children: _tabs.map((tab) => tab.widget).toList(),
        ),
      ),
    );
  }
}

void main() => runApp(
      MaterialApp(
        debugShowCheckedModeBanner: false,
        home: MyCustomApp(),
      ),
    );

在上面的例子中,通过点击FIRST 和 SECOND 按钮切换页面可以发现来回切换以后,页面状态被清理了。

在上面的例子中,当出现页面切换的情况,每次标签栏切换页面时,之前的页面就被清理了。

出现上面问题的原因是,之前页面的状态(State)没有被保留下来,状态的reset导致页面发生了初始化。

通过使用PageStorage管理改造:

Dart 复制代码
import 'package:flutter/material.dart';

class Page1 extends StatefulWidget {
  //为Page1 添加一个带参数的构造函数,
  //通过其将Key直接传给super,通过key可恢复状态
  Page1({Key? key}) : super(key: key);

  @override
  State<StatefulWidget> createState() {
    return _Page1State();
  }
}

class Page1Params {
  int counter = 0;
}

class _Page1State extends State<Page1> {
  Page1Params _params = Page1Params();

  @override
  Widget build(BuildContext context) {
    return Container(
      padding: EdgeInsets.all(20.0),
      child: Column(
        mainAxisAlignment: MainAxisAlignment.start,
        children: <Widget>[
          Text('${_params.counter}',
              style: TextStyle(
                fontSize: 50,
              )),
          Row(
            mainAxisAlignment: MainAxisAlignment.center,
            children: <Widget>[
              IconButton(
                  onPressed: () {
                    //setState的同时通过writeState将状态保存进PageStorage。
                    setState(() {
                      _params.counter--;
                    });
                    PageStorage.of(context).writeState(context, _params);
                  },
                  icon: Icon(Icons.remove, size: 32.0)),
              IconButton(
                  onPressed: () {
                    //setState的同时通过writeState将状态保存进PageStorage。
                    setState(() {
                      _params.counter++;
                    });
                    PageStorage.of(context).writeState(context, _params);
                  },
                  icon: Icon(Icons.add, size: 32.0)),
            ],
          )
        ],
      ),
    );
  }

  //didChangeDependencies会紧跟在initState之后被调用,便于进行state初始化
  @override
  void didChangeDependencies() {
    //重写State类的didChangeDependencies方法,
    //在里面通过readState从PageStorage读取并恢复被保存的状态。
    var p = PageStorage.of(context).readState(context);
    if (p != null) {
      _params = p;
    } else {
      _params = Page1Params();
    }
    super.didChangeDependencies();
  }
}

class Page2 extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Container();
  }
}

class TabInfo {
  String label;
  Widget widget;

  TabInfo(this.label, this.widget);
}

class MyCustomApp extends StatelessWidget {
  final List<TabInfo> _tabs = [
    //创建widget时指定key
    TabInfo("FIRST", Page1(key: PageStorageKey<String>("key_Page1"))),
    TabInfo("SECOND", Page2())
  ];

  @override
  Widget build(BuildContext context) {
    return DefaultTabController(
      length: _tabs.length,
      child: Scaffold(
        appBar: AppBar(
          title: Text('Tab Controller'),
          bottom: PreferredSize(
            child: TabBar(
              isScrollable: true,
              tabs: _tabs.map((TabInfo tab) {
                return Tab(text: tab.label);
              }).toList(),
            ),
            preferredSize: Size.fromHeight(50.0),
          ),
        ),
        body: TabBarView(
          children: _tabs.map((tab) => tab.widget).toList(),
        ),
      ),
    );
  }
}

void main() => runApp(
      MaterialApp(
        debugShowCheckedModeBanner: false,
        home: MyCustomApp(),
      ),
    );

上述代码示例中页面切换时通过PageStorage保存并恢复状态。

GlobalKey

GlobalKey 适用于在应用程序的全局范围内唯一标识的 Widgets。GlobalKey不应该在每次build的时候重新重建, 它是State拥有的长期存在的对象。GlobalKey 能够跨 Widget 访问状态。

默认实现是LabeledGlobalKey,每次创建都是新的GlobalKey。GlobalKey都保存在BuildOwner类中的一个map里,map的key为GlobalKey,map的value则为GlobalKey关联的element。

Dart 复制代码
/// A key that is unique across the entire app.
///
/// Global keys uniquely identify elements. Global keys provide access to other
/// objects that are associated with those elements, such as [BuildContext].
/// For [StatefulWidget]s, global keys also provide access to [State].
///
/// Widgets that have global keys reparent their subtrees when they are moved
/// from one location in the tree to another location in the tree. In order to
/// reparent its subtree, a widget must arrive at its new location in the tree
/// in the same animation frame in which it was removed from its old location in
/// the tree.
///
/// Reparenting an [Element] using a global key is relatively expensive, as
/// this operation will trigger a call to [State.deactivate] on the associated
/// [State] and all of its descendants; then force all widgets that depends
/// on an [InheritedWidget] to rebuild.
///
/// If you don't need any of the features listed above, consider using a [Key],
/// [ValueKey], [ObjectKey], or [UniqueKey] instead.
///
/// You cannot simultaneously include two widgets in the tree with the same
/// global key. Attempting to do so will assert at runtime.
///
/// ## Pitfalls
///
/// GlobalKeys should not be re-created on every build. They should usually be
/// long-lived objects owned by a [State] object, for example.
///
/// Creating a new GlobalKey on every build will throw away the state of the
/// subtree associated with the old key and create a new fresh subtree for the
/// new key. Besides harming performance, this can also cause unexpected
/// behavior in widgets in the subtree. For example, a [GestureDetector] in the
/// subtree will be unable to track ongoing gestures since it will be recreated
/// on each build.
///
/// Instead, a good practice is to let a State object own the GlobalKey, and
/// instantiate it outside the build method, such as in [State.initState].
///
/// See also:
///
///  * The discussion at [Widget.key] for more information about how widgets use
///    keys.
@optionalTypeArgs
abstract class GlobalKey<T extends State<StatefulWidget>> extends Key {
  /// Creates a [LabeledGlobalKey], which is a [GlobalKey] with a label used for
  /// debugging.
  ///
  /// The label is purely for debugging and not used for comparing the identity
  /// of the key.
  factory GlobalKey({ String? debugLabel }) => LabeledGlobalKey<T>(debugLabel);

  /// Creates a global key without a label.
  ///
  /// Used by subclasses because the factory constructor shadows the implicit
  /// constructor.
  const GlobalKey.constructor() : super.empty();

  Element? get _currentElement => WidgetsBinding.instance.buildOwner!._globalKeyRegistry[this];

  /// The build context in which the widget with this key builds.
  ///
  /// The current context is null if there is no widget in the tree that matches
  /// this global key.
  BuildContext? get currentContext => _currentElement;

  /// The widget in the tree that currently has this global key.
  ///
  /// The current widget is null if there is no widget in the tree that matches
  /// this global key.
  Widget? get currentWidget => _currentElement?.widget;

  /// The [State] for the widget in the tree that currently has this global key.
  ///
  /// The current state is null if (1) there is no widget in the tree that
  /// matches this global key, (2) that widget is not a [StatefulWidget], or the
  /// associated [State] object is not a subtype of `T`.
  T? get currentState {
    final Element? element = _currentElement;
    if (element is StatefulElement) {
      final StatefulElement statefulElement = element;
      final State state = statefulElement.state;
      if (state is T) {
        return state;
      }
    }
    return null;
  }
}

一个泛型类型,T必须要继承自State<StatefulWidget>,可以说这个GlobalKey专门用于StatefulWidget组件。GlobalKey包含一个Map,key和value分别为自身和Element。GlobalKey会在组件Mount阶段把自身放到一个Map里面缓存起来。

GlobalKey的注意点:

  • 当拥有GlobalKey的widget从树的一个位置上移动到另一个位置时,需要reparent它的子树。为了reparent它的子树,必须在一个动画帧里完成从旧位置移动到新位置的操作。
  • 上面的reparent操作代价昂贵,因为要调用所有相关联的State和所有子节点的deactive方法,并且所有依赖InheritedWidget的widget去重建。
  • 不要在build方法中创建GlobalKey,性能不好,容易出现异常。比如子树里的GestureDetector可能会由于每次build时重新创建GlobalKey而无法继续追踪手势事件。
  • GlobalKey提供了访问其关联的Element和State的方法。

GlobalKey使用场景:

获取或者修改Widget的状态(State)
实现局部刷新
查找Widget
维持Widget的持久性

参考:

Keys in Flutter. Working with Flutter, many times we... | by Karmacharyasamriddhi | codingmountain | Medium

相关推荐
孤鸿玉9 小时前
Fluter InteractiveViewer 与ScrollView滑动冲突问题解决
flutter
叽哥15 小时前
Flutter Riverpod上手指南
android·flutter·ios
BG1 天前
Flutter 简仿Excel表格组件介绍
flutter
zhangmeng1 天前
FlutterBoost在iOS26真机运行崩溃问题
flutter·app·swift
恋猫de小郭1 天前
对于普通程序员来说 AI 是什么?AI 究竟用的是什么?
前端·flutter·ai编程
卡尔特斯1 天前
Flutter A GlobalKey was used multipletimes inside one widget'schild list.The ...
flutter
w_y_fan2 天前
Flutter 滚动组件总结
前端·flutter
醉过才知酒浓2 天前
Flutter Getx 的页面传参
flutter
火柴就是我3 天前
flutter 之真手势冲突处理
android·flutter
Speed1233 天前
`mockito` 的核心“打桩”规则
flutter·dart