An overview of the
In almost all widgets, there is a key parameter. What does the key do, and when does it need to be used?
What can go wrong without a key?
Let’s look directly at an example of a counter:
class Box extends StatefulWidget {
final Color color;
Box(this.color);
@override
_BoxState createState() => _BoxState();
}
class _BoxState extends State<Box> {
int _count = 0;
@override
Widget build(BuildContext context) {
return GestureDetector(
child: Container(
width: 100,
height: 100,
color: widget.color,
alignment: Alignment.center,
child: Text(_count.toString(), style: TextStyle(fontSize: 30))), onTap: () => setState(() => ++_count), ); }}Copy the code
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Box(Colors.blue),
Box(Colors.red),
],
)
Copy the code
The running effect is as follows:
As you can see, the blue number is 3 and the red number is 5. Then change the code to switch the blue and red positions and reload the code as follows:
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Box(Colors.red),
Box(Colors.blue),
],
),
Copy the code
And then you see that the colors have been switched, but the numbers haven’t changed,
At this point, we add a new red at the end, as follows:
Then delete the first red with the number 3, which logically should be left with 5,0, as follows:
But you’ll see it’s still 3,5.
In this example, the flutter cannot be identified by the color of the Container, so there is no way to determine which one it is, so we need something like an ID to give each widget an identity, and the key is this identity.
Now let’s modify the above example:
class Box extends StatefulWidget {
final Color color;
Box(this.color, {Key key}) : super(key: key);
@override
_BoxState createState() => _BoxState();
}
Copy the code
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Box(Colors.blue, key: ValueKey(1)),
Box(Colors.red, key: ValueKey(2)),,)Copy the code
Add a key to the code, and you’ll see that the above problem is gone. However, if we wrap a Container around the Box, the number will become 0 when the Container is reloaded. After removing the Container, the number will also become 0.
The mapping between widgets and Elements
The definition of a widget is a description of an Element’s configuration. In other words, a widget is a configuration description, not a real render object. It is similar to XML in Android, which describes properties but is not a real View. And by looking at the source code, you can see that there is a createElement method in the widget that creates the Element.
An Element is an instance of a specific location in the Widget tree, as shown below:
The image above corresponds to the example above:
** In the absence of a key, ** if the first and second box substitutions are replaced, the second box uses the Element of the first box, so its state does not change, but because the color information is on the widget, the color changes. The final result of the substitution is that the color has changed, but the values inside have not changed.
Or if you delete the first box, the second box will use the state of the first boxElement, so the same problem will occur.
With key:
After the key is added, the widget will have a corresponding relationship with the element. If the key does not have a corresponding relationship, it will be searched again at the same level. If it does not have a corresponding relationship, the widget or element will be deleted
Explain the remaining questions above
The state is gone after the Container is nested outside the Box. This is because the type is consistent before the key is determined, and then the key is determined.
Because the types are inconsistent, the previous State cannot be used, so a new one is created.
Note that a Widget that inherits from a StatelessWidget does not need a Key because it has no state and does not need a Key.
Keys must be unique in [Element] that have the same parent. In contrast, [GlobalKey] must be unique across the entire application. See also: [Widget.key], which discusses how widgets use keys.
Three types of LocalKey
LocalKey, which is inherited from Key, must be unique within an Element that has the same parent. That is, LocalKey must be unique within the same hierarchy.
There are three subtypes of LocalKey, so let’s look at them:
-
ValueKey
class ValueKey<T> extends LocalKey { final T value; const ValueKey(this.value); @override bool operator= = (Object other) { if(other.runtimeType ! = runtimeType)return false; return other isValueKey<T> && other.value == value; }}Copy the code
Use a value of a specific type to identify its own key. ValueKey is already used in the top example. It can accept an object of any type as a key.
== == == == == == == == == == =
-
ObjectKey
class ObjectKey extends LocalKey { const ObjectKey(this.value); 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 => hashValues(runtimeType, identityHashCode(value)); } Copy the code
The main difference between ObjectKey and ValueKey is that the comparison is not the same. First, it is the comparison type, and then the indentical method is called to compare the memory address, which is equivalent to using == directly in Java to compare. LocalKey is the equivalent of the Equals method in Java that compares values.
Note that when using the == comparison in ValueKey, if hashCode and == are not overridden, the comparison will not be equal even if the values of the objects are equal. So rewrite as much as you can!
-
UniqueKey
class UniqueKey extends LocalKey { UniqueKey(); } Copy the code
It’s obvious from the name that this is a unique key.
Each time you rebuild, the UniqueKey is unique, so the corresponding Element cannot be found and the state is lost. So when do I need this UniqueKey? We can think about it for ourselves.
Another option is to define the UniqueKey outside the build so that no state loss occurs.
GlobalKey
GlobalKey inherits from Key, which is global in scope compared to LocalKey, which is only at the current level.
Earlier we encountered a problem that if we nested a layer around a Widget, the state of the Widget would be lost as follows:
children: <Widget>[
Box(Colors.red),
Box(Colors.blue),
],
///Change it to the following and rebuild
children: <Widget>[
Box(Colors.red),
Container(child:Box(Colors.blue)),
],
Copy the code
And the reason we talked about earlier is because of the types. The state is reserved only if the type and key are the same.
So what do you do when you have this problem? This is where GlobalKey comes in. Let’s look at the chestnuts below:
class Counter extends StatefulWidget {
Counter({Key key}) : super(key: key);
@override
_CounterState createState() => _CounterState();
}
class _CounterState extends State<Counter> {
int _count = 0;
@override
Widget build(BuildContext context) {
return RaisedButton(
onPressed: () => setState(() => _count++),
child: Text("$_count", style: TextStyle(fontSize: 70))); }}Copy the code
final _globalKey = GlobalKey();
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: Center(
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Counter(),
Counter(),
],
),
),
);
}
Copy the code
In the code above, we define a Counter component, count increment when clicked, and a GlobakKey object.
Then we click on the Counter component, and after the increment, we wrap the Container around the Counter and reload the Container, and we will find that the increment has disappeared. We are not using GlobalKey at this time.
Next we use GlobalKey, as follows
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Counter(),
Counter(key: _globalKey),
],
),
)
Copy the code
Run again, and click Autoincrement, the effect is as follows:
Now let’s change the code:
Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Counter(),
Container(child: Counter(key: _globalKey)),
],
),
Copy the code
We changed the outermost Row to Column, and wrapped the last Counter with a Container component, guess what the result will be? , let’s take a look at the results:
The result is that Column is in effect, the Counter state that uses GlobalKey is not cleared, and the one that is not used is not.
When we get to Column, we will find that the previous type is Row, and then the Element of the previous Row will be thrown away, and we will create another Element. When the Element of a Row is removed, all of its internal states disappear, but when it reaches the innermost Counter, it searches for the corresponding state based on the globalkey of the Counter and continues to use it.
Chestnut:
Change the layout arrangement when switching screen orientation, and ensure that the state does not reset
Center(
child: MediaQuery.of(context).orientation == Orientation.portrait
? Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Counter(),
Container(child: Counter(key: _globalKey)),
],
)
: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Counter(),
Container(child: Counter(key: _globalKey)),
],
),
)
Copy the code
Here is the code we started with. Let’s look at the result:
As you can see from the GIF above, the state of the second Container is correct, and the state of the first Container is incorrect. Because the first Container does not use GlobalKey, we need to add GlobalKey to the first Container as well, as follows:
Center(
child: MediaQuery.of(context).orientation == Orientation.portrait
? Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Counter(key: _globalKey1),
Counter(key: _globalKey2)
],
)
: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Counter(key: _globalKey1),
Container(child: Counter(key: _globalKey2))
],
),
)
Copy the code
However, this is a bit low and can be implemented without GlobalKey.
Center(
child: Flex(
direction: MediaQuery.of(context).orientation == Orientation.portrait
? Axis.vertical
: Axis.horizontal,
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[Counter(), Counter()],
),
)
Copy the code
After using Flex, it does not change at build time, so Element is found again, so state is not lost.
However, if the internal Container is re-nested during screen switching, GlobalKey is still needed for reasons that need not be explained.
Second use of GlobalKey
Flutter is declarative programming. If a component of the page needs to be updated, the updated value will be extracted to the global value, and the global value will be modified during the update and setState will be performed. This is the most recommended approach. If the state needs to be shared between two widgets, then it’s also the right thing to do.
However, GlobalKey allows you to perform updates, retrieve status, data in widgets, and more directly elsewhere. We need to retrieve the GlobalKey object, which is similar to the Android findViewById control, but GlobalKey can retrieve the State, Widget, RenderObject, etc.
Let’s look at chestnuts:
final _globalKey = GlobalKey();
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: Center(
child: Flex(
direction: MediaQuery.of(context).orientation == Orientation.portrait
? Axis.vertical
: Axis.horizontal,
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Counter(key: _globalKey),
],
),
),
floatingActionButton: FloatingActionButton(
onPressed: () {},
tooltip: 'Increment',
child: Icon(Icons.add),
),
);
Copy the code
It’s pretty much the same as the previous example, but now there’s only one Counter left. Now all we need to do is click on the FloatingActionButton to automatically increase the count in this Counter and get some of its properties as follows:
floatingActionButton: FloatingActionButton(
onPressed: () {
final state = (_globalKey.currentState as _CounterState);
state.setState(() => state._count++);
final widget = (_globalKey.currentWidget as Counter);
final context = _globalKey.currentContext;
final render =
(_globalKey.currentContext.findRenderObject() as RenderBox);
///Width height
print(render.size);
///Pixel from the top left corner
print(render.localToGlobal(Offset.zero));
},
child: Icon(Icons.add),
),
);
Copy the code
I/flutter: Size(88.0, 82.0) I/flutter: Offset(152.4, 378.6)Copy the code
As you can see from the code above, three properties are retrieved with _globakKey: state, Widget, and context.
State is used to increment _count.
Widgets are Counter.
But what is context? Let’s go to the source code and see:
Element? get _currentElement => _registry[this];
BuildContext? get currentContext => _currentElement;
Copy the code
We can see that context is an Element object, and we can see that Element inherits from BuildContext.
And using the findRenderObject method in this context, we can get the RenderObject, and the RenderObject is what’s finally displayed on the screen, and we can get some data from the RenderObject, For example, the width of the widget, the position from the top left corner of the screen, and so on.
There are many types of RenderObject, such as RenderBox, and different widgets may use different types of RenderObject, so be aware of this
The instance
For this example, let’s write a small game, a list of small squares of different colors, by dragging the squares to reorder the colors. The effect is as follows:
Shuffle the order by clicking the button, and then drag the box for reordering;
Let’s write the code below:
final boxes = [
Box(Colors.red[100], key: UniqueKey()),
Box(Colors.red[300], key: UniqueKey()),
Box(Colors.red[500], key: UniqueKey()),
Box(Colors.red[700], key: UniqueKey()),
Box(Colors.red[900], key: UniqueKey()),
];
_shuffle() {
setState(() => boxes.shuffle());
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: Center(
///A resortable list
child: Container(
child: ReorderableListView(
onReorder: (int oldIndex, newIndex) {
if (newIndex > oldIndex) newIndex--;
final box = boxes.removeAt(oldIndex);
boxes.insert(newIndex, box);
},
children: boxes),
width: 60,
),
),
floatingActionButton: FloatingActionButton(
onPressed: () => _shuffle(),
child: Icon(Icons.refresh),
),
);
}
Copy the code
ReorderableListView: A resortable list that supports drag sorting
- OnReorder: Drags the callback to give the new index and the old index, using these two parameters to change the position, as shown above
- ScrollDirection: Specifies landscape or vertical direction
Another thing to note is that the Item of the ReorderableListView must require a key, otherwise an error will be reported.
class Box extends StatelessWidget {
final Color color;
Box(this.color, {Key key}) : super(key: key);
@override
Widget build(BuildContext context) {
return UnconstrainedBox(
child: Container(
margin: EdgeInsets.all(5),
width: 50,
height: 50,
decoration: BoxDecoration(
color: color, borderRadius: BorderRadius.circular(10)))); }}Copy the code
Note that the UnconstrainedBox is used in the widget for the item in the list. This is because the ReorderableListView may have a size constraint that prevents the width and height set in the item from taking effect. So we use UnconstrainedBox.
I tried it a few times and found some problems,
- You can only drag in one dimension, up and down or left and right,
- When you drag it, you drag the whole item, and you have some shadow effects and so on,
- You must hold down to drag
Since the ReorderableListView does not provide properties to fix these problems, we can implement a similar effect ourselves. As follows:
class _MyHomePageState extends State<MyHomePage> {
final colors = [
Colors.red[100],
Colors.red[300],
Colors.red[500],
Colors.red[700],
Colors.red[900]]; _shuffle() { setState(() => colors.shuffle()); }int _slot;
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: Listener(
onPointerMove: (event) {
// Get the position of the move
final x = event.position.dx;
// If it is larger than the next one in the raised position, then swap
if (x > (_slot + 1) * Box.width) {
if (_slot == colors.length - 1) return;
setState(() {
final temp = colors[_slot];
colors[_slot] = colors[_slot + 1];
colors[_slot + 1] = temp;
_slot++;
});
} else if (x < _slot * Box.width) {
if (_slot == 0) return;
setState(() {
final temp = colors[_slot];
colors[_slot] = colors[_slot - 1];
colors[_slot - 1] = temp;
_slot--;
});
}
},
child: Stack(
children: List.generate(colors.length, (i) {
return Box(
colors[i],
x: i * Box.width,
y: 300, onDrag: (Color color) => _slot = colors.indexOf(color), key: ValueKey(colors[i]), ); }), ), ), floatingActionButton: FloatingActionButton( onPressed: () => _shuffle(), child: Icon(Icons.refresh), ), ); }}class Box extends StatelessWidget {
final Color color;
final double x, y;
static final width = 50.0;
static final height = 50.0;
static final margin = 2;
final Function(Color) onDrag;
Box(this.color, {this.x, this.y, this.onDrag, Key key}) : super(key: key);
@override
Widget build(BuildContext context) {
return AnimatedPositioned(
child: Draggable(
child: box(color),
feedback: box(color),
onDragStarted: () => onDrag(color),
childWhenDragging: box(Colors.transparent),
),
duration: Duration(milliseconds: 100),
top: y,
left: x,
);
}
box(Color color) {
return Container(
width: width - margin * 2,
height: height - margin * 2,
decoration:
BoxDecoration(color: color, borderRadius: BorderRadius.circular(10))); }}Copy the code
As you can see on the top we changed the ReorderableListView directly to the Stack, because in the Stack we can maneuver the position of the ReorderableListView by the position of the tourists. And there is a layer of listeners around the Stack, which is used to listen for moving events.
And then Box, Box is a little square that can move. In the outermost compartment, the drawn action of the tourist is used, and after the position of the tourist is changed, the animation effect of the translation will occur.
Let’s look at the Draggable component. Draggable is a Draggable component with the following properties:
- Feedback: Follow dragged components
- ChildWhenDragging: Style displayed by chilID child components
- OnDargStarted: Callback pressed for the first time
The code workflow is as follows:
1. Calculate the index of the Box after holding the finger down.
2. Compare the position of the finger when it starts to move with the position when it is pressed.
3. If the value is greater than, index and index +1 are swapped. If the value is less than, index and index-1 are swapped.
4. Perform judgment processing. Return directly if you are in the first or last position.
Notice that the UniqueKey is not used because the UniqueKey is unique, and on the rebuild because the key is not equal, the previous position would be lost, causing the animation of animatedtoy cannot be performed, So we use ValueKey here. This ensures that no state loss occurs.
You can also create a unique UniqueKey for each Box.
In the example above, the result is as follows:
Because it’s a GIF, it’s a little clunky.
The problem
In the finished example above, there are still some problems, such as only landscape, if vertical, you need to change the code.
And the x coordinates start at 0, and if you have something in front of it you’re going to have a problem. For example, if it is vertical and there is an appbar at the top, then there is a problem.
Modify the code as follows:
class _MyHomePageState extends State<MyHomePage> {
///.
int _slot;
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: Listener(
onPointerMove: (event) {
// Get the position of the move
final y = event.position.dy;
// If it is larger than the next one in the raised position, then swap
if (y > (_slot + 1) * Box.height) {
if (_slot == colors.length - 1) return;
setState(() {
final temp = colors[_slot];
colors[_slot] = colors[_slot + 1];
colors[_slot + 1] = temp;
_slot++;
});
} else if (y < _slot * Box.height) {
if (_slot == 0) return;
setState(() {
final temp = colors[_slot];
colors[_slot] = colors[_slot - 1];
colors[_slot - 1] = temp;
_slot--;
});
}
},
child: Stack(
children: List.generate(colors.length, (i) {
return Box(
colors[i],
x: 300, y: i * Box.height, onDrag: (Color color) => _slot = colors.indexOf(color), key: ValueKey(colors[i]), ); }), ), ), floatingActionButton: FloatingActionButton( onPressed: () => _shuffle(), child: Icon(Icons.refresh), ), ); }}Copy the code
In the above code will originally across the components into a stand, and then drag will find problems, such as drag need two drag to move upwards, that is because the y axis is not starting from 0, at the top there will be a appbar, we don’t have counted his height, so there is the question.
At this point we can use GlobalKey to solve the problem:
final _globalKey = GlobalKey();
double _offset;
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: Column(
children: [
SizedBox(height: 30),
Text("WelCome", style: TextStyle(fontSize: 28, color: Colors.black)),
SizedBox(height: 30),
Expanded(
child: Listener(
onPointerMove: (event) {
// Get the position of the move
final y = event.position.dy - _offset;
// If it is larger than the next one in the raised position, then swap
if (y > (_slot + 1) * Box.height) {
if (_slot == colors.length - 1) return;
setState(() {
final temp = colors[_slot];
colors[_slot] = colors[_slot + 1];
colors[_slot + 1] = temp;
_slot++;
});
} else if (y < _slot * Box.height) {
if (_slot == 0) return;
setState(() {
final temp = colors[_slot];
colors[_slot] = colors[_slot - 1];
colors[_slot - 1] = temp;
_slot--;
});
}
},
child: Stack(
key: _globalKey,
children: List.generate(colors.length, (i) {
return Box(
colors[i],
x: 180,
y: i * Box.height,
onDrag: (Color color) {
_slot = colors.indexOf(color);
final renderBox = (_globalKey.currentContext
.findRenderObject() as RenderBox);
// Get the distance from the top
_offset = renderBox.localToGlobal(Offset.zero).dy;
},
key: ValueKey(colors[i]),
);
}),
),
))
],
),
floatingActionButton: FloatingActionButton(
onPressed: () => _shuffle(),
child: Icon(Icons.refresh),
),
);
}
Copy the code
The solution is very simple,
Get the current Stack position from the top with GlobalKey and subtract it from dy. The final effect is as follows:
Optimize the details
After the above operation, the basic functions are realized, and finally we optimize the details, such as random color, fixing the first color, adding the game success detection and so on.
The final code is as follows:
class _MyHomePageState extends State<MyHomePage> {
MaterialColor _color;
List<Color> _colors;
initState() {
super.initState();
_shuffle();
}
_shuffle() {
_color = Colors.primaries[Random().nextInt(Colors.primaries.length)];
_colors = List.generate(8, (index) => _color[(index + 1) * 100]);
setState(() => _colors.shuffle());
}
int _slot;
final _globalKey = GlobalKey();
double _offset;
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text(widget.title), actions: [
IconButton(
onPressed: () => _shuffle(),
icon: Icon(Icons.refresh, color: Colors.white),
)
]),
body: Column(
children: [
SizedBox(height: 30),
Text("WelCome", style: TextStyle(fontSize: 28, color: Colors.black)),
SizedBox(height: 30),
Container(
width: Box.width - Box.margin * 2,
height: Box.height - Box.margin * 2,
decoration: BoxDecoration(
color: _color[900], borderRadius: BorderRadius.circular(10)),
child: Icon(Icons.lock, color: Colors.white),
),
SizedBox(height: Box.margin * 2.0),
Expanded(
child: Center(
child: Listener(
onPointerMove: event,
child: SizedBox(
width: Box.width,
child: Stack(
key: _globalKey,
children: List.generate(_colors.length, (i) {
return Box(
_colors[i],
y: i * Box.height,
onDrag: (Color color) {
_slot = _colors.indexOf(color);
final renderBox = (_globalKey.currentContext
.findRenderObject() as RenderBox);
// Get the distance from the top_offset = renderBox.localToGlobal(Offset.zero).dy; }, onEnd: _checkWinCondition, ); }),),),),)))); } _checkWinCondition() {List<double> lum = _colors.map((e) => e.computeLuminance()).toList();
bool success = true;
for (int i = 0; i < lum.length - 1; i++) {
if (lum[i] > lum[i + 1]) {
success = false;
break; }}print(success ? "Success" : "");
}
event(event) {
// Get the position of the move
final y = event.position.dy - _offset;
// If it is larger than the next one in the raised position, then swap
if (y > (_slot + 1) * Box.height) {
if (_slot == _colors.length - 1) return;
setState(() {
final temp = _colors[_slot];
_colors[_slot] = _colors[_slot + 1];
_colors[_slot + 1] = temp;
_slot++;
});
} else if (y < _slot * Box.height) {
if (_slot == 0) return;
setState(() {
final temp = _colors[_slot];
_colors[_slot] = _colors[_slot - 1];
_colors[_slot - 1] = temp; _slot--; }); }}}class Box extends StatelessWidget {
final double x, y;
final Color color;
static final width = 200.0;
static final height = 50.0;
static final margin = 2;
final Function(Color) onDrag;
final Function onEnd;
Box(this.color, {this.x, this.y, this.onDrag, this.onEnd})
: super(key: ValueKey(color));
@override
Widget build(BuildContext context) {
return AnimatedPositioned(
child: Draggable(
child: box(color),
feedback: box(color),
onDragStarted: () => onDrag(color),
onDragEnd: (drag) => onEnd(),
childWhenDragging: box(Colors.transparent),
),
duration: Duration(milliseconds: 100),
top: y,
left: x,
);
}
box(Color color) {
return Container(
width: width - margin * 2,
height: height - margin * 2,
decoration:
BoxDecoration(color: color, borderRadius: BorderRadius.circular(10))); }}Copy the code
The final effect is as follows:
reference
B station Wang Shu not bald video
Flutter of actual combat
If this article is helpful to your place, we are honored, if there are mistakes and questions in the article, welcome to put forward!