Flutter Notes | Principios básicos de Flutter (7) ¡La clave es la clave!

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!

inserte la descripción de la imagen aquí

【¡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".

inserte la descripción de la imagen aquí

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 FancyButtoncomponente 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
  ];
}

FancyButtonEs muy simple, solo envuelva ElevatedButtonel componente, administrará el color de fondo internamente, para hacer que el FancyButtoncolor de fondo de cada uno sea diferente, aquí use un número aleatorio para obtener un color aleatorio, y utilícelo Mappara almacenar el color en caché, de modo que a continuación tiempo que puede Mapobtener 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 FancyButtonpara la transformación. Agregamos dos botones en la página FancyButton, que se usan para sumar y restar countervalores . 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:

inserte la descripción de la imagen aquí

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:

inserte la descripción de la imagen aquí

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 Treey :State

  • StateLos objetos en realidad son Element Treeadministrados por . (precisamente StatefulElementcreado y mantenido por

  • StateLos objetos son de larga vida . A diferencia de Widget, Widgetno se destruyen y reconstruyen cada vez que se vuelven a renderizar.

  • StateLos objetos son reutilizables .

  • ElementcitadoWidget _ Al mismo tiempo, Stateel objeto también tiene Widgetuna referencia al objeto, pero esta posesión no es permanente.

ElementSimple, porque solo contienen metainformación y referencias a , pero también saben cómo actualizar ellos mismos diferentes referencias Widgetsi cambian.WidgetWidget

inserte la descripción de la imagen aquí
inserte la descripción de la imagen aquí

Cuando Flutter decide buildreconstruir y volver a renderizar al llamar a un método, uno buscará el que está exactamente en la misma ubicación elementque el anterior al que hacía referenciaWidgetWidget .

Luego, decidirá Widgetsi es el mismo (si es el mismo, no necesita hacer nada), Widgetsi algo ha cambiado o si es completamente diferente Widget(si es completamente diferente, debe volver a renderizarse). ).

Pero el problema se Elementbasa en qué juzgar el contenido actualizado, solo miran Widgetalgunas propiedades en:

  • tipo exacto en tiempo de ejecución (runtimeType)

  • uno Widget( keysi lo hay)

De hecho, es la lógica de Flutter ejecutar el método Elementen 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:

inserte la descripción de la imagen aquí

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 FancyButtonson 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, canUpdatese 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 Widgetcolores no están Widgeten la configuración; se almacenan en el Widgetobjeto correspondiente State. ElementApunta a la actualizada Widgety muestra la nueva configuración, pero aún conserva los Stateobjetos originales. Entonces, cuando Elementve 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 Statela correcta que coincide con mi objeto Widget."

inserte la descripción de la imagen aquí

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 keypuede ayudar a Flutter a comprender Widgetcuá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 UniqueKeyusamos 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:

inserte la descripción de la imagen aquí

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:

inserte la descripción de la imagen aquí

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, Elemental 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

  • GlobalKeySe utiliza para administrar el estado y mover widgets en el árbol de widgets . Por ejemplo, puede Widgetusar uno en uno GlobalKeyque mostrará una casilla de verificación y usar eso en varias páginas Widget. Esto keyle dice al marco que use Widgetla 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 es ObjectKeyalgo 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 un idatributo comercial único en la clase comercial que nos devuelve la interfaz de back-end, que se puede usar para crearlo ValueKey<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 un ObjectKeyobjeto literal que se pasa a . Por ejemplo:
    inserte la descripción de la imagen aquí

  • 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 especificar ValueKeyo .ObjectKeyUniqueKey

  • 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

LocalKeyOtra GlobalKeydiferencia 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 Columncuando el dispositivo está en orientación horizontal . RowEl efecto de ejecución es el siguiente:

inserte la descripción de la imagen aquí

Use GlobalKeypuede 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:

inserte la descripción de la imagen aquí

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 GlobalKeymediante la lógica de reutilización (se analizará más adelante).GlobalKeyWidgetElementElement Tree

Use GlobalKey para obtener el objeto State

