Flutter:setState() 不会触发构建

Flutter: setState() does not trigger a build

我有一个非常简单的(有状态的)小部件,其中包含一个 Text 小部件,它显示列表的长度,该列表是小部件状态的成员变量。

initState() 方法中,我使用 setState() 将列表变量(以前为 null)覆盖为具有四个元素的列表。但是,Text 小部件仍然显示“0”。

我添加的打印暗示尚未触发小部件的重建,尽管我认为这是 setState() 方法的唯一目的。

代码如下:

import 'package:flutter/material.dart';

class Scan extends StatefulWidget {
  @override
  _ScanState createState() => _ScanState();
}

class _ScanState extends State<Scan> {
  List<int> numbers;

  @override
  void initState() {
    super.initState();
    _initializeController();
  }

  @override
  Widget build(BuildContext context) {
    print('Build was scheduled');
    return Center(
      child: Text(
        numbers == null ? '0' : numbers.length.toString()
      )
    );
  }

  Future<List<int>> _getAsyncNumberList() {
    return Future.delayed(Duration(seconds: 5), () => [1, 2, 3, 4]);
  }

  _initializeController() async {
    List<int> newNumbersList = await _getAsyncNumberList();

    print("Number list was updated to list of length ${newNumbersList.length}");

    setState(() {
      numbers = newNumbersList;
    });
  }
}

我的问题:为什么小部件只构建一次?我本希望至少有两个构建,第二个构建是由 setState().

的执行触发的

您的代码在 DartPad.dev

中有效

就copy/paste这个看看吧运行:

import 'package:flutter/material.dart';

final Color darkBlue = Color.fromARGB(255, 18, 32, 47);

void main() {
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      debugShowCheckedModeBanner: false,
      home: Scaffold(
        body: Center(
          child: Scan(),
        ),
      ),
    );
  }
}

class Scan extends StatefulWidget {
  @override
  _ScanState createState() => _ScanState();
}

class _ScanState extends State<Scan> {
  List<int> numbers;

  @override
  void initState() {
    super.initState();
    _initializeController();
  }

  @override
  Widget build(BuildContext context) {
    print('Build was scheduled');
    return Center(
      child: Text(
        numbers == null ? '0' : numbers.length.toString()
      )
    );
  }

  Future<List<int>> _getAsyncNumberList() {
    return Future(() => [1,2,3,4]);
  }

  _initializeController() async {
    List<int> newNumbersList = await _getAsyncNumberList();

    print("Number list was updated to list of length ${newNumbersList.length}");

    setState(() {
      numbers = newNumbersList;
    });
  }
}

我不知道你为什么说你的代码不工作,但在这里你可以看到即使是印刷品也能正常工作。您的示例可能过于简单。如果你给那个 Future 添加一个延迟(这是一个真实的案例场景,导致获取数据并等待它有时需要几秒钟),那么代码确实显示 0.

您的代码现在可以正常工作的原因是 Future return 在构建方法开始呈现 Widgets 之前立即列出列表。这就是为什么首先出现在屏幕上的是 4。

如果将 .delayed() 添加到 Future,那么它确实会停止工作,因为数字列表会在一段时间后检索,并且构建会在数字更新之前呈现。

问题解释

您的代码中的 SetState 未正确调用。你要么这样做(在这种情况下没有意义,因为你使用“等待”,但通常它也有效)

    _initializeController() async {
    setState(() {
    List<int> newNumbersList = await _getAsyncNumberList();

    print("Number list was updated to list of length ${newNumbersList.length}");

    
      numbers = newNumbersList;
    });
  }

或者像这样

    _initializeController() async {
    List<int> newNumbersList = await _getAsyncNumberList();

    print("Number list was updated to list of length ${newNumbersList.length}");

    
    numbers = newNumbersList;
    setState(() {
    /// this thing right here is an entire function. You MUST HAVE THE AWAIT in 
    /// the same function as the update, otherwise, the await is callledn, and on 
    /// another thread, the other functions are executed. In your case, this one 
    /// too. This one finishes early and updates nothing, and the await finishes later.
    });
  }

