Flutter 笔记 | Flutter 核心原理(七)The key is the key!

题外话

本文这个标题其实让我想起了电影《蝙蝠侠大战超人:正义黎明》中的一句话,在该片中,从未来穿越回来的闪电侠对蝙蝠侠发出警告: It’s Lois! She’s the key!

在这里插入图片描述

【布鲁斯,是露易丝!露易丝是关键!】
【你是对的,你对他的看法一直都是对的……】
【我来得太早了吗?我来得太早了……】
【敬畏他!敬畏他!】
【你一定要找到我们!你必须要找到我们!……】

这是闪电侠借助神速力从未来的某个时间点穿越回来,他是来警告蝙蝠侠的。从前几句话可以推断出,在未来的时间线里,因为露易丝出事了,超人可能成为了某种可怕的存在,正义联盟根本没人能拦得住他,所以闪电侠只能回到过去告诉战友们提前做好准备,不能让超人堕落成这种样子。不料他穿越回来的时候发现蝙蝠侠还不认识他,所以他就知道自己来得太早了(因为这个时间点正义联盟还没有建立)。他知道只有正义联盟有能力保护好超人和他的家人,所以他最后恳求蝙蝠侠一定要找到他们并组建正义联盟。这段剧情应该是改编自漫画和游戏《不义联盟:人间之神》。

在这里插入图片描述

剧情回忆: 在电影《蝙蝠侠大战超人:正义黎明》中,正当蝙蝠侠暴揍大超之时,超人提到了玛莎,他让蝙蝠侠去救玛莎,愤怒的蝙蝠侠质问超人为什么要提起这个名字,这时一旁的超人女友及时赶到,她告诉蝙蝠侠那是他母亲的名字。因为蝙蝠侠的妈妈也叫玛莎,由于蝙蝠侠当年自己还是年幼的时候因为没能救下自己妈妈,而亲眼目睹了自己的母亲玛莎·韦恩在自己面前遭遇劫匪杀害,所以当超人叫蝙蝠侠救玛莎时,就想到了自己妈妈。而这时,蝙蝠侠也想起了之前闪电侠对他的警告,顿时恍然大悟,这才明白过来,自己差点犯下大错,假如他没能救出超人的母亲玛莎,那么之后超人将会黑化,世界就会变成蝙蝠侠之前梦境中的那般由黑化的超人所统治,世上再也无人能够阻止超人。

好了,下面开始进入本文正题。


为什么 key 非常重要

我们先看一个例子,现在有一个自定义的 FancyButton 组件,代码如下:

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 很简单,只是包装了一下ElevatedButton组件,它在内部会自己管理背景色,为了使每个 FancyButton 的背景色不同,这里使用了随机数来获取一个随机的颜色,并且使用Map来缓存颜色,这样下次可直接从Map中获取而不用每次计算。

下面基于 Flutter 默认的计数器示例应用使用上面的 FancyButton 进行改造,我们在页面上添加两个 FancyButton 按钮,点击的时候分别用来加减counter值,另外我们添加一个“Swap”按钮,点击的时候可以交换页面上的两个 FancyButton,页面要实现的静态效果如下:

在这里插入图片描述

实现代码如下:

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"))
        ],
      ),
    );
  }
}

运行后测试效果:

在这里插入图片描述

这时你会发现一个奇怪的现象:当按下交换按钮时,上面两个按钮确实会交换位置,但是只有按钮的文字被交换了,背景色却没有变,而点击按钮时对应的功能正常。也就是说,左边的按钮与交换前的背景颜色相同,即使按钮本身是不同的。这是为什么呢?这明显不是我们期望的,我们期望的是两个按钮可以“真正的”对调位置。但这只换了一半是几个意思?

Element Tree 和 State

在解释这个问题之前我们先来了解一下关于 Element TreeState 的几个概念:

  • State对象实际上是由 Element Tree 管理的。(确切的说是由StatefulElement创建并持有)

  • State 对象是长期存在的。与Widget不同,每当Widget重新渲染时,它们都不会被销毁和重新构建。

  • State 对象是可以被重用的

  • Element 引用了Widget。同时State 对象也会保存对Widget的引用,但这种持有不是永久的。

Element很简单,因为它们只包含元信息和对Widget的引用,但它们也知道如果一旦Widget发生更改,自己该如何更新对不同Widget的引用。

在这里插入图片描述
在这里插入图片描述

当Flutter决定在调用build方法进行重建和重新渲染时,一个element将会查找与它引用的前一个Widget完全相同的位置处的Widget

然后,它将决定Widget是否相同(若相同,它不需要做任何事情),或者Widget是否发生了变化,或者它是一个完全不同的Widget(若完全不同,它需要重新被渲染)。

但问题是Element是根据什么来判断更新的内容,它们只查看Widget上的几个属性:

  • 在运行时的确切类型(runtimeType)

  • 一个Widgetkey(如果有的话)

其实也就是 Flutter 执行 Build 重建流程中Element源码中 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;
  }

该方法主要逻辑总结如下:

在这里插入图片描述

其中的Widget.canUpdate()方法的源码如下:

  static bool canUpdate(Widget oldWidget, Widget newWidget) {
   
    return oldWidget.runtimeType == newWidget.runtimeType
        && oldWidget.key == newWidget.key;
  }

本例中交换的两个FancyButton对象是内存地址完全不同的两个实例对象,因此肯定会直接执行上面updateChild() 方法逻辑的情况2,排除情况1。也就是会执行 canUpdate 方法的逻辑判断。

我们知道,Widget Tree 只是 Element Tree 的映射,它只提供描述UI树的配置信息,而在本例中,这些Widget的颜色不在Widget的配置中;它们保存在对应WidgetState对象中。Element指向更新的Widget并显示新的配置,但仍然保留原始State对象。因此,当Element

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

川峰

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值