В предыдущем параграфе мы рассмотрели виджеты стандартной библиотеки. Наверняка вы обратили внимание, что в некоторых случаях мы передавали в конструктор родителького класса параметр key:
1class SnackbarExample extends StatefulWidget {
2 const SnackbarExample({super.key});
3
4}
Этот параметр ещё называют ключом. В этом параграфе мы разберём, что такое ключи, а также когда, зачем и как их стоит использовать.
Что такое Key
Key — это уникальный идентификатор виджета. Он используется при перестроении дерева виджетов, чтобы отличать друг от друга одинаковые по типу виджеты.
Ключи необходимо использовать, когда у одного родителя (например, Column или Row) несколько дочерних виджетов одного типа, и у этих виджетов есть состояние. Если мы поменяем такие виджеты местами или удалим один из них, фреймворк не сможет отличить один виджет от другого, так как у них одинаковый тип.
Давайте рассмотрим наглядный пример: у нас есть Column, в котором находятся два Stateful-виджета MyStatefulWidget, и мы удаляем один из них.
Какой из виджетов мы удалили? В данном случае фреймворк будет ориентироваться на позицию виджетов внутри children и удалит второй, но, если мы хотим удалить первый, нужно, чтобы фреймворк их мог различить. Именно для этого используются ключи.
Пример использования ключей
Давайте рассмотрим следующий пример. У нас есть виджет ColorBlock — это StatefulWidget, который представляет собой цветной Container. При инициализации для Container задаётся переданный извне цвет. По тапу на ColorBlock цвет меняется по определённой логике, а именно — параметры цвета red/green /blue инкрементируются.
Пример (здесь и далее мы скрыли под катом часть объёмных примеров для удобства чтения)
1class ColorBlock extends StatefulWidget {
2 final Color color;
3
4 const ColorBlock({
5 required this.color,
6 Key? key,
7 }) : super(key: key);
8
9 @override
10 State<ColorBlock> createState() => _ColorBlockState();
11}
12
13class _ColorBlockState extends State<ColorBlock> {
14 late Color color;
15
16 @override
17 void initState() {
18 super.initState();
19 color = widget.color;
20 }
21
22 void incrementColor() {
23 setState(() {
24 color = Color.fromARGB(
25 color.alpha,
26 (color.red + 4) % 256,
27 (color.green + 4) % 256,
28 (color.blue + 4) % 256,
29 );
30 });
31 }
32
33 @override
34 Widget build(BuildContext context) {
35 return GestureDetector(
36 onTap: incrementColor,
37 child: Container(
38 color: color,
39 width: 100.0,
40 height: 100.0,
41 ),
42 );
43 }
44}
Также у нас есть виджет MyHomePage — это StatefulWidget, который представляет собой экран. На экране располагается список виджетов ColorBlock, а также есть виджет FloatingActionButton, по тапу на который наш список виджетов ColorBlock разворачивается.
Ещё пример
1class MyHomePage extends StatefulWidget {
2 const MyHomePage({Key? key, required this.title}) : super(key: key);
3
4 final String title;
5
6 @override
7 State<MyHomePage> createState() => _MyHomePageState();
8}
9
10class _MyHomePageState extends State<MyHomePage> {
11 var _colorBlocks = <Widget>[];
12
13 @override
14 void initState() {
15 super.initState();
16 final random = Random().nextInt(Colors.primaries.length - 2);
17 _colorBlocks = [
18 ColorBlock(
19 color: Colors.primaries[random],
20 ),
21 ColorBlock(
22 color: Colors.primaries[random + 1],
23 ),
24 ColorBlock(
25 color: Colors.primaries[random + 2],
26 ),
27 ];
28 }
29
30 @override
31 Widget build(BuildContext context) {
32 return Scaffold(
33 appBar: AppBar(
34 title: Text(widget.title),
35 ),
36 body: Column(children: _colorBlocks),
37 floatingActionButton: FloatingActionButton(
38 child: const Icon(Icons.swap_vert),
39 onPressed: () {
40 setState(() {
41 _colorBlocks = _colorBlocks.reversed.toList();
42 });
43 },
44 ),
45 );
46 }
47}
Запустив приложение, мы увидим следующее:
Когда мы нажимаем на виджет ColorBlock, цвет ожидаемо меняется, а если нажать на виджет FloatingActionButton, ничего не происходит, хотя мы меняем последовательность ColorBlock и вызываем setState.
👉 Это происходит потому, что фреймворк не может отличить виджеты
ColorBlockдруг от друга и определить, что их последовательность поменялась.
Давайте воспользуемся ключами и поправим данный пример. При создании виджетов ColorBlock будем передавать туда уникальный Key. В классе Key есть factory-конструктор, который принимает String и создаёт ключ.
1_colorBlocks = [
2 ColorBlock(
3 color: Colors.primaries[random],
4 key: Key(Colors.primaries[random].toString()),
5 ),
6 ColorBlock(
7 color: Colors.primaries[random + 1],
8 key: Key(Colors.primaries[random + 1].toString()),
9 ),
10 ColorBlock(
11 color: Colors.primaries[random + 2],
12 key: Key(Colors.primaries[random + 2].toString()),
13 ),
14];
Теперь, когда мы нажимаем на FloatingActionButton, всё работает как ожидается:
Может возникнуть ощущение, что важно добавлять ключи к StatefulWidget или к первому StatefulWidget среди дочерних виджетов, но это не так. Давайте обернём наши ColorBlock в Padding.
Вероятно, вы ожидаете, что к нашим ColorBlock просто добавится Padding, а в остальном поведение не изменится.
Пример
1_colorBlocks = [
2 Padding(
3 padding: const EdgeInsets.all(8.0),
4 child: ColorBlock(
5 color: Colors.primaries[random],
6 key: Key(Colors.primaries[random].toString()),
7 ),
8 ),
9 Padding(
10 padding: const EdgeInsets.all(8.0),
11 child: ColorBlock(
12 color: Colors.primaries[random + 1],
13 key: Key(Colors.primaries[random + 1].toString()),
14 ),
15 ),
16 Padding(
17 padding: const EdgeInsets.all(8.0),
18 child: ColorBlock(
19 color: Colors.primaries[random + 2],
20 key: Key(Colors.primaries[random + 2].toString()),
21 ),
22 ),
23];
Однако, когда мы нажимаем на виджет FloatingActionButton, все наши ColorBlock вместе с Padding меняются местами, но состояние ColorBlock теряется.
Всё дело в том, что при перестановке Padding фреймворк переиспользует те же самые Padding, для него ничего не меняется, но дочерний виджет у каждого Padding меняет ключ, и поэтому State создаётся заново. Это легко исправить, указав ключ у самого Padding.
Пример
1_colorBlocks = [
2 Padding(
3 key: Key(Colors.primaries[random].toString()),
4 padding: const EdgeInsets.all(8.0),
5 child: ColorBlock(
6 color: Colors.primaries[random],
7 ),
8 ),
9 Padding(
10 key: Key(Colors.primaries[random + 1].toString()),
11 padding: const EdgeInsets.all(8.0),
12 child: ColorBlock(
13 color: Colors.primaries[random + 1],
14 ),
15 ),
16 Padding(
17 key: Key(Colors.primaries[random + 2].toString()),
18 padding: const EdgeInsets.all(8.0),
19 child: ColorBlock(
20 color: Colors.primaries[random + 2],
21 ),
22 ),
23];
И вот что у нас получилось:
Типы ключей
Итак, мы разобрались, для чего нужны ключи. Давайте теперь рассмотрим, какие они бывают.
Есть множество ключей, и они делятся на две группы: LocalKey и GlobalKey, выглядит это следующим образом:
Key— это абстрактный класс, общий предок для всех ключей, у него естьfactoryконструктор, который создаётValueKey<String>;LocalKey— это абстрактный класс, наследуется отKey, общий предок для всех локальных ключей. Особенность локальных ключей в том, что они должны быть уникальны для всех виджетов с одним родителем;ValueKey**<T>**— локальный ключ, уникальность которого определяется по значению объектаT, это значит, что для корректной работы ключа необходимо, чтобы были переопределеныhashCodeи оператор сравнения==у классаT;ObjectKey— локальный ключ, уникальность которого определяется не по значению объекта, а по ссылке на него;UniqueKey— уникальный локальный ключ, который равен только самому себе.
UniqueKey может быть использован, когда нам нужно на каждое перестроение дерева виджетов пересоздавать поддерево, либо когда у нас нет какого-то конкретного value, с которым мы можем ассоциировать виджеты.
В примере выше мы фактически использовали ValueKey (именно он создаётся с помощью factory-конструктора Key), но могли использовать любой из локальных ключей, в том числе UniqueKey. То, какой из ключей использовать, зависит от задачи и данных, которые у нас есть.
Давайте рассмотрим пример использования UniqueKey, для этого изменим наш пример следующим образом:
- вместо
ColorBlockнапишемRandomColorBlock, который будет случайным образом определять свой цвет вinitStateи больше не будет его менять; - в
initStateвиджетаMyHomePageбудем создавать список изRandomColorBlock; - по тапу на
FloatingActionButtonвместо разворота списка будем его обновлять и вызыватьsetState.
Поскольку сама структура дерева виджетов у нас меняться не будет, передадим UniqueKey в наши RandomColorBlock при создании — благодаря этому фреймворк поймёт, что это другие RandomColorBlock.
Пример
1class RandomColorBlock extends StatefulWidget {
2 const RandomColorBlock({Key? key}) : super(key: key);
3
4 @override
5 State<RandomColorBlock> createState() => _RandomColorBlockState();
6}
7
8class _RandomColorBlockState extends State<RandomColorBlock> {
9 late Color color;
10
11 @override
12 void initState() {
13 super.initState();
14 final random = Random();
15 color = Color.fromARGB(
16 255,
17 random.nextInt(256),
18 random.nextInt(256),
19 random.nextInt(256),
20 );
21 }
22
23 @override
24 Widget build(BuildContext context) {
25 return Container(
26 color: color,
27 width: 100.0,
28 height: 100.0,
29 );
30 }
31}
32
33class MyHomePage extends StatefulWidget {
34 const MyHomePage({Key? key, required this.title}) : super(key: key);
35
36 final String title;
37
38 @override
39 State<MyHomePage> createState() => _MyHomePageState();
40}
41
42class _MyHomePageState extends State<MyHomePage> {
43 var _colorBlocks = <Widget>[];
44
45 @override
46 void initState() {
47 super.initState();
48 _generateRandomColorBlocks();
49 }
50
51 void _generateRandomColorBlocks() {
52 setState(() {
53 _colorBlocks = List.generate(
54 3,
55 (_) => RandomColorBlock(key: UniqueKey()),
56 );
57 });
58 }
59
60 @override
61 Widget build(BuildContext context) {
62 return Scaffold(
63 appBar: AppBar(title: Text(widget.title)),
64 body: Column(children: _colorBlocks),
65 floatingActionButton: FloatingActionButton(
66 onPressed: _generateRandomColorBlocks,
67 child: const Icon(Icons.swap_vert),
68 ),
69 );
70 }
71}
Вот как это выглядит:
GlobalKey — это абстрактный класс, наследуется от Key, общий предок для всех глобальных ключей. Особенность глобальных ключей в том, что они должны быть уникальны для всего приложения.
LabeledGlobalKey — своего рода глобальный аналог UniqueKey, уникален и равен сам себе. У ключа есть debug label, который никак не влияет на его уникальность и используется только для целей дебага.
GlobalObjectKey — своего рода глобальный аналог ObjectKey, равенство / уникальность определяются по инстансу Object (то есть фактически по ссылке в памяти).
Глобальные ключи могут использоваться в двух целях:
- переместить виджет и его
Stateиз одной части дерева виджетов в совершенно другое место в дереве виджетов (иными словами, поменять родителя виджета), при этом при перемещении виджет с нашим ключом не должен покидать дерево виджетов и не должен быть использован в двух разных местах одновременно; - получить доступ к
State/BuildContextStatefulWidgetиз любой точки приложения.
Используем GlobalKey
Представим, что в примере со списком из ColorBlock вёрстка в portrait- и landscape-ориентации сильно различается: в landscape добавляется дополнительный Padding к Column.
Пример
1@override
2Widget build(BuildContext context) {
3 return OrientationBuilder(builder: (context, orientation) {
4 return Scaffold(
5 appBar: AppBar(
6 title: Text(widget.title),
7 ),
8 body: orientation == Orientation.portrait
9 ? Column(
10 children: _colorBlocks,
11 )
12 : Padding(
13 padding: const EdgeInsets.all(8.0),
14 child: Column(
15 children: _colorBlocks,
16 ),
17 ),
18 floatingActionButton: FloatingActionButton(
19 child: const Icon(Icons.swap_vert),
20 onPressed: () {
21 setState(() {
22 _colorBlocks = _colorBlocks.reversed.toList();
23 });
24 },
25 ),
26 );
27 });
28}
Выглядит это так:
В таком случае, поскольку при повороте экрана у виджета Scaffold меняется виджет body, у нас будет пересоздаваться всё поддерево body. Чтобы решить эту проблему, мы можем воспользоваться глобальным ключом. Давайте добавим его к Column и посмотрим, что произойдёт.
Пример
1class _MyHomePageState extends State<MyHomePage> {
2 final _columnKey = GlobalKey();
3
4 @override
5Widget build(BuildContext context) {
6 return OrientationBuilder(builder: (context, orientation) {
7 return Scaffold(
8 appBar: AppBar(
9 title: Text(widget.title),
10 ),
11 body: orientation == Orientation.portrait
12 ? Column(
13 key: _columnKey,
14 children: _colorBlocks,
15 )
16 : Padding(
17 padding: const EdgeInsets.all(8.0),
18 child: Column(
19 key: _columnKey,
20 children: _colorBlocks,
21 ),
22 ),
23 floatingActionButton: FloatingActionButton(
24 child: const Icon(Icons.swap_vert),
25 onPressed: () {
26 setState(() {
27 _colorBlocks = _colorBlocks.reversed.toList();
28 });
29 },
30 ),
31 );
32 });
33 }
34}
Вот что произойдёт:
Как видите, GlobalKey позволяет поменять родителя Column и смонтировать его в любом другом месте без потери состояния. Происходит это за один фрейм, и Column не уходит из дерева виджетов.
Также необходимо следить за тем, чтобы в один фрейм один и тот же глобальный ключ не был использован более чем в одном виджете. Иначе мы получим следующую ошибку: Duplicate GlobalKey detected in widget tree.
👉 Очень важно понимать, что использование глобальных ключей для сохранения состояния и перемещения виджета не самая дешёвая операция, поэтому не рекомендуется без необходимости использовать для этого глобальные ключи. По возможности желательно выносить состояние из виджета в слой презентационной логики или в инхеритед-виджеты.
В примере выше мы могли вместо добавления виджета Padding в landscape добавить его в обеих ориентациях и только менять значение параметра padding. Так и стоит поступать, если есть такая возможность.
Пример
1@override
2Widget build(BuildContext context) {
3 return OrientationBuilder(builder: (context, orientation) {
4 return Scaffold(
5 appBar: AppBar(
6 title: Text(widget.title),
7 ),
8 body: Padding(
9 padding: orientation == Orientation.portrait
10 ? EdgeInsets.zero
11 : const EdgeInsets.all(8.0),
12 child: Column(
13 children: _colorBlocks,
14 ),
15 ),
16 floatingActionButton: FloatingActionButton(
17 child: const Icon(Icons.swap_vert),
18 onPressed: () {
19 setState(() {
20 _colorBlocks = _colorBlocks.reversed.toList();
21 });
22 },
23 ),
24 );
25 });
26}
Теперь давайте рассмотрим пример с получением доступа к State. На нашем экране сейчас есть три ColorBlock и FloatingActionButton, который их разворачивает. Давайте воспользуемся GlobalKey и поменяем логику следующим образом: будем по тапу на FloatingActionButton инкрементировать все ColorBlock, то есть вызывать incrementColor у каждого ColorBlock. Для этого нужно внести следующие изменения:
- в каждый
ColorBlockпередатьGlobalKey<_ColorBlockState>; - по тапу на
FloatingActionButtonпробегаться по всем ключам, получать доступ кStateчерезkey.currentStateи вызыватьincrementColor.
Пример
1class _MyHomePageState extends State<MyHomePage> {
2 final _colorBlockKeys = <GlobalKey<_ColorBlockState>>[];
3}
1@override
2void initState() {
3 super.initState();
4 final random = Random().nextInt(Colors.primaries.length - 2);
5 _colorBlocks = [
6 ColorBlock(
7 key: GlobalKey<_ColorBlockState>(),
8 color: Colors.primaries[random],
9 ),
10 ColorBlock(
11 key: GlobalKey<_ColorBlockState>(),
12 color: Colors.primaries[random + 1],
13 ),
14 ColorBlock(
15 key: GlobalKey<_ColorBlockState>(),
16 color: Colors.primaries[random + 2],
17 ),
18 ];
19 for (final colorBlock in _colorBlocks) {
20 final key = colorBlock.key;
21 if (key is GlobalKey<_ColorBlockState>) {
22 _colorBlockKeys.add(key);
23 }
24 }
25}
1floatingActionButton: FloatingActionButton(
2 child: const Icon(Icons.swap_vert),
3 onPressed: () {
4 for (final key in _colorBlockKeys) {
5 key.currentState?.incrementColor();
6 }
7 },
8),
Вот что получится:
Таким образом GlobalKey позволяет получить доступ к State из любой точки приложения.
Этим также не стоит злоупотреблять — управлять состоянием виджетов лучше с помощью отдельных классов с бизнес-логикой / презентационной логикой. Подробнее про управление состоянием мы расскажем в следующих параграфах.
Тем не менее бывают кейсы, когда глобальные ключи удобны. Например, для навигации. Об этом мы также расскажем подробнее в следующих параграфах.
Итак, в этом параграфе мы рассмотрели, что такое ключи, какие они бывают, когда, зачем и, главное, как их использовать.
Чтобы закрепить новые знания, советуем посмотреть видео по теме от команды Flutter, а ещё — пройти квиз.
И переходите к следующему параграфу — в нём мы подробно разберём лейаут во Flutter: процесс при котором определяются размеры и позиции виджетов на экране.
