Параметр key можно найти практически в каждом конструкторе виджета, но используют этот параметр при разработке достаточно редко. Keys сохраняют состояние при перемещении виджетов в дереве виджетов. На практике это означает, что они могут быть полезны для сохранения местоположения прокрутки пользователя или сохранения состояния при изменении коллекции.


Данная статья адаптирована из следующего видео. Если вы предпочитаете слушать / смотреть, а не читать, то видео предоставит вам тот же материал.


Секретная информация о keys


Большую часть времени… keys вам не нужны. В общем, нет никакого вреда в их добавлении, но в этом также нет необходимости, так как они просто занимают место, как новое ключевое слово или объявление типов с обеих сторон новой переменной (я о тебе, Map<Foo, Bar> aMap = Map<Foo, Bar>()).


Но если вы обнаружите, что добавляете, удаляете или переставляете в коллекции виджеты, которые содержат некоторое состояние и имеют один тип, то стоит обратить внимание на keys!

Чтобы продемонстрировать, почему вам нужны keys при изменении коллекции виджетов, я написала чрезвычайно простое приложение с двумя разноцветными виджетами, которые меняются местами при нажатии на кнопку:



В данной версии приложения у меня два виджета случайного цвета без состояния (StatelessWidget) в Row и виджете PositionedTiles с состоянием (StatefulWidget), чтобы хранить в нем порядок цветных виджетов. Когда я нажимаю кнопку FloatingActionButton внизу, цветные виджеты правильно меняют своё место в списке:


void main() => runApp(new MaterialApp(home: PositionedTiles()));

class PositionedTiles extends StatefulWidget {
 @override
 State<StatefulWidget> createState() => PositionedTilesState();
}

class PositionedTilesState extends State<PositionedTiles> {
 List<Widget> tiles = [
   StatelessColorfulTile(),
   StatelessColorfulTile(),
 ];

 @override
 Widget build(BuildContext context) {
   return Scaffold(
     body: Row(children: tiles),
     floatingActionButton: FloatingActionButton(
         child: Icon(Icons.sentiment_very_satisfied), onPressed: swapTiles),
   );
 }

 swapTiles() {
   setState(() {
     tiles.insert(1, tiles.removeAt(0));
   });
 }
}

class StatelessColorfulTile extends StatelessWidget {
 Color myColor = UniqueColorGenerator.getColor();
 @override
 Widget build(BuildContext context) {
   return Container(
       color: myColor, child: Padding(padding: EdgeInsets.all(70.0)));
 }
}

Но если мы добавим в наши цветные виджеты состояние (сделаем их StatefulWidget) и будем в них хранить цвет, то при клике по кнопке выглядит так, как будто ничего не происходит:



List<Widget> tiles = [
   StatefulColorfulTile(),
   StatefulColorfulTile(),
];

...
class StatefulColorfulTile extends StatefulWidget {
 @override
 ColorfulTileState createState() => ColorfulTileState();
}

class ColorfulTileState extends State<ColorfulTile> {
 Color myColor;

 @override
 void initState() {
   super.initState();
   myColor = UniqueColorGenerator.getColor();
 }

 @override
 Widget build(BuildContext context) {
   return Container(
       color: myColor,
       child: Padding(
         padding: EdgeInsets.all(70.0),
       ));
 }
}

В качестве разъяснения: код, указанный выше, глючит в том, что он не показывает обмен цветами, когда пользователь нажимает кнопку. Чтобы исправить эту ошибку, надо добавить key параметр в цветные StatefulWidget виджеты, а затем виджеты поменяются местами, как мы хотим:



List<Widget> tiles = [
  StatefulColorfulTile(key: UniqueKey()), // Keys added here
  StatefulColorfulTile(key: UniqueKey()),
];

...
class StatefulColorfulTile extends StatefulWidget {
  StatefulColorfulTile({Key key}) : super(key: key);  // NEW CONSTRUCTOR

  @override
  ColorfulTileState createState() => ColorfulTileState();
}

class ColorfulTileState extends State<ColorfulTile> {
  Color myColor;

  @override
  void initState() {
    super.initState();
    myColor = UniqueColorGenerator.getColor();
  }

  @override
  Widget build(BuildContext context) {
    return Container(
        color: myColor,
        child: Padding(
          padding: EdgeInsets.all(70.0),
        ));
  }
}

