【问题标题】:setState does not seem to work inside a builder functionsetState 似乎在构建器函数中不起作用
【发布时间】:2019-08-02 07:16:18
【问题描述】:

setState 实际上是如何工作的?

当应该重建的小部件被构建在构建器函数中时,它似乎没有做我期望它做的事情。我目前遇到的问题是 ListView.builder 和 AlertDialog 中的按钮。

这里的按钮之一是“AutoClean”,它会自动从对话框中显示的列表中删除某些项目。

注意:此处的目的是显示确认,其中包含将提交的“工作”列表。这些作业被标记以显示哪些看起来无效。用户可以返回更新参数,也可以按“自动清理”删除无效的参数。

onTap 按钮如下所示:

    GeneralButton(
      color: Colors.yellow,
      label: 'Clear Overdue',
      onTap: () {
        print('Nr of jobs BEFORE: ${jobQueue.length}');

        for (int i = jobQueue.length - 1; i >= 0; i--) {
          print('Checking item at $i');
          Map task = jobQueue[i];
          if (cuttoffTime.isAfter(task['dt'])) {
            print('Removing item $i');
            setState(() {                             // NOT WORKING
              jobQueue = List<Map<String, dynamic>>.from(jobQueue)
                ..removeAt(i);                        // THIS WORKS
            });

          }
        }

        print('Nr of jobs AFTER: ${jobQueue.length}');
        updateTaskListState();                        // NOT WORKING 
        print('New Task-list state: $taskListState');
      },
    ),

其中jobQueue 用作构建ListView 的源。

updateTaskListState 看起来像这样:

  void updateTaskListState() {
    DateTime cuttoffTime = DateTime.now().add(Duration(minutes: 10));
    if (jobQueue.length == 0) {
      setState(() {
        taskListState = TaskListState.empty;
      });
      return;
    }
    bool allDone = true;
    bool foundOverdue = false;
    for (Map task in jobQueue) {
      if (task['result'] == null) allDone = false;
      if (cuttoffTime.isAfter(task['dt'])) foundOverdue = true;
    }
    if (allDone) {
      setState(() {
        taskListState = TaskListState.done;
      });
      return;
    }
    if (foundOverdue) {
      setState(() {
        taskListState = TaskListState.needsCleaning;
      });
      return;
    }
    setState(() {
      taskListState = TaskListState.ready;
    });
  }

TaskListState 只是一个枚举,用于决定作业队列是否准备好提交。