StatefulWidgetPara obtener el objeto del componente en Flutter State, una forma es usar context.findAncestorStateOfType<T>()el método, que puede widgetbuscar en el árbol desde el nodo actual el objeto StatefulWidgetcorrespondiente Statedel tipo especificado. Por ejemplo, aquí hay SnackBarun 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 StatefulWidgetel estado de es privado (no debe estar expuesto al exterior), entonces no deberíamos obtener directamente su Stateobjeto en el código; si StatefulWidgetse espera que el estado de esté expuesto (generalmente hay algunos métodos de operación de componentes), Podemos ir directamente a conseguir su Stateobjeto.

  • Sin embargo, el método para context.findAncestorStateOfTypeobtener StatefulWidgetel estado de es universal y no podemos especificar StatefulWidgetsi el estado de es privado a nivel gramatical, por lo que existe un acuerdo predeterminado en el desarrollo de Flutter: si StatefulWidgetse va a exponer el estado de, StatefulWidgetse debe proporcionar uno En ofEl método estático se utiliza para obtener su Stateobjeto, y los desarrolladores pueden obtenerlo directamente a través de este método; si Stateno quieren estar expuestos, no ofse 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.ofmé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, snackbarpodemos 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 StatefulWidgetdel 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:StateGlobalKeyglobalKey.currentStateglobalKey.currentWidgetWidgetglobalKey.currentContextcontext

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:

inserte la descripción de la imagen aquí

Análisis de código fuente clave

KeyEs casi omnipresente en el código fuente de Flutter, pero rara vez se involucra en el desarrollo diario. En términos oficiales, Keyel escenario de uso es: debe agregar, eliminar y clasificar una serie de Widgets del mismo tipo y con diferentes estados (Estado) .

KeyPrincipalmente divididas en GlobalKeyy LocalKey, las clases clave y sus relaciones se muestran en la Figura 8-3.

Figura 8-3 Clases de claves clave y sus relaciones

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

GlobalKeyEl 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();  
}

_registerLa lógica de se muestra en el Listado 8-15, es decir, el actual Elementse 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 Widgety cree uno nuevo , la lógica completa se muestra en el Listado de Código 8-16.ElementGlobalKey

// 代码清单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;
}

WidgetDe la lógica anterior se puede ver que cuando existe uno nuevo GlobalKey, intentará _retakeInactiveElementobtener su Elementobjeto correspondiente y reutilizarlo; de lo contrario, Elementse creará una nueva instancia y se montará en él Element Tree.

Primero analice Elementla 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 Keycorrespondiente actual Elementy 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 _inactiveElementsla lista Eliminado para evitar ser reciclado durante la fase de limpieza.

Cada vez que se Element Treeelimina , se agrega a _inactiveElementsla 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 inflateWidgetel 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.ElementElement 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 Elementlos campos relevantes del nodo actual y dejar que se correspondan con Element Treela 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
}

_hadUnsatisfiedDependenciesEl campo indica que la dependencia actual no ha sido procesada porque no se encuentra el tipo correspondiente InheritedElement. Cuando Elementse 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.StatefulElementState

Cuando Elementel nodo esté completamente desinstalado, como se muestra en el Listado 8-8, GlobalKeyse 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 LocalKeymismo nodo, por lo que su lógica es más oscura. ElementNo GlobalKeyexiste "descaradamente" en el proceso de compilación de esa manera. Dado que LocalKeyel alcance de la acción es cada subnodo debajo del nodo, su lógica debe estar relacionada con MultiChildRenderObjectElementesta Elementsubclase y MultiChildRenderObjectElementla 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(); // 本次更新结束,重置
}

RenderObjectElementEl método llamado principalmente por la lógica anterior updateChildrense muestra en el Listado 8-22. Entre ellos, _forgottenChildrenel campo representa el nodo que se excluye de la reutilización porque se GlobalKeyusa , y la lógica de registro de la lista se implementa en el método de la lista de códigos 8-17 .LocalKey_forgottenChildrenforgetChildMultiChildRenderObjectElement

updateChildrenEl 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;
}

updateChildrenLa principal responsabilidad de es actualizar el subárbol del nodo actual de acuerdo con la Elementlista anterior de hijos oldChildreny la nueva Widgetlista 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.newWidgetsElementnewChildren

Figura 8-4 esquema updateChildren

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 Widgetla 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 LocalKeylos 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 oldChildrenlos nodos restantes.Si replaceWithNullIfForgottenel retorno no es null, significa que no se GlobalKeyusa, por lo que LocalKeypuede 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 newChildrenellos mismos , se reutilizarán directamente, como se muestra en la lista de códigos 8-25.KeyoldKeyedChildren

// 代码清单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 newChildrenen el medio , y estos nodos se reutilizarán primero.ElementLocalKey

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, Elementse ha generado un nuevo subárbol, pero oldKeyedChildrenes posible que todavía falten Keyelementos 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 LocalKeyel proceso de acción. No GlobalKeytiene rastros obvios en el código como ese, pero mejora Element Treevirtualmente la eficiencia de la actualización.

Supongo que te gusta

Origin blog.csdn.net/lyabc123456/article/details/130847443
Recomendado
Clasificación