Но это необходимо, только если у вас есть виджеты с состоянием в поддереве, которое вы меняете. Если все поддерево виджета в вашей коллекции не имеет состояния, ключи не нужны.
Вот так! В общем-то все, что вам нужно знать, чтобы использовать keys во Flutter. Но если вы хотите несколько углубиться в происходящее…




Разбираемся, почему keys иногда необходимы


Вы все еще здесь, да? Ну, тогда подходите ближе, чтобы узнать истинную природу деревьев элементов и виджетов, чтобы стать Flutter Магом! Ухахаха! Ха-ха! Ха-ха! ГМ, извините.


Как вы знаете, внутри для каждого виджета Flutter строит соответствующий элемент. Так же, как Flutter строит дерево виджетов, он также создает дерево элементов (ElementTree). ElementTree чрезвычайно просто, содержит только информацию о типе каждого виджета и ссылку на дочерние элементы. Вы можете думать о думать ElementTree как о скелете вашего Flutter приложения. Он показывает структуру вашего приложения, но всю дополнительную информацию можно посмотреть по ссылке на исходный виджет.


Row виджет в приведенном выше примере содержит набор упорядоченных слотов для каждого из его дочерних элементов. Когда мы меняем порядок цветных виджетов в Row, Flutter ходит по ElementTree, чтобы проверить, является ли структура скелета приложения такой же.



Проверка начинается с RowElement, а затем переходит к дочерним элементам. ElementTree проверяет, что у нового виджета те же тип и key, что и у старого, и если это так, то элемент обновляет свою ссылку на новый виджет. В версии кода без состояния у виджетов нет key, поэтому Flutter просто проверяет только тип. (Если слишком много информации за раз, то посмотрите анимированную диаграмму выше.)


Ниже ElementTree для виджетов с состоянием выглядит немного иначе. Есть виджеты и элементы, как и раньше, но также есть пара объектов состояния для виджетов, и информация о цвете хранится в них, а не в самих виджетах.



В случае цветных StatefulWidget виджетов без key, когда я меняю порядок двух виджетов, Flutter ходит по ElementTree, проверяет тип RowWidget и обновляет ссылку. Затем элемент цветного виджета проверяет, что соответствующий виджет имеет тот же тип, и обновляет ссылку. То же самое происходит со вторым виджетом. Поскольку Flutter использует ElementTree и соответствующее ему состояние, чтобы определить, что на самом деле отображать на вашем устройстве, с нашей точки зрения, похоже, что виджеты не поменялись местами!



В исправленной версии кода в цветных виджетах с состоянием в конструкторе я определила свойство key. Теперь, если мы поменяем виджеты в Row, то по типу они совпадут как и раньше, но значения key у цветного виджета и у соответствующего элемента в ElementTree будут разными. Это заставляет Flutter деактивировать эти элементы цветных виджетов и удалить ссылки на них в ElementTree, начиная с первого, у которого не совпадает key.



Затем Flutter ищет для дочерних виджетов в Row элемент в ElementTree с соответствующим key. При совпадении добавляет ссылку в элемент на виджет. Flutter делает для каждого дочернего элемента без ссылки. Теперь Flutter отобразит то, что мы ожидаем, цветные виджеты меняются местами, когда я нажимаю кнопку.


Таким образом, keys полезны, если вы изменяете порядок или количество виджетов с состоянием в коллекции. В данном я примере я сохранила цвет. Однако, часто состояние бывает не таким явным. Воспроизведение анимации, отображение введенных пользователем данных и прокрутка местоположения, – всё имеет состояние.




Когда мне использовать keys?


Короткий ответ: если вам нужно добавить keys в приложение, то следует добавить их в верхней части поддерева виджетов с состоянием, которое необходимо сохранить.


Распространенная ошибка, которую я видела, — люди думают, что им нужно определить key только для первого виджета с состоянием, но есть нюансы. Не верите мне? Чтобы показать, в какие неприятности мы можем попасть, я обернула мои цветные виджеты в Padding виджеты, при этом оставив ключи для цветных виджетов.


void main() => runApp(new MaterialApp(home: PositionedTiles()));

class PositionedTiles extends StatefulWidget {
  @override
  State<StatefulWidget> createState() => PositionedTilesState();
}