建议的解决方案

这将在等待 5 秒等待 Future 到 return 包含数据的新列表时显示 0,然后将显示 4。如果您想在等待数据时显示其他内容,请使用一个 FutureBuilder 小部件。

没有 FutureBuilder 的完整代码:

class Scan extends StatefulWidget {
  @override
  _ScanState createState() => _ScanState();
}

class _ScanState extends State<Scan> {
  List<int> numbers;

  @override
  void initState() {
    super.initState();
    _initializeController();
  }

  @override
  Widget build(BuildContext context) {
    print('Build was scheduled');

    return Center(
        child: Text(numbers == null ? '0' : numbers.length.toString()));
  }

  Future<List<int>> _getAsyncNumberList() {
    return Future.delayed(Duration(seconds: 5), () => [1, 2, 3, 4]);
  }

  _initializeController() async {
      List<int> newNumbersList = await _getAsyncNumberList();

      print(
          "Number list was updated to list of length ${newNumbersList.length}");
      numbers = newNumbersList;
      setState(() {});
  }
}

我强烈推荐使用这个版本,因为它在等待数据的整个过程中都会向用户显示一些东西,并且在出现错误时也有故障保护。尝试一下并选择最适合您的,但我再次推荐这个。

FutureBuilder 的完整代码:

class _ScanState extends State<Scan> {
  List<int> numbers;

  @override
  void initState() {
    super.initState();

  }

  @override
  Widget build(BuildContext context) {
    print('Build was scheduled');

    return FutureBuilder(
      future: _getAsyncNumberList(),
      builder: (BuildContext context, AsyncSnapshot<List<int>> snapshot) {
        switch (snapshot.connectionState) {
          case ConnectionState.waiting: return Center(child: Text('Fetching numbers...'));
          default:
            if (snapshot.hasError)
              return Center(child: Text('Error: ${snapshot.error}'));
            else
              /// snapshot.data is the result that the async function returns
              return Center(child: Text('Result: ${snapshot.data.length}'));
        }
      },
    );
  }

  Future<List<int>> _getAsyncNumberList() {
    return Future.delayed(Duration(seconds: 5), () => [1, 2, 3, 4]);
  }
}

是一个更详细的示例,完整解释了 FutureBuilder 的工作原理。花一些时间仔细阅读它。这是 Flutter 提供的一个非常强大的东西。

我有一种感觉,答案没有解决我的问题。我的问题是 为什么小部件只构建一次 以及为什么 setState() 不触发 第二个 build .

像“使用 FutureBuilder”这样的答案没有帮助,因为它们完全绕过了 setState() 的问题。因此,无论异步函数完成多晚,触发重建都应该在执行 setState() 时用新列表更新 UI。

此外,异步函数不会过早完成(在构建完成之前)。我通过尝试 WidgetsBinding.instance.addPostFrameCallback 来确保它不会改变:没有。

我发现问题出在其他地方。在我的 main() 函数中,前两行是:

  SystemChrome.setEnabledSystemUIOverlays([SystemUiOverlay.bottom]);
  SystemChrome.setPreferredOrientations(
    [DeviceOrientation.portraitUp,DeviceOrientation.portraitDown]
  );

这在某种程度上影响了构建顺序。但只在我的华为 P20 Lite 上,没有在我的其他测试设备上,不在模拟器中,也不在 Dartpad 上。

所以结论: 代码很好。我对setState()的理解也还行。我没有为您提供足够的上下文来重现错误。我的解决方案是使 main() 函数中的前两行异步:

void main() async {
  await SystemChrome.setEnabledSystemUIOverlays([SystemUiOverlay.bottom]);
  await SystemChrome.setPreferredOrientations(
    [DeviceOrientation.portraitUp,DeviceOrientation.portraitDown]
  );
  ...
}