Параметр 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
qwert2603
GlobalKeys
могут быть очень полезны для получения текущегоState
виджета. С их помощью можно запустить обновлениеRefreshIndicator
по нажатию на кнопку или показать анимацию добавления / удаления элемента списка при использованииAnimatedList
.sharpfellow Автор
Пример работы с `RefreshIndicator` с помощью `GlobalKeys ` можно посмотреть в официальном репозитории Flutter в данном примере. Так же можно почитать об этом в официальной документации
qwert2603
Аналогично есть хорошая статья про анимирование списка.
Это, конечно, не нативный RecyclerView (move анимации не поддерживаются), но вполне себе удобно.
https://medium.com/flutter-community/the-magic-of-animatedlist-18afb2ba564c