一旦 taskListState 设置为TaskListState.ready,“提交”按钮应该会变为活动状态。 AlertDialog 按钮行使用 taskListState,如下所示:

  Row(
    mainAxisAlignment: MainAxisAlignment.spaceBetween,
    mainAxisSize: MainAxisSize.max,
    children: <Widget>[
      if (taskListState == TaskListState.ready)
        ConfirmButton(
            onTap: (isValid && isOnlineNow)
                ? () {
                  postAllInstructions().then((_) {
                    updateTaskListState();
                    // navigateBack();
                  });
                : null),

从控制台输出中,我可以看到这种情况正在发生,但它不起作用。它似乎与同一问题有关。

当我使用build 内部的一个简单的小部件树构建所有小部件时,我似乎没有遇到这种问题。但在这种情况下,我无法更新对话框的显示以显示没有删除项目的新列表。

这篇文章越来越长,但 AleryDialog 内的 ListView 构建器如下所示:

  Flexible(
    child: ListView.builder(
      itemBuilder: (BuildContext context, int itemIndex) {
        DateTime itemTime = jobQueue[itemIndex]['dt'];
        bool isPastCutoff = itemTime.isBefore(cuttoffTime);
        return Row(
          mainAxisAlignment: MainAxisAlignment.spaceBetween,
          mainAxisSize: MainAxisSize.max,
          children: <Widget>[
            Text(
              userDateFormat.format(itemTime),
              style: TextStyle(
                color:
                    isPastCutoff ? Colors.deepOrangeAccent : Colors.blue,
              ),
            ),
            Icon(
              isPastCutoff ? Icons.warning : Icons.cached,
              color: isPastCutoff ? Colors.red : Colors.green,
            )
          ],
        );
      },
      itemCount: jobQueue.length,
    ),
  ),

但由于带有按钮的 Row() 也不会对 setState 做出反应,我怀疑问题出在构建器函数本身。

FWIW 所有代码,除了像“GeneralButton”这样的一些项目,它只是一个样板小部件,都驻留在屏幕的 State 类中。

我的直觉是,这与jobQueue 没有传递给任何小部件的事实有关。 builder函数引用jobQueue[itemIndex],它直接访问jobQueue属性。

我可能会尝试将 AlertDialog 提取到外部小部件中。这样做意味着它只能访问jobQueue,如果它被传递给Widget的构造函数......

【问题讨论】:

  • 在将 AlertDialog 的 content 提取到一个小部件中后,它得到了 jobQueue 和 ``taskListState` 以及所有回调作为参数,我仍然得到完全相同的行为。

标签: flutter dart


【解决方案1】:

由于您写的是在使用对话框时发生这种情况,这可能是您的问题的原因:

https://api.flutter.dev/flutter/material/showDialog.html

因此,对话框内的 setState 调用不会触发所需的对话框内容 UI 重建。正如 API 中所述,在另一个 context 中实现重建的一种简短而简单的方法是使用 StatefulBuilder 小部件:

showDialog(
    context: context,
    builder: (dialogContext) {
      return StatefulBuilder(
        builder: (stateContext, setInnerState) {
          // return your dialog widget - Rows in ListView in Container
          ...
          // call it directly as part of onTap of a widget of yours or
          // pass the setInnerState down to another widgets
          setInnerState((){
            ...
          })

    }
);

编辑

与编程世界中的几乎所有情况一样,有各种方法来处理setInnerState 调用以更新对话框 UI。这在很大程度上取决于您决定如何管理数据流/管理和逻辑分离的一般方式。例如,我使用您的 GeneralButton 小部件(假设它是 StatefulWidget):

class GeneralButton extends StatefulWidget {
  // all your parameters
  ...
  // your custom onTap you provide as instantiated
  final VoidCallback onTap;

  GeneralButton({..., this.onTap});

  @override
  State<StatefulWidget> createState() => _GeneralButtonState();
}

class _GeneralButtonState extends State<GeneralButton> {
  ...
  @override
  Widget build(BuildContext context) {
    // can be any widget acting as a button - Container, GestureRecognizer...
    return MaterialButton(
      ...
      onTap: {
        // your button logic which has either been provided fully
        // by the onTap parameter or has some fixed code which is
        // being called every time
        ...
        // finally calling the provided onTap function which has the
        // setInnerState call!
        widget.onTap();
      },
    );
  }

如果您的GeneralButton 小部件中没有固定逻辑,您可以编写:onTap: widget.onTap

这将导致您按如下方式使用您的 GeneralButton:

...
GeneralButton(
  ...
  onTap: {
    // the desired actions like provided in your first post
    ...
    // calling setInnerState to trigger the dialog UI rebuild
    setInnerState((){});
  },
)

【讨论】:

  • 好的,我想我应该有 RTFM。我会试试这个,我认为这正是我遇到的问题。
  • 您能否更新您的答案以显示如何为对话框中的按钮提供回调(匿名或作为状态上的方法),这将在完成工作后更新对话框。
  • 我猜我是否向 Dialog 提供了一个在 pageState 中运行的函数的回调,该函数将不起作用。您的 setInnerState 也调用外部状态的 setState。它如何知道要更新对话框 Statefull 小部件中的状态?我越来越倾向于认为我应该 a) 创建一个有状态的小部件“MyDialog”来保存对话框内容。 b) 使用更新状态的方法为其创建一个“控制器”MyDialogController" c) 将此控制器从调用者传递给新的 MyDialog 小部件。d) 使用父级中的控制器来更新对话框。
  • 另外你的 setInnerState -> setState 在内部函数中没有任何内容,它只是一个匿名的默认无操作函数。我经常看到人们即使使用 setState 也会这样做,但 Flutter 文档说要更新此函数内部的 State 属性。为什么/如何/何时这很重要?
  • 选项 a 非常可行,并且还可以提取代码(但显然只有在对话内容实际上很大时才需要) - setInnerState 不会调用外部上下文的 setState。 StatefulBuilder 通过 builder 函数为我们提供了自己的 setState(我只是将其命名为 setInnerState 以便能够区分)。是的,您实际上不需要布局 setState 调用,只需像我在最后所做的那样调用它,因为它仅用于触发重建
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2014-03-20
  • 2018-08-11
  • 2015-06-29
  • 2022-12-03
  • 2010-11-03
  • 2013-06-19
  • 1970-01-01
相关资源
最近更新 更多