This is the 28th day of my participation in the November Gwen Challenge. Check out the event details: The last Gwen Challenge 2021
In the previous article, some classes intentionally omitted a Key. What does this Key do? Let’s explore it today.
1. Principle of Key
- Key itself is an abstract class with a factory constructor to create ValueKey
- Direct subclasses include LocalKey and GlobalKey
- GlobalKey: Helps us access information about a Widget
- LocalKey: This is used to distinguish which Element to keep and which Element to remove. The heart of the DIff algorithm
- ValueKey: Takes a value as an argument (number, string)
- ObjectKey: Enter as an object
- LocalKey (create unique identity)
2. Key exploration
Any Widget has a key, and the key can be null.
Create a KeyDemo in the MaterialApp home
class KeyDemo extends StatefulWidget { const KeyDemo({Key? key}) : super(key: key); @override _KeyDemoState createState() => _KeyDemoState(); } class _KeyDemoState extends State<KeyDemo> { @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar(title: const Text('KeyDemo'),), body: const Text('hello'), ); }}Copy the code
Next create a randomly colored square with a title.
class StfulItem extends StatefulWidget { const StfulItem(this.title,{Key? key}) : super(key: key); final String title; @override _StfulItemState createState() => _StfulItemState(); } class _StfulItemState extends State<StfulItem> { final color = Color.fromRGBO(Random().nextInt(256), The Random (). NextInt (256), Random () nextInt (256), 1.0); @override Widget build(BuildContext context) { return Container( width: 100, height: 100, child: Text(widget.title), color: color, ); }}Copy the code
Declare an array of widgets to put StfulItem in _KeyDemoState, and then set the array of widgets to the children of the row in the body.
class _KeyDemoState extends State<KeyDemo> { List<Widget> items = [StfulItem('1111'),StfulItem('2222'),StfulItem('3333')]; @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar(title: const Text('KeyDemo'),), body: Row( mainAxisAlignment: MainAxisAlignment.center, children: items, ) ); }}Copy the code
Display after running:
The Scaffold in _KeyDemoState adds floatingActionButton. When clicked, setState is called and the first element of the items is removed.
floatingActionButton: FloatingActionButton(
onPressed: (){
setState(() {
items.removeAt(0);
});
},
child: Icon(Icons.add),
),
Copy the code
When I click on it, I see that the 1 is missing, but I remove the color of the 3, and that’s a problem.
Here we’re creating the same random color square with the title, but here’s the StatelessWidget.
class StlItem extends StatelessWidget { StlItem(this.title,{Key? key}) : super(key: key); final String title; Final color = color.fromrgbo (Random().nextint (256), Random().nextint (256), Random().nextint (256), 1.0); @override Widget build(BuildContext context) { return Container( width: 100, height: 100, child: Text(title), color: color, ); }}Copy the code
Change the widget in the previous items to StlItem.
List<Widget> items = [StlItem('1111'),StlItem('2222'),StlItem('3333')];
Copy the code
So after running here:
Click the hover button and find that this is deleted correctly.
Here we’re changing StlItem to StfulItem, but we’re passing a key to StfulItem.
List<Widget> items = [StfulItem('1111',key: const ValueKey(111) ,),StfulItem('2222',key: const ValueKey(222)),StfulItem('3333',key: const ValueKey(333))];
Copy the code
Rerun:
After clicking the button, I found that the first one was deleted correctly.
So why does StatefulWidget have this problem? What’s the difference between a StatefulWidget and a StatelessWidget? Here, the Color of the StatelessWidget belongs to the Widget, while the Color property of the StatefulWidget belongs to state. Put color in the Widget and remove the key from the items list.
class StfulItem extends StatefulWidget { StfulItem(this.title,{Key? key}) : super(key: key); final String title; Final color = color.fromrgbo (Random().nextint (256), Random().nextint (256), Random().nextint (256), 1.0); @override _StfulItemState createState() => _StfulItemState(); } class _StfulItemState extends State<StfulItem> { @override Widget build(BuildContext context) { return Container( width: 100, height: 100, child: Text(widget.title), color: widget.color, ); }}Copy the code
Run:
When I click on it, the problem disappears.
So, once color is inside state, there’s a bug. Why is there a bug like this? This is because when deleted, the Widget’s contents are removed from the StatefulWidget, while the state object is still being reused in memory. That is, there are three colors stored in the State object. When the first Widget is removed, the colors in the State are not removed, so the second Widget corresponds to the first color, and the last color is not displayed because there is no Widget binding.
The reason for this is that you need to see the canUpdate method in the Widget. Incremental rendering is used in Flutter. No matter how much the page is refreshed, the page is not re-rendered. Instead, the changed content is rendered and a large amount of content is reused. What has changed and what has not changed during rendering is determined by canUpdate, which returns true only when the runtimeType and key of the new and old widgets are equal. Oldwidget. key == newWidget.key is invalid when no key is passed, there is only type judgment, so if they have the same type, they are considered to match even if their child parts are completely different. When both widgets belong to StlItem, they are of the same type. This returns yes when the pointing widget is of the same type as the next widget, and it is not rendered, but the Element tree, or even the Render tree, points to the widget.
When we create a Widget, there are elements that correspond to one of them.
When we remove Widget 111, the Element object calls the CanUpdate method to see if the previously saved Widget is the same as the current Widget. The judgment is sequential, so Element 111 will be judged first and later. Here Element111 compares Widget 222 and finds that the type is the same, and there is no key, so it returns true, which is reused for efficiency rather than removing Element111. This creates a situation where Element 111 points to Element 222, and then Element 222 compares Widget 333. Element 333 was removed from the Widget because it was no longer there. The color is stored in the State, and the State is stored in the Element, so the color is wrong.
So if you change it for widgets, the key becomes very powerful.
3. GlobalKey
Create a GlobalKeyDemo and ChildPage and use it in the MaterialApp home inside main.dart.
class GlobalKeyDemo extends StatelessWidget { const GlobalKeyDemo({Key? key}) : super(key: key); @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: const Text('Global Key Demo'), ), body: ChildPage(), floatingActionButton: FloatingActionButton( onPressed: (){}, child: const Icon(Icons.add), ), ); } } class ChildPage extends StatefulWidget { const ChildPage({Key? key}) : super(key: key); @override _ChildPageState createState() => _ChildPageState(); } class _ChildPageState extends State<ChildPage> { int count = 0; String data = 'hello'; @override Widget build(BuildContext context) { return Center( child: Column( children: [ Text(count.toString()), Text(data), ], ), ); }}Copy the code
After the operation:
There is no way to pass onPress to childPage in GlobalKeyDemo if we want to click +1, but we can use GlobalKey to do so.
Add a _globalKey to GlobalKeyDemo and pass it in as ChildPage. Then GlobalKeyDemo can access ChildPage state via _globalKey. Widgets, such as the Context. Here onPressed modiates the data and count for ChildPage state.
class GlobalKeyDemo extends StatelessWidget { GlobalKeyDemo({Key? key}) : super(key: key); final GlobalKey<_ChildPageState> _globalKey = GlobalKey(); @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: const Text('Global Key Demo'), ), body: ChildPage(key: _globalKey,), floatingActionButton: FloatingActionButton( onPressed: (){ _globalKey.currentState! .setState(() { _globalKey.currentState! .data = "old:" + _globalKey.currentState! .count.toString(); _globalKey.currentState! .count++; }); }, child: const Icon(Icons.add), ), ); }}Copy the code
To change the contents of ChildPage, click the GlobalKeyDemo button.