class PositionedTilesState extends State<PositionedTiles> {
  // Stateful tiles now wrapped in padding (a stateless widget) to increase height 
  // of widget tree and show why keys are needed at the Padding level.
  List<Widget> tiles = [
    Padding(
      padding: const EdgeInsets.all(8.0),
      child: StatefulColorfulTile(key: UniqueKey()),
    ),
    Padding(
      padding: const EdgeInsets.all(8.0),
      child: StatefulColorfulTile(key: UniqueKey()),
    ),
  ];

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Row(children: tiles),
      floatingActionButton: FloatingActionButton(
          child: Icon(Icons.sentiment_very_satisfied), onPressed: swapTiles),
    );
  }

  swapTiles() {
    setState(() {
      tiles.insert(1, tiles.removeAt(0));
    });
  }
}

class StatefulColorfulTile extends StatefulWidget {
  StatefulColorfulTile({Key key}) : super(key: key);

  @override
  ColorfulTileState createState() => ColorfulTileState();
}

class ColorfulTileState extends State<ColorfulTile> {
  Color myColor;

  @override
  void initState() {
    super.initState();
    myColor = UniqueColorGenerator.getColor();
  }

  @override
  Widget build(BuildContext context) {
    return Container(
        color: myColor,
        child: Padding(
          padding: EdgeInsets.all(70.0),
        ));
  }
}

Теперь по нажатию на кнопку виджеты получают совершенно случайные цвета!



Так выглядит дерево виджетов и ElementTree с добавленными Padding виджетами:



Когда мы меняем позиции дочерних виджетов, алгоритм поиска соответствия между элементами и виджетами смотрит на один уровень в дереве элементов. На диаграмме дочерние элементы дочерних элементы затемнены, чтобы ничто не отвлекало от первого уровня. На этом этом уровне все совпадает правильно.



На втором уровне Flutter замечает, что key цветного элемента не соответствует key виджета, поэтому он деактивирует этот элемент, отбрасывая удаляя все ссылки на него. keys, которые мы используем в этом примере, – LocalKeys. Это означает, что при сопоставлении виджета с элементами Flutter ищет совпадения keys только на определенном уровне дерева.


Поскольку он не может найти элемент цветного виджета на этом уровне с соответствующим key, он создает новый и инициализирует новое состояние, делая в данном случае виджет оранжевым!



Если мы определим keys для Padding виджетов:


void main() => runApp(new MaterialApp(home: PositionedTiles()));

class PositionedTiles extends StatefulWidget {
  @override
  State<StatefulWidget> createState() => PositionedTilesState();
}

class PositionedTilesState extends State<PositionedTiles> {
  List<Widget> tiles = [
    Padding(
      // Place the keys at the *top* of the tree of the items in the collection.
      key: UniqueKey(), 
      padding: const EdgeInsets.all(8.0),
      child: StatefulColorfulTile(),
    ),
    Padding(
      key: UniqueKey(),
      padding: const EdgeInsets.all(8.0),
      child: StatefulColorfulTile(),
    ),
  ];

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Row(children: tiles),
      floatingActionButton: FloatingActionButton(
          child: Icon(Icons.sentiment_very_satisfied), onPressed: swapTiles),
    );
  }

  swapTiles() {
    setState(() {
      tiles.insert(1, tiles.removeAt(0));
    });
  }
}

class StatefulColorfulTile extends StatefulWidget {
  StatefulColorfulTile({Key key}) : super(key: key);

  @override
  ColorfulTileState createState() => ColorfulTileState();
}

class ColorfulTileState extends State<ColorfulTile> {
  Color myColor;

  @override
  void initState() {
    super.initState();
    myColor = UniqueColorGenerator.getColor();
  }

  @override
  Widget build(BuildContext context) {
    return Container(
        color: myColor,
        child: Padding(
          padding: EdgeInsets.all(70.0),
        ));
  }
}

Flutter замечает проблему и обновляет ссылки правильно, как это было в нашем предыдущем примере. Порядок во Вселенной восстановлен.





Какой тип Key мне следует использовать?


Flutter APIs дали нам на выбор несколько Key классов. Тип key, который вы должны использовать, зависит от того, какая отличительная характеристика у элементов, нуждающихся в keys. Посмотрите на информацию, которую вы храните в соответствующих виджетов.


