软件编程
位置:首页>> 软件编程>> Android编程>> Flutter开发setState能否在build中直接调用详解

Flutter开发setState能否在build中直接调用详解

作者:deepfunc  发布时间:2022-05-17 14:55:27 

标签:Flutter,setState,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&lt;TestPage&gt; {
 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&lt;DiagnosticsNode&gt; information = &lt;DiagnosticsNode&gt;[
         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,所以就没有问题了。

来源:https://juejin.cn/post/7153186376433795108

0
投稿

猜你喜欢

手机版 软件编程 asp之家 www.aspxhome.com