Flutter:setState() 能在 build() 中直接调用吗?

热榜客
• 阅读 1233

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 的运行原理。

点赞
收藏
评论区
推荐文章
皕杰报表之UUID
​在我们用皕杰报表工具设计填报报表时,如何在新增行里自动增加id呢?能新增整数排序id吗?目前可以在新增行里自动增加id,但只能用uuid函数增加UUID编码,不能新增整数排序id。uuid函数说明:获取一个UUID,可以在填报表中用来创建数据ID语法:uuid()或uuid(sep)参数说明:sep布尔值,生成的uuid中是否包含分隔符'',缺省为
顺心 顺心
4年前
可以用Flutter愉快的开发web 了
简单的试了一下,完全用flutter现有的widget进行开发。github上面有说现在是preview版本,有些widget还不能用。但是最终是会支持整个的flutter现有的UI的。跟用flutter开发原生app一样。flutter_web还有很长的一段路要走。希望年底能出个像样的版本。不支持第三方库说明目前pub.da
亚瑟 亚瑟
4年前
Flutter - 深入理解Dart虚拟机启动
基于Flutter1.5,从源码视角来深入剖析引擎启动中的Dart虚拟机启动流程,相关源码目录见文末附录一、概述1.1Dart虚拟机概述Dart虚拟机拥有自己的Isolate,完全由虚拟机自己管理的,Flutter引擎也无法直接访问。Dart的UI相关操作,是由RootIsolate通过Dart的C调用,或者是发送消息通知的方式
Souleigh ✨ Souleigh ✨
4年前
React 灵魂 23 问,你能答对几个?
1、setState是异步还是同步?1.合成事件中是异步2.钩子函数中的是异步3.原生事件中是同步4.setTimeout中是同步相关链接:你真的理解setState吗?:2、聊聊react@16.4的生命周期相关连接:React生命周期我对Reactv16.4生命周期的
Stella981 Stella981
4年前
Flutter获取Build完成状态监听 及每一帧绘制完成的监听
Flutter在Build完成后的监听和每一帧绘制完成后的监听这个是我们监听要用的重要的类WidgetsBinding官方是这么描述它的ThegluebetweenthewidgetslayerandtheFlutterengine.中文的意思是 控件层和Flutter引擎之间的粘合剂。就
Stella981 Stella981
4年前
Flutter开发环境搭建Mac版
由于我公司使用的是Mac电脑,但家里是windows。所以这篇文章没有视频,但我会写的尽量详细。希望你能通过阅读文章,也能在mac上搭建起Flutter环境。照着这篇文章配置时,你最好自备了梯子,否则不保证能顺利完成。系统环境要求因为Flutter是新出的框架,所以对系统还是有一定的要求的。MacOS(64bit)磁
Stella981 Stella981
4年前
Flutter
在Flutter加载网页?也是有WebView的哦,和Android一样1.添加依赖dependencies:flutter\_webview\_plugin:^0.2.122.导入库import'import'package:flutter\_webview\_plugin/flutter\_webview\_plug
Stella981 Stella981
4年前
Flutter BottomSheet底部弹窗效果
BottomSheet是一个从屏幕底部滑起的列表(以显示更多的内容)。你可以调用showBottomSheet()或showModalBottomSheet弹出import'package:flutter/material.dart';import'dart:async';classBottomSheetDem
Stella981 Stella981
4年前
FLutter父子组件通信
本文介绍flutter父子组件数据传递和回调.还是以之前的代码为例Flutter\_DayByDay(https://gitee.com/Royce_he/Flutter_DayByDay)由于之前用ReactNative写项目,顺便对比一下RN父组件直接在xml标签中写属性{值/方法},子组件通过props去取属性和方法
Stella981 Stella981
4年前
34 Flutter仿京东商城项目 用户注册 注册流程 POST发送验证码 倒计时功能 验证验证码
加群_452892873_下载对应34课文件,运行方法,建好项目,直接替换lib目录以下列出的是本课涉及的文件。RegisterFirst.dartimport'package:flutter/material.dart';import'package:flutter_jdshop/services/ScreenA
少湖说 少湖说
1年前
鸿蒙Flutter实战:05-使用第三方插件
鸿蒙Flutter实战:使用第三方插件在鸿蒙Flutter开发中,如果涉及到使用原生功能,就要使用插件。使用插件有两种方式,一种是自己编写原生ArkTS代码,在Dart侧调用。另外一种是使用第三方代码。方式一:编号原生ArkTS代码该方案可以使用Platf