关于flutter:FluttersetState-能在-build-中直接调用吗

setState() 能在 build() 中间接调用吗?答案是能也不能。

两种状况

来看一段简略的代码:

import 'package:flutter/material.dart';

class TestPage extends StatefulWidget {
  const TestPage({super.key});

  @override
  State<TestPage> createState() => _State();
}

class _State extends State<TestPage> {
  int _count = 0;

  @override
  Widget build(BuildContext context) {
    setState(() {
      _count++;
    });

    return Scaffold(
      appBar: AppBar(
        title: const Text('测试页面'),
      ),
      body: Center(
        child: Text(
          '$_count',
          style: const TextStyle(fontSize: 24),
        ),
      ),
    );
  }
}

跑起来后代码不会报错,Text(‘$_count’) 显示后果是 1,看来 build() 调用 setState() 没啥问题呀。小改一下,来看看这个:

class _State extends State<TestPage> {
  int _count = 0;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('测试页面'),
      ),
      body: Center(
        child: Builder(
          builder: (context) {
            setState(() {
              _count++;
            });

            return Text(
              '$_count',
              style: const TextStyle(fontSize: 24),
            );
          }
        ),
      ),
    );
  }
}

改变次要是在 Text 下面加了一个 Builder,而后把 setState() 放在了 Builder 的 builder 中去调用。运行起来,后果呈现报错了:The following assertion was thrown building Builder(dirty): setState() or markNeedsBuild() called during build.提醒在 Builder 的 build() 过程中呈现了断言谬误:build() 中不能调用 setState() 或 markNeedsBuild()。

这是什么状况呢,为什么第一种状况下能够在 build() 中调用 setState() 而第二种状况不行?上面来简略地剖析下其中蕴含的原理。

原理剖析

先说一下论断,在 build() 中间接调用 setState() 要满足一个前提条件:

如果以后有组件 A 处于 build() 中,那么 setState() 引起 rebuild 的组件必须是 A 或者 A 的子孙组件,不能是 A 的先人组件。

这是因为组件 build 的程序是从父到子,如果在子组件 build 的过程中执行 setState() 之类会引起父组件的从新 build 那就死循环必定是不行的。

接下来看下 Flutter 源码中是如何判断和管制的。setState() 的外部会调用 _element!.markNeedsBuild()markNeedsBuild() 中有如下代码:

void markNeedsBuild() {
  // ...
  
  // 前半部分,断言从新 build 是否满足下面说的前提。
  assert(() {
    if (owner!._debugBuilding) {
      assert(owner!._debugCurrentBuildTarget != null);
      assert(owner!._debugStateLocked);
      // _debugIsInScope() 用来判断是否满足前提条件。
      if (_debugIsInScope(owner!._debugCurrentBuildTarget!)) {
        return true;
      }
      if (!_debugAllowIgnoredCallsToMarkNeedsBuild) {
        final List<DiagnosticsNode> information = <DiagnosticsNode>[
          ErrorSummary('setState() or markNeedsBuild() called during build.'),
          // ...
        ];
        // ...
      }
      // ...
    }());
    
  // ...
}

markNeedsBuild() 代码的前半部分有断言来解决是否满足下面说到的前提条件,_debugCurrentBuildTarget 就是以后正处于 build 状态的 element。_debugCurrentBuildTarget() 的内容如下:

bool _debugIsInScope(Element target) {
  Element? current = this;
  while (current != null) {
    if (target == current) {
      return true;
    }
    current = current._parent;
  }
  return false;
}

_debugIsInScope() 中的 this 就是调用 setState() 会引起 rebuild 的组件,target 就是以后正处于 build 的组件。其中的 while 循环会逐渐比对 current 及其父组件是否以后 build 的对象,找到了才会返回 true,否则就是 false。如果是 false,则前面的断言就会呈现谬误:setState() or markNeedsBuild() called during build.

如果以后有组件正在 build 那么决不能引起父组件的 rebuild,咱们来看下后面举例报错的第二种状况。Builder 是 TestPage 的子组件,Builder 的 builder 办法里调用的 setState 是 TestPage 上的,也就是在子组件的 build 过程中使父组件 rebuild 了,那么就会引起断言失败;而第一种状况下是在 TestPage 的 build 过程中调用 setState 使本人从新 rebuild,能够满足论断的前提,所以是能够调用的。

这里咱们能够接着想下在第一种状况下,组件本人的 build 过程中调用了 setState 引起了本人从新 rebuild 的时候不是也会死循环了吗?咱们接着看下 markNeedsBuild() 的后半局部代码,如果断言胜利后前面的逻辑:

void markNeedsBuild() {
  // ...
  // 前半部分是断言。
  
  if (dirty) {
    return;
  }
  _dirty = true;
  owner!.scheduleBuildFor(this);
}

这里能够看到组件在 build 过程中 markNeedsBuild() 会使组件变为 dirty 状态,这个时候在 build 中间接调用 setState 后发现曾经是 dirty 状态后会间接返回,而不会调度从新 build,所以就没有问题了。

总结

通过以上的剖析咱们晓得了 Flutter 是如何判断如果在 build 过程中间接调用 setState 是否非法的。当然咱们在写代码的时候是不会在 build() 中间接调用 setState 的,理解以上过程更有助于咱们排查问题和学习 Flutter 的运行原理。

评论

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注

这个站点使用 Akismet 来减少垃圾评论。了解你的评论数据如何被处理