Digresión
El título de este artículo me recuerda una frase de la película "Batman v Superman: Dawn of Justice", en la que Flash, que viaja desde el futuro, advierte a Batman: ¡Es Lois! ¡Ella es la clave!
【¡Bruce, soy Louise! ¡Louise es la clave! ]
[Tienes razón, siempre has tenido razón sobre él...]
[¿Llegué demasiado pronto? Llegué demasiado temprano...]
[¡Impresionante! ¡Alégrate de él! ]
[¡Debes encontrarnos! ¡Debes encontrarnos! ...]
Este es Flash que viajó de regreso desde un cierto punto en el futuro con la ayuda de Speed Force, y vino a advertir a Batman. Se puede inferir de las oraciones anteriores que en la línea de tiempo futura, debido al accidente de Louise, Superman puede convertirse en una especie de existencia terrible. Nadie en la Liga de la Justicia puede detenerlo, por lo que Flash solo puede volver al pasado. , les dije a mis camaradas que se prepararan con anticipación y no permitieran que Superman degenerara así. Inesperadamente, cuando regresó de un viaje en el tiempo, descubrió que Batman aún no lo conocía, por lo que sabía que había llegado demasiado pronto (porque la Liga de la Justicia aún no se había establecido). Sabía que solo la Liga de la Justicia era capaz de proteger a Superman y su familia, por lo que finalmente le rogó a Batman que los encontrara y formara la Liga de la Justicia. Esta trama debería estar adaptada de los cómics y el juego "Injustice: Gods Among Us".
Memoria de la trama: En la película "Batman v Superman: El amanecer de la justicia", cuando Batman golpeaba violentamente a Superman, Superman mencionó a Martha y le pidió a Batman que salvara a Martha, y Batman enojado preguntó por qué Superman mencionó a Martha. La novia de Superman llegó a tiempo, le dijo a Batman que era el nombre de su madre. Debido a que la madre de Batman también se llama Martha, porque Batman no pudo salvar a su madre cuando era joven y fue testigo de cómo los ladrones mataban a su madre Martha Wayne frente a él, así que cuando Batman Cuando Superman le pidió a Batman que salvara a Martha, pensó en su madre. En ese momento, Batman también recordó la advertencia que Flash le dio antes, y de repente se dio cuenta de que casi cometió un gran error. Si no lograba rescatar a la madre de Superman, Martha, entonces Superman sería ennegrecido más tarde. , el mundo será gobernado por el Superman ennegrecido en el sueño anterior de Batman, y nadie en el mundo puede detener a Superman.
Bueno, comencemos a entrar en el tema de este artículo.
Por qué la clave es muy importante
Primero veamos un ejemplo, ahora hay un FancyButton
componente personalizado, el código es el siguiente:
import 'dart:math';
import 'package:flutter/material.dart';
class FancyButton extends StatefulWidget {
const FancyButton({
Key? key, required this.onPressed, required this.child})
: super(key: key);
final VoidCallback onPressed;
final Widget child;
@override
State<FancyButton> createState() => _FancyButtonState();
}
class _FancyButtonState extends State<FancyButton> {
@override
Widget build(BuildContext context) {
return ElevatedButton(
style: ButtonStyle(
backgroundColor: MaterialStateProperty.all(_getColors()),
),
onPressed: widget.onPressed,
child: Padding(
padding: const EdgeInsets.all(30),
child: widget.child,
),
);
}
// 以下代码用于生成随机背景色,以确保每个Button的背景色不同
Color _getColors() {
return _buttonColors.putIfAbsent(this, () => _colors[next(0, 5)]); // map中不存在就放入map, 否则直接返回
}
final Map<_FancyButtonState, Color> _buttonColors =
{
}; // 注意,这里使用了一个Map保存当前State对应的Color
final Random _random = Random();
int next(int min, int max) => min + _random.nextInt(max - min);
final List<Color> _colors = [
Colors.blue,
Colors.green,
Colors.orange,
Colors.purple,
Colors.amber
];
}
FancyButton
Es muy simple, solo envuelva ElevatedButton
el componente, administrará el color de fondo internamente, para hacer que el FancyButton
color de fondo de cada uno sea diferente, aquí use un número aleatorio para obtener un color aleatorio, y utilícelo Map
para almacenar el color en caché, de modo que a continuación tiempo que puede Map
obtener directamente de él sin calcularlo cada vez.
Lo siguiente se basa en la aplicación de ejemplo de contador predeterminado de Flutter que usa lo anterior FancyButton
para la transformación. Agregamos dos botones en la página FancyButton
, que se usan para sumar y restar counter
valores . Además, agregamos un botón "Intercambiar", que se puede intercambiar en la página cuando se hace clic. Dos FancyButton
, el efecto estático que debe lograr la página es el siguiente:
El código de implementación es el siguiente:
class FancyButtonPage extends StatefulWidget {
const FancyButtonPage({
Key? key}) : super(key: key);
@override
State<FancyButtonPage> createState() => _FancyButtonPageState();
}
class _FancyButtonPageState extends State<FancyButtonPage> {
int counter = 0;
bool _reversed = false;
void resetCounter() {
setState(() => counter = 0);
swapButton();
}
void swapButton() {
setState(() => _reversed = !_reversed);
}
@override
Widget build(BuildContext context) {
final incrementButton = FancyButton(
onPressed: () => setState(() => counter++),
child: const Text(
"Increment",
style: TextStyle(fontSize: 20),
));
final decrementButton = FancyButton(
onPressed: () => setState(() => counter--),
child: const Text(
"Decrement",
style: TextStyle(fontSize: 20),
));
List<Widget> buttons = [incrementButton, decrementButton];
if (_reversed) {
buttons = buttons.reversed.toList();
}
return Scaffold(
appBar: AppBar(title: const Text("FancyButton")),
body: Column(
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.center,
children: [
const SizedBox(height: 30),
Text("$counter", style: const TextStyle(fontSize: 22)),
const SizedBox(height: 30),
Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: buttons,
),
const SizedBox(height: 30),
ElevatedButton(
onPressed: resetCounter, child: const Text("Swap Button"))
],
),
);
}
}
Efecto de prueba después de correr:
En este momento, encontrará un fenómeno extraño: cuando se presiona el botón de intercambio, los dos botones de arriba intercambiarán posiciones, pero solo se intercambiará el texto del botón, pero el color de fondo no cambiará, y la función correspondiente es normal cuando se hace clic en el botón. Es decir, el botón de la izquierda tiene el mismo color de fondo que antes del intercambio, aunque el botón en sí es diferente. ¿Por qué es esto? Obviamente, esto no es lo que esperamos, lo que esperamos es que los dos botones puedan "realmente" cambiar de posición. Pero, ¿qué quieres decir con cambiar solo la mitad?
Árbol de elementos 和 Estado
Antes de explicar esta pregunta, comprendamos algunos conceptos sobre Element Tree
y :State
-
State
Los objetos en realidad sonElement Tree
administrados por . (precisamenteStatefulElement
creado y mantenido por -
State
Los objetos son de larga vida . A diferencia deWidget
,Widget
no se destruyen y reconstruyen cada vez que se vuelven a renderizar. -
State
Los objetos son reutilizables . -
Element
citadoWidget
_ Al mismo tiempo,State
el objeto también tieneWidget
una referencia al objeto, pero esta posesión no es permanente.
Element
Simple, porque solo contienen metainformación y referencias a , pero también saben cómo actualizar ellos mismos diferentes referencias Widget
si cambian.Widget
Widget
Cuando Flutter decide build
reconstruir y volver a renderizar al llamar a un método, uno buscará el que está exactamente en la misma ubicación element
que el anterior al que hacía referenciaWidget
Widget
.
Luego, decidirá Widget
si es el mismo (si es el mismo, no necesita hacer nada), Widget
si algo ha cambiado o si es completamente diferente Widget
(si es completamente diferente, debe volver a renderizarse). ).
Pero el problema se Element
basa en qué juzgar el contenido actualizado, solo miran Widget
algunas propiedades en:
-
tipo exacto en tiempo de ejecución (runtimeType)
-
uno
Widget
(key
si lo hay)
De hecho, es la lógica de Flutter ejecutar el método Element
en el código fuente del proceso de reconstrucción de Build updateChild()
:
// flutter/lib/src/widgets/framework.dart
Element? updateChild(Element? child, Widget? newWidget, Object? newSlot) {
// Element
if (newWidget == null) {
if (child != null) {
deactivateChild(child);
}
return null;
}
final Element newChild;
if (child != null) {
...
if (hasSameSuperclass && child.widget == newWidget) {
if (child.slot != newSlot) {
updateSlotForChild(child, newSlot);
}
newChild = child;
} else if (hasSameSuperclass && Widget.canUpdate(child.widget, newWidget)) {
if (child.slot != newSlot) {
updateSlotForChild(child, newSlot);
}
...
child.update(newWidget);
...
newChild = child;
} else {
deactivateChild(child);
newChild = inflateWidget(newWidget, newSlot);
}
} else {
newChild = inflateWidget(newWidget, newSlot);
}
return newChild;
}
La lógica principal de este método se resume de la siguiente manera:
El código fuente del Widget.canUpdate()
método es el siguiente:
static bool canUpdate(Widget oldWidget, Widget newWidget) {
return oldWidget.runtimeType == newWidget.runtimeType
&& oldWidget.key == newWidget.key;
}
Los dos objetos intercambiados en este ejemplo FancyButton
son dos objetos de instancia con direcciones de memoria completamente diferentes, por lo que updateChild()
el caso 2 de la lógica del método anterior definitivamente se ejecutará directamente, el caso 1 está excluido. Es decir, canUpdate
se ejecutará el juicio lógico del método.
Sabemos que Widget Tree es solo un mapeo de Element Tree, que solo proporciona información de configuración que describe el árbol de la interfaz de usuario y, en este ejemplo, estos Widget
colores no están Widget
en la configuración; se almacenan en el Widget
objeto correspondiente State
. Element
Apunta a la actualizada Widget
y muestra la nueva configuración, pero aún conserva los State
objetos originales. Entonces, cuando Element
ve el nuevo insertado en este lugar en el árbol de la interfaz de usuario Widget
, es como: "emm, no key
, el tipo de tiempo de ejecución todavía está allí FancyButton
, así que no necesito actualizar mi referencia. Esta es State
la correcta que coincide con mi objeto Widget
."
Teclas de widget
Una vez que el análisis del problema es claro, la solución más simple a este problema es: clave . Finalmente, llegando al punto de este artículo, cuando se trata de colecciones Widget
, proporcionarlas key
puede ayudar a Flutter a comprender Widget
cuándo dos del mismo tipo son realmente diferentes. Esto es especialmente útil para widgets con múltiples hijos. A menudo, como en nuestro ejemplo anterior, cuando todos los nodos secundarios en una fila o columna son del mismo tipo, es bueno darle a Flutter un poco de información adicional para distinguir entre estos nodos secundarios.
Esto es lo que UniqueKey
usamos para resolver este problema:
class FancyButtonPage extends StatefulWidget {
const FancyButtonPage({
Key? key}) : super(key: key);
State<FancyButtonPage> createState() => _FancyButtonPageState();
}
class _FancyButtonPageState extends State<FancyButtonPage> {
int counter = 0;
bool _reversed = false;
final List _buttonKeys = [UniqueKey(), UniqueKey()]; // add key
void resetCounter() {
setState(() => counter = 0);
swapButton();
}
void swapButton() {
setState(() => _reversed = !_reversed);
}
Widget build(BuildContext context) {
final incrementButton = FancyButton(
key: _buttonKeys.first, // add key
onPressed: () => setState(() => counter++),
child: const Text(
"Increment",
style: TextStyle(fontSize: 20),
));
final decrementButton = FancyButton(
key: _buttonKeys.last, // add key
onPressed: () => setState(() => counter--),
child: const Text(
"Decrement",
style: TextStyle(fontSize: 20),
));
List<Widget> buttons = [incrementButton, decrementButton];
if (_reversed) {
buttons = buttons.reversed.toList();
}
return Scaffold(
appBar: AppBar(title: const Text("FancyButton")),
body: Column(
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.center,
children: [
const SizedBox(height: 30),
Text("$counter", style: const TextStyle(fontSize: 22)),
const SizedBox(height: 30),
Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: buttons,
),
const SizedBox(height: 30),
ElevatedButton(
onPressed: resetCounter, child: const Text("Swap Button"))
],
),
);
}
}
Después de volver a ejecutar, el efecto es el siguiente:
Puede ver que ahora es como esperábamos, el fondo y el texto del botón se intercambiarán y la función será normal.
Ahora, finalmente sabemos por qué el compilador siempre nos recuerda agregar un parámetro llamado clave al crear la clase Widget en Flutter y escribir el constructor:
Si no lo agrega, se tomará la molestia de brindarle indicaciones y advertencias para recordárselo. Además, encontramos que la especificación recomendada para esta clave se define como un tipo anulable, lo que significa que cuando creamos un componente Widget, no siempre necesitamos pasar una clave.
En Flutter, Key generalmente se usa como un identificador único, por lo que Key no se puede reutilizar . Como se mencionó anteriormente, Element
al actualizar, la decisión de reutilizar se basa principalmente en juzgar si el valor de clave de tipo && del componente es consistente. Por lo tanto, cuando los tipos de componentes son diferentes, el tipo es suficiente para distinguir diferentes componentes y no necesitamos usar la clave en este momento . Pero si hay varios controles del mismo tipo al mismo tiempo, el tipo ya no se puede usar como una condición distintiva en este momento, necesitamos usar la tecla.
LocalKey y GlobalKey
Las claves en Flutter se dividen principalmente en dos categorías: LocalKey y GlobalKey .
- 局部 Clave (LocalKey):ValueKey、ObjectKey、UniqueKey
- Clave clave (GlobalKey):GlobalKey、GlobalObjectKey
1. Claves globales
-
GlobalKey
Se utiliza para administrar el estado y mover widgets en el árbol de widgets . Por ejemplo, puedeWidget
usar uno en unoGlobalKey
que mostrará una casilla de verificación y usar eso en varias páginasWidget
. Estokey
le dice al marco que useWidget
la misma instancia de this. Entonces, cuando navegue a una página diferente para ver la casilla de verificación, permanecerá marcada. Si lo selecciona en la página A , también se seleccionará en la página B. -
GlobalObjectKey
: clave de objeto global, que puede generar una clave global basada en el objeto, que esObjectKey
algo similar a .
El uso debe garantizar que no seGlobalKey
repita la unicidad global , una cosa a tener en cuenta es que usarlo también tiene una desventaja, es decir, pérdida de rendimiento, porque necesita mantener siempre un estado disponible globalmente para ocupar recursos.
2. Claves locales
-
ValueKey<T>
: ValueKey es la mejor opción cuando el objeto que se va a agregarkey
tiene algún tipo de propiedad única que no cambia . Por ejemplo, en una aplicación de lista de tareas pendientes, cada widget que muestra una tarea puede tener un texto constante y único. En otras palabras, hay unid
atributo comercial único en la clase comercial que nos devuelve la interfaz de back-end, que se puede usar para crearloValueKey<T>
. -
ObjectKey
: Use un objeto para crear una clave. Cuando los objetos tienen el mismo tipo pero sus valores de atributo son diferentes, es adecuado usar ObjectKey . Por ejemplo, considere un objeto llamado "Producto" en una aplicación de comercio electrónico: dos productos pueden tener el mismo título (dos vendedores diferentes pueden vender coles de Bruselas). Un vendedor puede tener varios productos. Lo que hace que un producto sea único es la combinación del nombre del producto y el nombre del vendedor. Entonces, key es unObjectKey
objeto literal que se pasa a . Por ejemplo:
-
UniqueKey
: Puede usar UniqueKey si desea agregar claves a los elementos secundarios de una colección cuyos valores no se conocen hasta que se crean los elementos secundarios . O podemos usarlo directamente cuando no sabemos cómo especificarValueKey
o .ObjectKey
UniqueKey
-
PageStorageKey
: Esta es una tecla especial que se utiliza para almacenar información de la página, como la posición de desplazamiento.
Use GlobalKey para resolver el problema de pérdida de estado
LocalKey
Otra GlobalKey
diferencia obvia es que el estado de la página se perderá después de rotar la pantalla ,LocalKey
como el siguiente código:
import 'package:flutter/material.dart';
class LocalKeyPage extends StatefulWidget {
const LocalKeyPage({
Key? key}) : super(key: key);
State<LocalKeyPage> createState() => _LocalKeyPageState();
}
class _LocalKeyPageState extends State<LocalKeyPage> {
List<Widget> list = [
const Box(
key: ValueKey('1'),
color: Colors.red,
),
Box(
key: UniqueKey(), //唯一值 每次运行的时候会随机生成
color: Colors.yellow,
),
const Box(key: ObjectKey(Box(color: Colors.blue)), color: Colors.blue)
];
Widget build(BuildContext context) {
return Scaffold(
floatingActionButton: FloatingActionButton(
child: const Icon(Icons.refresh),
onPressed: () =>setState(() => list.shuffle()), //shuffle:打乱list元素的顺序
),
appBar: AppBar(title: const Text('LocalKey')),
body: Center(
child: MediaQuery.of(context).orientation==Orientation.portrait?Column(
mainAxisAlignment: MainAxisAlignment.center,
children: list,
):Row(
mainAxisAlignment: MainAxisAlignment.center,
children: list,
),
),
);
}
}
class Box extends StatefulWidget {
final Color color;
const Box({
Key? key, required this.color}) : super(key: key);
State<Box> createState() => _BoxState();
}
class _BoxState extends State<Box> {
int _count = 0;
Widget build(BuildContext context) {
return SizedBox(
height: 100,
width: 100,
child: ElevatedButton(
style: ButtonStyle(
backgroundColor: MaterialStateProperty.all(widget.color)),
onPressed: () {
setState(() {
_count++;
});
},
child: Text(
"$_count",
style: Theme.of(context).textTheme.headline2,
),
),
);
}
}
En el código anterior, se usa un componente para mostrar la lista cuando el dispositivo está en orientación vertical , y un componente se usa para mostrar la lista Column
cuando el dispositivo está en orientación horizontal . Row
El efecto de ejecución es el siguiente:
Use GlobalKey
puede evitar este problema, modifique el código de la siguiente manera:
class GlobalKeyPage extends StatefulWidget {
const GlobalKeyPage({
Key? key}) : super(key: key);
State<GlobalKeyPage> createState() => _GlobalKeyPagePageState();
}
final GlobalKey _globalKey1 = GlobalKey();
final GlobalKey _globalKey2 = GlobalKey();
final GlobalKey _globalKey3 = GlobalKey();
class _GlobalKeyPagePageState extends State<GlobalKeyPage> {
List<Widget> list = [];
void initState() {
super.initState();
list = [
Box(
key: _globalKey1,
color: Colors.red,
),
Box(
key: _globalKey2,
color: Colors.yellow,
),
Box(key: _globalKey3, color: Colors.blue)
];
}
Widget build(BuildContext context) {
return Scaffold(
floatingActionButton: FloatingActionButton(
child: const Icon(Icons.refresh),
onPressed: () => setState(() => list.shuffle()), // shuffle:打乱list元素的顺序
),
appBar: AppBar(title: const Text('LocalKey')),
body: Center(
child: MediaQuery.of(context).orientation == Orientation.portrait
? Column(
mainAxisAlignment: MainAxisAlignment.center,
children: list,
)
: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: list,
),
),
);
}
}
class Box extends StatefulWidget {
final Color color;
const Box({
Key? key, required this.color}) : super(key: key);
State<Box> createState() => _BoxState();
}
class _BoxState extends State<Box> {
int _count = 0;
Widget build(BuildContext context) {
return SizedBox(
height: 100,
width: 100,
child: ElevatedButton(
style: ButtonStyle(
backgroundColor: MaterialStateProperty.all(widget.color)),
onPressed: () {
setState(() {
_count++;
});
},
child: Text(
"$_count",
style: Theme.of(context).textTheme.headline2,
),
),
);
}
}
resultado de ejecución:
La razón por la que aún se puede mantener el estado después de rotar la pantalla es porque Flutter ha vuelto a montar el nodo correspondiente creado GlobalKey
mediante la lógica de reutilización (se analizará más adelante).GlobalKey
Widget
Element
Element Tree
Use GlobalKey para obtener el objeto State
StatefulWidget
Para obtener el objeto del componente en Flutter State
, una forma es usar context.findAncestorStateOfType<T>()
el método, que puede widget
buscar en el árbol desde el nodo actual el objeto StatefulWidget
correspondiente State
del tipo especificado. Por ejemplo, aquí hay SnackBar
un ejemplo de implementación de un open :
class GetStateObjectRoute extends StatefulWidget {
const GetStateObjectRoute({
Key? key}) : super(key: key);
State<GetStateObjectRoute> createState() => _GetStateObjectRouteState();
}
class _GetStateObjectRouteState extends State<GetStateObjectRoute> {
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("子树中获取State对象"),
),
body: Center(
child: Column(
children: [
Builder(builder: (context) {
return ElevatedButton(
onPressed: () {
// 查找父级最近的Scaffold对应的ScaffoldState对象
ScaffoldState _state = context.findAncestorStateOfType<ScaffoldState>()!;
// 打开抽屉菜单
_state.openDrawer();
},
child: Text('打开抽屉菜单1'),
);
}),
],
),
),
drawer: Drawer(),
);
}
}
Aviso:
-
En términos generales, si
StatefulWidget
el estado de es privado (no debe estar expuesto al exterior), entonces no deberíamos obtener directamente suState
objeto en el código; siStatefulWidget
se espera que el estado de esté expuesto (generalmente hay algunos métodos de operación de componentes), Podemos ir directamente a conseguir suState
objeto. -
Sin embargo, el método para
context.findAncestorStateOfType
obtenerStatefulWidget
el estado de es universal y no podemos especificarStatefulWidget
si el estado de es privado a nivel gramatical, por lo que existe un acuerdo predeterminado en el desarrollo de Flutter: siStatefulWidget
se va a exponer el estado de,StatefulWidget
se debe proporcionar uno Enof
El método estático se utiliza para obtener suState
objeto, y los desarrolladores pueden obtenerlo directamente a través de este método; siState
no quieren estar expuestos, noof
se proporciona ningún método. Esta convención se puede ver en todas partes en Flutter SDK.
Por lo tanto, el método de adquisición en el ejemplo anterior, Flutter SDK también proporciona un Scaffold.of
método que podemos usar directamente:
Builder(builder: (context) {
return ElevatedButton(
onPressed: () {
// 直接通过of静态方法来获取ScaffoldState
ScaffoldState _state = Scaffold.of(context);
// 打开抽屉菜单
_state.openDrawer();
},
child: Text('打开抽屉菜单2'),
);
}),
Para otro ejemplo, si queremos mostrar, snackbar
podemos llamarlo a través del siguiente código:
Builder(builder: (context) {
return ElevatedButton(
onPressed: () {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text("我是SnackBar")),
);
},
child: Text('显示SnackBar'),
);
}),
Otra forma de obtener el objeto StatefulWidget
del componente en Flutter es usar , podemos usar para obtener el estado del subcomponente y ejecutar el método del subcomponente. De manera similar, podemos obtener el objeto del subcomponente, y podemos obtener el objeto del subcomponente. Aquí hay un ejemplo de uso:State
GlobalKey
globalKey.currentState
globalKey.currentWidget
Widget
globalKey.currentContext
context
class GlobalKeyExample extends StatefulWidget {
const GlobalKeyExample({
Key? key}) : super(key: key);
State createState() => _GlobalKeyExampleState();
}
final GlobalKey<GlobalKeyTestState> _globalKey =
GlobalKey<GlobalKeyTestState>();
class _GlobalKeyExampleState extends State<GlobalKeyExample> {
Widget build(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
GlobalKeyTest(key: _globalKey), // 子组件指定 key 使用 globalKey
ElevatedButton(
child: const Text("Add"),
onPressed: () {
// 获取子组件 widget 的 state 对象,并执行其方法
_globalKey.currentState?.addCount(20);
// GlobalKeyTest wg = _globalKey.currentWidget as GlobalKeyTest;
// _globalKey.currentContext!.findRenderObject();
// 系统暴露state对象的范例
// ScaffoldMessenger.of(context).showSnackBar(
// const SnackBar(content: Text("这是SnackBar")),
// );
},
),
],
);
}
}
// 子组件
class GlobalKeyTest extends StatefulWidget {
const GlobalKeyTest({
Key? key}) : super(key: key);
GlobalKeyTestState createState() => GlobalKeyTestState();
}
class GlobalKeyTestState extends State<GlobalKeyTest> {
int count = 0;
addCount(int x) {
setState(() {
count = count + x;
});
}
Widget build(BuildContext context) {
return Column(
children: <Widget>[
Text(count.toString()),
],
);
}
}
resultado de ejecución:
Análisis de código fuente clave
Key
Es casi omnipresente en el código fuente de Flutter, pero rara vez se involucra en el desarrollo diario. En términos oficiales, Key
el escenario de uso es: debe agregar, eliminar y clasificar una serie de Widgets del mismo tipo y con diferentes estados (Estado) .
Key
Principalmente divididas en GlobalKey
y LocalKey
, las clases clave y sus relaciones se muestran en la Figura 8-3.
A continuación, echaremos un vistazo a la función y el principio desde la perspectiva del código fuente Key
.
Análisis del código fuente de GlobalKey
GlobalKey
El código de registro es el siguiente:
// 代码清单5-3 flutter/packages/flutter/lib/src/widgets/framework.dart
// Element
void mount(Element? parent, dynamic newSlot) {
_parent = parent; // 对根节点而言,parent为null
_slot = newSlot;
_lifecycleState = _ElementLifecycle.active; // 更新状态
_depth = _parent != null ? _parent!.depth + 1 : 1; // 树的深度
if (parent != null) _owner = parent.owner; // 绑定BuildOwner
final Key? key = widget.key; // Global Key 注册
if (key is GlobalKey) {
key._register(this); } // 见代码清单8-15
_updateInheritance();
}
_register
La lógica de se muestra en el Listado 8-15, es decir, el actual Element
se agregará a un campo global _registry
.
// 代码清单8-15 flutter/packages/flutter/lib/src/widgets/framework.dart
static final Map<GlobalKey, Element> _registry = <GlobalKey, Element>{
}; // 全局注册表
Element? get _currentElement => _registry[this];
void _register(Element element) {
_registry[this] = element; // this即GlobalKey子类的实例
}
void _unregister(Element element) {
if (_registry[this] == element) _registry.remove(this); // 移除注册
}
Entonces, GlobalKey
¿cómo se usa? En el Listado de Código 5-8, la lógica que se activará cuando se analice Widget
y cree uno nuevo , la lógica completa se muestra en el Listado de Código 8-16.Element
GlobalKey
// 代码清单5-8 flutter/packages/flutter/lib/src/widgets/framework.dart
Element inflateWidget(Widget newWidget, dynamic newSlot) {
final Key? key = newWidget.key;
if (key is GlobalKey) {
...... } // 见代码清单8-16
final Element newChild = newWidget.createElement(); // 创建对应的Element
newChild.mount(this, newSlot); // 由对应的Element实例继续子节点的挂载
return newChild;
}
// 代码清单8-16 flutter/packages/flutter/lib/src/widgets/framework.dart
Element inflateWidget(Widget newWidget, dynamic newSlot) {
// 见代码清单5-8
assert(newWidget != null);
final Key? key = newWidget.key;
if (key is GlobalKey) {
// 当前Widget含有配置Key信息
final Element? newChild = _retakeInactiveElement(key, newWidget); // 见代码清单8-17
if (newChild != null) {
// 若能找到Key对应的Element,则复用
newChild._activateWithParent(this, newSlot); // 见代码清单8-19
// 得到目标Element,基于它进行更新
final Element? updatedChild = updateChild(newChild, newWidget, newSlot);
assert(newChild == updatedChild); // 检查确实是同一个Element对象
return updatedChild!;
} // 如果找不到,仍会进入下面的逻辑,新建一个Element节点并挂载
} // if
final Element newChild = newWidget.createElement();
newChild.mount(this, newSlot);
return newChild;
}
Widget
De la lógica anterior se puede ver que cuando existe uno nuevo GlobalKey
, intentará _retakeInactiveElement
obtener su Element
objeto correspondiente y reutilizarlo; de lo contrario, Element
se creará una nueva instancia y se montará en él Element Tree
.
Primero analice Element
la lógica de obtención del objeto, como se muestra en el Listado 8-17.
// 代码清单8-17 flutter/packages/flutter/lib/src/widgets/framework.dart
Element? _retakeInactiveElement(GlobalKey key, Widget newWidget) {
final Element? element = key._currentElement; // 即key._registry[this]
if (element == null) return null;
if (!Widget.canUpdate(element.widget, newWidget)) // 见代码清单5-48
return null; // 正常情况下,Key相同的Widget,其类型应该相同
final Element? parent = element._parent;
if (parent != null) {
// 从原来的位置卸载此Element,即从Element Tree中移除
parent.forgetChild(element); // 登记到_forgottenChildren字段
parent.deactivateChild(element);
}
assert(element._parent == null);
owner!._inactiveElements.remove(element); // 移除,避免被finalizeTree方法清理
return element;
}
// MultiChildRenderObjectElement
void forgetChild(Element child) {
_forgottenChildren.add(child); // 用于代码清单8-21的相关逻辑
super.forgetChild(child);
}
La lógica anterior primero elimina el objeto Key
correspondiente actual Element
y luego lo descarga del nodo original. Generalmente, el nodo reutilizado en la misma ronda del proceso de compilaciónGlobalKey
no se ha atravesado, pero la reutilización ha desencadenado el resultado y finalmente lo elimina de _inactiveElements
la lista Eliminado para evitar ser reciclado durante la fase de limpieza.
Cada vez que se Element Tree
elimina , se agrega a _inactiveElements
la lista, como se muestra en el Listado 8-18.
// 代码清单8-18 flutter/packages/flutter/lib/src/widgets/framework.dart
void deactivateChild(Element child) {
child._parent = null;
child.detachRenderObject(); // 从Render Tree中移除对应节点
owner!._inactiveElements.add(child); // 登记该节点,如果在清理阶段该节点仍在本列表中,则清理释放
}
En el método del Listado de código 8-16 , después de sacar inflateWidget
el objeto reutilizable , se debe volver a montar Esta lógica se implementa a través del método, como se muestra en el Listado de código 8-19.Element
Element Tree
_activateWithParent
// 代码清单8-19 flutter/packages/flutter/lib/src/widgets/framework.dart
void _activateWithParent(Element parent, dynamic newSlot) {
assert(_lifecycleState == _ElementLifecycle.inactive); // 状态检查,只有inactive节点才会触发
_parent = parent; // 更新相关成员字段
_updateDepth(_parent!.depth);
_activateRecursively(this); // 递归调用每个Element子节点的activate方法
attachRenderObject(newSlot); // 更新Render Tree
assert(_lifecycleState == _ElementLifecycle.active); // 状态检查
}
static void _activateRecursively(Element element) {
assert(element._lifecycleState == _ElementLifecycle.inactive);
element.activate(); // 见代码清单8-20,此时会触发_lifecycleState的更新
assert(element._lifecycleState == _ElementLifecycle.active);
element.visitChildren(_activateRecursively);
}
La lógica anterior es principalmente para inicializar Element
los campos relevantes del nodo actual y dejar que se correspondan con Element Tree
la nueva posición en el nodo. Finalmente, el método de cada nodo hijo se llama recursivamente activate
, como se muestra en el Listado 8-20.
// 代码清单8-20 flutter/packages/flutter/lib/src/widgets/framework.dart
void activate() {
final bool hadDependencies = // 是否存在依赖,详见8.2节
(_dependencies != null && _dependencies!.isNotEmpty) || _hadUnsatisfied
Dependencies;
_lifecycleState = _ElementLifecycle.active; // 更新状态
_dependencies?.clear(); // 清理原来的依赖
_hadUnsatisfiedDependencies = false;
_updateInheritance(); // 更新可用依赖集合,见代码清单8-14
if (_dirty) owner!.scheduleBuildFor(this); // 如有必要,请求刷新
if (hadDependencies) didChangeDependencies(); // 通知依赖发生变化,见代码清单8-13
}
_hadUnsatisfiedDependencies
El campo indica que la dependencia actual no ha sido procesada porque no se encuentra el tipo correspondiente InheritedElement
. Cuando Element
se vuelve a montar en Element Tree
, si hay un cambio de dependencia, la devolución de llamada del ciclo de vida correspondiente que se activará eventualmente se llamará didChangeDependencies
.StatefulElement
State
Cuando Element
el nodo esté completamente desinstalado, como se muestra en el Listado 8-8, GlobalKey
se realizará la limpieza.
// 代码清单8-8 flutter/packages/flutter/lib/src/widgets/framework.dart
// StatefulElement
void unmount() {
super.unmount();
state.dispose(); // 触发dispose回调
state._element = null;
}
void unmount() {
// Element
final Key? key = _widget.key;
if (key is GlobalKey) {
key._unregister(this); // 取消key的注册
}
_lifecycleState = _ElementLifecycle.defunct;
}
Análisis del código fuente de LocalKey
En comparación con GlobalKey
, el alcance del efecto es solo entre los nodos secundarios del LocalKey
mismo nodo, por lo que su lógica es más oscura. Element
No GlobalKey
existe "descaradamente" en el proceso de compilación de esa manera. Dado que LocalKey
el alcance de la acción es cada subnodo debajo del nodo, su lógica debe estar relacionada con MultiChildRenderObjectElement
esta Element
subclase y MultiChildRenderObjectElement
la lógica de actualización del subnodo se muestra en el Listado 8-21.
// 代码清单8-21 flutter/packages/flutter/lib/src/widgets/framework.dart
// MultiChildRenderObjectElement
void update(MultiChildRenderObjectWidget newWidget) {
super.update(newWidget); // 见代码清单5-49
assert(widget == newWidget);
_children = updateChildren(_children, widget.children, forgottenChildren:
_forgottenChildren);
_forgottenChildren.clear(); // 本次更新结束,重置
}
RenderObjectElement
El método llamado principalmente por la lógica anterior updateChildren
se muestra en el Listado 8-22. Entre ellos, _forgottenChildren
el campo representa el nodo que se excluye de la reutilización porque se GlobalKey
usa , y la lógica de registro de la lista se implementa en el método de la lista de códigos 8-17 .LocalKey
_forgottenChildren
forgetChild
MultiChildRenderObjectElement
updateChildren
El método iniciará la lógica de actualización del nodo secundario real, como se muestra en el Listado 8-22.
// 代码清单8-22 flutter/packages/flutter/lib/src/widgets/framework.dart
List<Element> updateChildren(List<Element> oldChildren,
List<Widget> newWidgets, {
Set<Element>? forgottenChildren }) {
Element? replaceWithNullIfForgotten(Element child) {
// 被GlobalKey索引的节点返回null
return forgottenChildren != null && forgottenChildren.contains(child) ? null : child;
} // GlobalKey的优先级高于LocalKey,所以这里返回null,避免在两处复用
int newChildrenTop = 0; // 新Element列表的头部索引
int oldChildrenTop = 0; // 旧Element列表的头部索引
int newChildrenBottom = newWidgets.length - 1; // 新Element列表的尾部索引
int oldChildrenBottom = oldChildren.length - 1; // 旧Element列表的尾部索引
final List<Element> newChildren = oldChildren.length == newWidgets.length ?
oldChildren : List<Element>.filled(newWidgets.length, _NullElement.instance,
growable: false);
Element? previousChild; // 见代码清单 8-23 ~ 代码清单8-27
return newChildren;
}
updateChildren
La principal responsabilidad de es actualizar el subárbol del nodo actual de acuerdo con la Element
lista anterior de hijos oldChildren
y la nueva Widget
lista de hijos , es decir . Cuando los números finales de los nodos secundarios antiguo y nuevo sean iguales, se actualizará directamente en función de la lista original; de lo contrario, se creará una nueva lista. La razón por la cual la lista original solo se reutiliza cuando las longitudes son iguales aquí se debe principalmente a que el mecanismo de actualización del algoritmo no es adecuado para tratar situaciones de longitudes desiguales En lugar de aumentar la complejidad de la lógica, es mejor crear una nueva lista directamente. A continuación se toma el proceso de la Figura 8-4 como un ejemplo para analizar en detalle.Para la conveniencia de la demostración, aunque la longitud de las listas vieja y nueva es la misma, todavía se muestran por separado.newWidgets
Element
newChildren
La lógica de actualización del método de análisis formal a continuación updateChildren
, la primera etapa en la Figura 8-4 se muestra en el Listado 8-23.
// 代码清单8-23 flutter/packages/flutter/lib/src/widgets/framework.dart
// 更新两个列表的头部索引和尾部索引,分别定位到第1个不可复用的Element节点
while ((oldChildrenTop <= oldChildrenBottom) && (newChildrenTop <=
newChildrenBottom)) {
final Element? oldChild = replaceWithNullIfForgotten(oldChildren[oldChildrenTop]);
final Widget newWidget = newWidgets[newChildrenTop];
assert(oldChild == null || oldChild._lifecycleState == _ElementLifecycle.active);
if (oldChild == null || !Widget.canUpdate(oldChild.widget, newWidget)) break;
final Element newChild = // 完成Element节点的更新
updateChild(oldChild, newWidget, IndexedSlot<Element?>(newChildrenTop,
previousChild))!;
assert(newChild._lifecycleState == _ElementLifecycle.active);
newChildren[newChildrenTop] = newChild; // 加入newChildren列表
previousChild = newChild;
newChildrenTop += 1;
oldChildrenTop += 1; // 处理下一个
}
// 更新尾部索引,但是不加入newChildren列表,逻辑大致同上
while ((oldChildrenTop <= oldChildrenBottom) && (newChildrenTop <=
newChildrenBottom)) {
final Element? oldChild = replaceWithNullIfForgotten(oldChildren
[oldChildrenBottom]);
final Widget newWidget = newWidgets[newChildrenBottom];
assert(oldChild == null || oldChild._lifecycleState == _ElementLifecycle.active);
if (oldChild == null || !Widget.canUpdate(oldChild.widget, newWidget)) break;
oldChildrenBottom -= 1;
newChildrenBottom -= 1; // 只更新索引
}
En la primera etapa, los punteros de cabeza de las listas antigua y nueva se escanearán sincrónicamente, y Widget
la actualización se puede completar directamente en función del nodo actualizado; el índice de cola se escanea de la misma manera, pero no se actualizará directamente, pero sólo registrar la posición. La razón para no actualizar directamente aquí es garantizar el orden de ejecución, de lo contrario, se volverá muy incontrolable en escenarios como la salida de registros.
Después de la primera etapa, los nodos restantes no escaneados no pueden corresponder en orden. En la Fase 2 que se muestra en la Figura 8-4, estos nodos se escanean y se registran LocalKey
los nodos disponibles , como se muestra en el Listado 8-24.
// 代码清单8-24 flutter/packages/flutter/lib/src/widgets/framework.dart
// Scan the old children in the middle of the list.
final bool haveOldChildren = oldChildrenTop <= oldChildrenBottom;
Map<Key, Element>? oldKeyedChildren;
if (haveOldChildren) {
oldKeyedChildren = <Key, Element>{
};
while (oldChildrenTop <= oldChildrenBottom) {
// 开始扫描oldChildren的剩余节点
final Element? oldChild = replaceWithNullIfForgotten(oldChildren[oldChildrenTop]);
assert(oldChild == null || oldChild._lifecycleState == _ElementLifecycle.active);
if (oldChild != null) {
// 没有被GlobalKey使用
if (oldChild.widget.key != null) // 存在Key
oldKeyedChildren[oldChild.widget.key!] = oldChild; // 记录,以备复用
else
deactivateChild(oldChild); // 直接移出Element Tree
}
oldChildrenTop += 1;
} // while
}
La lógica anterior atraviesa oldChildren
los nodos restantes.Si replaceWithNullIfForgotten
el retorno no es null
, significa que no se GlobalKey
usa, por lo que LocalKey
puede agregarlo a su índice temporal oldKeyedChildren
.
Para los elementos restantes actualizados en la tercera etapa que se muestran en la Figura 8-4 , si el índice correspondiente se puede encontrar en newChildren
ellos mismos , se reutilizarán directamente, como se muestra en la lista de códigos 8-25.Key
oldKeyedChildren
// 代码清单8-25 flutter/packages/flutter/lib/src/widgets/framework.dart
while (newChildrenTop <= newChildrenBottom) {
// 还有Widget节点未处理
Element? oldChild;
final Widget newWidget = newWidgets[newChildrenTop];
if (haveOldChildren) {
// 存在可复用的Element节点
final Key? key = newWidget.key;
if (key != null) {
oldChild = oldKeyedChildren![key];
if (oldChild != null) {
if (Widget.canUpdate(oldChild.widget, newWidget)) {
oldKeyedChildren.remove(key);
} else {
// 无法基于新的Widget进行更新,放弃复用
oldChild = null;
}
}
} // if
} // if
assert(oldChild == null || Widget.canUpdate(oldChild.widget, newWidget));
final Element newChild = // 计算新的Element节点,见代码清单5-7和代码清单5-47
updateChild(oldChild, newWidget, IndexedSlot<Element?>(newChildrenTop,
previousChild))!;
newChildren[newChildrenTop] = newChild;
previousChild = newChild;
newChildrenTop += 1;
} // while
La lógica anterior es principalmente para actualizar los nodos newChildren
en el medio , y estos nodos se reutilizarán primero.Element
LocalKey
En las etapas cuarta y quinta que se muestran en la Figura 8-4, la posición del índice de cola se restablece y se completa la actualización de los nodos restantes, como se muestra en el Listado 8-26.
// 代码清单8-26 flutter/packages/flutter/lib/src/widgets/framework.dart
assert(oldChildrenTop == oldChildrenBottom + 1); // 检查索引位置
assert(newChildrenTop == newChildrenBottom + 1);
assert(newWidgets.length - newChildrenTop == oldChildren.length - oldChildrenTop);
newChildrenBottom = newWidgets.length - 1; // 重置尾部索引,以便更新
oldChildrenBottom = oldChildren.length - 1;
// 开始更新newChildren的尾部,代码清单8-23中已经确认过可复用
while ((oldChildrenTop <= oldChildrenBottom) && (newChildrenTop <=
newChildrenBottom)) {
final Element oldChild = oldChildren[oldChildrenTop];
final Widget newWidget = newWidgets[newChildrenTop];
final Element newChild = // 更新Element节点
updateChild(oldChild, newWidget, IndexedSlot<Element?>(newChildrenTop,
previousChild))!;
newChildren[newChildrenTop] = newChild;
previousChild = newChild;
newChildrenTop += 1;
oldChildrenTop += 1;
}
Hasta el momento, Element
se ha generado un nuevo subárbol, pero oldKeyedChildren
es posible que todavía falten Key
elementos que deben liberarse, como se muestra en la lista de códigos 8-27.
// 代码清单8-27 flutter/packages/flutter/lib/src/widgets/framework.dart
if (haveOldChildren && oldKeyedChildren!.isNotEmpty) {
// oldKeyedChildren有未被
// 复用的节点
for (final Element oldChild in oldKeyedChildren.values) {
if (forgottenChildren == null || !forgottenChildren.contains(oldChild))
deactivateChild(oldChild); // 彻底移除Element Tree
}
}
Lo anterior es LocalKey
el proceso de acción. No GlobalKey
tiene rastros obvios en el código como ese, pero mejora Element Tree
virtualmente la eficiencia de la actualización.