Рассмотрим следующее To-do приложение[1], где вы можете изменить порядок элементов в списке задач на основе приоритета, а когда закончите, вы можете их удалить.



ValueKey
В данном случае можно ожидать, что текст пункта на выполнение будет постоянным и уникальным. Если это так, то, вероятно, это хороший кандидат для ValueKey, где текст является "значением".


return TodoItem(
  key: ValueKey(todo.task),
  todo: todo,
  onDismissed: (direction) => _removeTodo(context, todo),
);

ObjectKey
В другом случае, у вас может быть приложение "Адресная книга", в котором перечислены сведения о каждом пользователе. В этом случае каждый дочерний виджет хранит более сложную комбинацию данных. Любое из отдельных полей, например имя или день рождения, может совпадать с другой записью, но комбинация уникальна. В этом случае скорее всего лучше всего подходит ObjectKey.



UniqueKey
Если у вас есть несколько виджетов в коллекции с одинаковым значением или если вы хотите действительно убедиться, что каждый виджет отличается от всех других, то можно использовать UniqueKey. Я использовала UniqueKey в примере приложения для переключения цветов, потому что у нас не было других постоянных данных, которые хранились бы в наших виджетах, и мы не знали, какой цвет будет у виджета при создании.


Однако одна вещь, которую вы не хотите использовать в качестве вашего key, — это случайное число. Каждый раз, когда виджет будет создан, будет генерироваться новое случайное число, и вы потеряете согласованность между фреймами. При таком сценарии вы можете вообще не использовать keys!


PageStorageKeys
PageStorageKeys – это специализированные keys, которые содержит текущее состояние скролла, чтобы приложение могло сохранить его для последующего использования.



GlobalKeys
Есть два варианта использования GlobalKeys: они позволяют виджетам менять родителей в любом месте приложения без потери состояния и могут использоваться для доступа к информации о другом виджете в совершенно другой части дерева виджетов. В качестве примера первого сценария можно представить, что вы хотите показать один и тот же виджет на двух разных экранах, но с одинаковым состоянием, для того, чтобы данные виджета сохранились, вы будете использовать GlobalKey. Во втором случае может возникнуть ситуация, когда вам надо проверить пароль, но при этом вы не хотите делиться информацией о состоянии с другими виджетами в дереве. GlobalKeys также могут быть полезны для тестирования, используя key для доступа к конкретному виджету и запроса информации о его состоянии.



Часто (но не всегда!) GlobalKeys немного похожи на глобальные переменные. Зачастую их можно заменить на использование InheritedWidgets или что-то вроде Redux, или шаблона BLoC.




Краткое заключение


В общем используйте Keys, если вы хотите сохранить состояние между поддеревьями виджетов. Это чаще всего происходит при изменении коллекции виджетов одного типа. Поместите key в верхней части поддерева виджетов, которое необходимо сохранить, и выберите тип key на основе данных, хранящихся в виджете.


Поздравляю, теперь вы на пути к тому, чтобы стать Flutter Магом! О, я сказала маг? Я имела в виду мага[2], как тот, кто пишет исходный код приложения… что почти так же хорошо. …Почти.


[1] Вдохновление для написания кода To-do приложения получено здесь
https://github.com/brianegan/flutter_architecture_samples/tree/master/vanilla
[2] Автор использует слово sorcerer и позже добавляет в него лишнюю букву до sourcerer

Комментарии (3)


  1. qwert2603
    05.04.2019 14:17
    +1

    GlobalKeys могут быть очень полезны для получения текущего State виджета. С их помощью можно запустить обновление RefreshIndicator по нажатию на кнопку или показать анимацию добавления / удаления элемента списка при использовании AnimatedList.


    1. sharpfellow Автор
      05.04.2019 14:49

      Пример работы с `RefreshIndicator` с помощью `GlobalKeys ` можно посмотреть в официальном репозитории Flutter в данном примере. Так же можно почитать об этом в официальной документации


    1. qwert2603
      05.04.2019 15:38

      Аналогично есть хорошая статья про анимирование списка.
      Это, конечно, не нативный RecyclerView (move анимации не поддерживаются), но вполне себе удобно.
      https://medium.com/flutter-community/the-magic-of-animatedlist-18afb2ba564c