Flutter - setState不更新内部Stateful Widget

70
基本上,我正在尝试制作一个应用程序,其内容将使用异步函数更新,该函数获取来自网站的信息,但是当我尝试设置新状态时,它不会重新加载新内容。如果我调试该应用程序,则会显示当前内容为新内容,但在“重新构建”整个小部件后,它不会显示新信息。 编辑:loadData()方法基本上使用http包读取URL,URL包含一个JSON文件,其内容每5分钟更改一次以获取新闻。例如,带有体育实时记分板的.json文件,其得分总是在变化,因此内容应始终随着新结果而更改。
class mainWidget extends StatefulWidget
{    
  State<StatefulWidget> createState() => new mainWidgetState();
}

class mainWidgetState extends State<mainWidget>
{

  List<Widget> _data;
  Timer timer;

  Widget build(BuildContext context) {
     return new ListView(
              children: _data);
  }

  @override
  void initState() {
    super.initState();
    timer = new Timer.periodic(new Duration(seconds: 2), (Timer timer) async {
      String s = await loadData();
      this.setState(() {
        _data = <Widget> [new childWidget(s)];
      });
      });
  }
}

class childWidget extends StatefulWidget {
  childWidget(String s){
    _title = s;
  }

  Widget _title;

  createState() => new childState();
}

class childState extends State<gameCardS> {

  Widget _title;

  @override
  Widget build(BuildContext context) {
    return new GestureDetector(onTap: foo(),
       child: new Card(child: new Text(_title));

  }

  initState()
  {
    super.initState();
    _title = widget._title;
  }
}

请描述各种移动部件,并提供预计会更改的事物的示例值。我们需要输入和期望输出。 - sorak
我刚刚添加了更多关于问题的信息。 - Jesús Martín
你尝试过在setState中包装_title = widget._title吗? - Robert
@Robert 是的,我必须添加那个带有GestureRecognizer的子StatefulWidget()可以正常工作,问题在于尝试使用新的childWidget实例刷新祖先。 - Jesús Martín
通常我所做的是创建一个Future并使用FutureBuilder,这似乎每次都能为我完成工作。您也可以使用Streams执行相同的操作。 - Robert
10个回答

37

这应该可以解决你的问题。基本上你总是希望在你的build方法层次结构中创建小部件。

import 'dart:async';

import 'package:flutter/material.dart';

void main() => runApp(new MaterialApp(home: new Scaffold(body: new MainWidget())));

class MainWidget extends StatefulWidget {
    @override
    State createState() => new MainWidgetState();
}

class MainWidgetState extends State<MainWidget> {

    List<ItemData> _data = new List();
    Timer timer;

    Widget build(BuildContext context) {
        return new ListView(children: _data.map((item) => new ChildWidget(item)).toList());
    }

    @override
    void initState() {
        super.initState();
        timer = new Timer.periodic(new Duration(seconds: 2), (Timer timer) async {
            ItemData data = await loadData();
            this.setState(() {
                _data = <ItemData>[data];
            });
        });
    }


    @override
    void dispose() {
        super.dispose();
        timer.cancel();
    }

    static int testCount = 0;

    Future<ItemData> loadData() async {
        testCount++;
        return new ItemData("Testing #$testCount");
    }
}

class ChildWidget extends StatefulWidget {

    ItemData _data;

    ChildWidget(ItemData data) {
        _data = data;
    }

    @override
    State<ChildWidget> createState() => new ChildState();
}

class ChildState extends State<ChildWidget> {

    @override
    Widget build(BuildContext context) {
        return new GestureDetector(onTap: () => foo(),
            child: new Padding(
                padding: const EdgeInsets.symmetric(vertical: 12.0, horizontal: 24.0),
                child: new Card(
                    child: new Container(
                        padding: const EdgeInsets.all(8.0),
                        child: new Text(widget._data.title),
                    ),
                ),
            )
        );
    }

    foo() {
        print("Card Tapped: " + widget._data.toString());
    }
}

class ItemData {
    final String title;

    ItemData(this.title);

    @override
    String toString() {
        return 'ItemData{title: $title}';
    }
}

10
@JesúsMartín,我忘了提到如果您有多个相同类型的列表项,则应为每个列表项分配一个Key。有关更多信息,请参阅https://flutter.io/widgets-intro/#keys。 - Simon
有人能真正解释一下他代码中的错误原因吗?我遇到了类似的问题。 - Robison Karls Custódio

32

这真的让我头痛,没有谷歌的结果可用。最终起作用的是如此简单。在您的child build()中,将值分配给局部变量,然后再返回。一旦我这样做了,随后的数据加载就一切正常了。我甚至删除了initState()代码。

非常感谢@Simon。你的答案在某种程度上激发了我尝试这个方法。

在你的childState中:

@override
Widget build(BuildContext context) {
_title = widget._title; // <<< ADDING THIS HERE IS THE FIX
return new GestureDetector(onTap: foo(),
   child: new Card(child: new Text(_title));

}
希望这在你的代码中有效。对我来说,我使用一个 Map 来存储整个传入的 JSON 记录,而不是单个字符串,但这仍然应该可行。

1
谢谢,这对我有用。但我仍然困惑为什么没有那个就不行了。 - Emre Akcan
好的,这解决了问题... 因为这个 bug,我把很多有状态的小部件变成了无状态的...结果发现只需要这个简单的修复就可以了。然而,现在我开始质疑自己。我为什么需要有状态的小部件呢?我感到很困惑... - raiton

27

解释根本问题

  • initState() 方法只在 Widget 被插入到树中时调用一次。因此,当父 Widget 中的变量改变时,子 Widget 的变量将永远不会更新。从技术上讲,Widget 的变量正在改变,但您没有在状态类中捕获该变化。

  • build() 是在 Widget 发生更改时调用的方法。这就是为什么@gregthegeek 的解决方案有效的原因。在子 Widget 的 build 方法中更新变量将确保其从父级获取最新值。

有效

class ChildState extends State<ChildWidget> {
    late String _title;
    @override
    Widget build(BuildContext context) {
        _title = widget._title; // <==== IMPORTANT LINE
        return new GestureDetector(onTap: () => foo(),
            child: new Text(_title),
        );
    }
}

无法工作

(当父级中的 _title 更改时,它不会更新)

class ChildState extends State<ChildWidget> {
    late String _title;

    @override
    void initState() {
      super.initState();
      _title = widget._title; // <==== IMPORTANT LINE
    }

    @override
    Widget build(BuildContext context) {
        return new GestureDetector(onTap: () => foo(),
            child: new Text(_title),
        );
    }
}

3
请确保对层级中的所有儿童执行此操作。 @eriel 的解释很好。 - Paul
非常感谢,这个错误让我浪费了两个小时。 - Alif Irhas

13

调用setState(...)在异步函数中时,我不确定为什么会发生这种情况,但一个简单的解决方案是使用:

我不确定为什么在异步函数中调用setState(...)会出现问题,但一个简单的解决方案是使用:

WidgetsBinding.instance.addPostFrameCallback((_) => setState(...));

不仅仅是使用setState(...)


7
对于任何有兴趣的人:通过这样做,您将在构建之后调用 setState()。 - Paul

8

关于 Child StatefulWidget 不重建的真正问题在于 KEY

嘿,我有点晚了参与讨论,但我认为这很重要。前一段时间我遇到类似的问题,甚至来到这个帖子中获取一些想法。

在我的情况下,我只是直接在 childWidget 的 build 方法中获取 widget.value ,并且它没有在我在 mainWidget 中调用 setState 时更新。

然后我找到了这个视频: https://youtu.be/kn0EOS-ZiIc (When to Use Keys - Flutter Widgets 101 Ep. 4) - 在这里,谷歌开发人员谈论了 Flutter 中的键。

简短的答案是

StatefulWidget 中,您传递的实际值存储在状态中,而不是像 StatelessWidget 一样存储在小部件本身中。

当您在 mainWidget 中调用 setState 时,Flutter 沿着小部件树向下走,并检查每个 childWidget 的类型和键,以查看是否有任何更改。由于有状态的窗口小部件将其值存储在状态中,Flutter 认为子小部件没有更改(因为类型和键相同),即使值已更改,也不会重建它们。

真正的解决方案是给小部件一个包含正在更改的值的键,这样当 Flutter 沿着树向下走时,它会注意到键已更改,并重新构建有状态的小部件。

这里的其他解决方案可能也有效,但如果您想真正理解它,那么观看这个视频是值得的。


1
谢谢,这让我真正理解了它的原因,而不仅仅是修复它。 - user12496215

4

这解决了我的问题... 如果你需要对变量进行初始化赋值,请在initState()中使用它。

注意:我在尝试在build函数中设置初始值时遇到了这个问题。

@override
  void initState() {
    count = widget.initialValue.length; // Initial value
    super.initState();
  }

3
不要在一个将来时态中使用另一个将来时态;使用不同的函数,将每个将来时态单独返回,就像这样:

不要在future中使用future;使用不同的函数,将每个future单独返回,就像这样
 List<Requests> requestsData;
 List<DocumentSnapshot> requestsDocumentData;
 var docId;



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

    getRequestDocs();
  }

  Future<FirebaseUser> getData() {
    var _auth = FirebaseAuth.instance;
    return _auth.currentUser();
  }

  getRequestDocs() {
    getData().then((FirebaseUser user) {
      this.setState(() {
        docId = user.uid;
      });
    });

    FireDb()
        .getDocuments("vendorsrequests")
        .then((List<DocumentSnapshot> documentSnapshots) {
      this.setState(() {
        requestsDocumentData = documentSnapshots;
      });
    });

    for (DocumentSnapshot request in requestsDocumentData) {
      this.setState(() {
        requestsData.add(Requests(
            request.documentID,
            request.data['requests'],
            Icons.data_usage,
            request.data['requests'][0],
            "location",
            "payMessage",
            "budget",
            "tokensRequired",
            "date"));
      });
    }
  }

你可以为每个功能创建单独的函数。
  FireDb().getDocuments("vendorsrequests")
            .then((List<DocumentSnapshot> documentSnapshots) {
          this.setState(() {
            requestsDocumentData = documentSnapshots;
          });
        });

最初的回答
并且
  for (DocumentSnapshot request in requestsDocumentData) {
          this.setState(() {
            requestsData.add(Requests(
                request.documentID,
                request.data['requests'],
                Icons.data_usage,
                request.data['requests'][0],
                "location",
                "payMessage",
                "budget",
                "tokensRequired",
                "date"));
          });
        }

I found that the use of

this

with setState is must


0
在我的情况下,问题就在于在构建方法中将状态定义为类属性而不是局部变量。
这样做的话 -
  List<Task> tasks = [
    Task('Buy milk'),
    Task('Buy eggs'),
    Task('Buy bread'),
  ];
  @override
  Widget build(BuildContext context) {
  
    return ListView.builder(
      itemBuilder: (context, index) => TaskTile(
...

用这个代替 -

 @override
  Widget build(BuildContext context) {
  List<Task> tasks = [
    Task('Buy milk'),
    Task('Buy eggs'),
    Task('Buy bread'),
  ];
  
    return ListView.builder(
      itemBuilder: (context, index) => TaskTile(
...

0

首先检查它是无状态或有状态的小部件,如果该类是无状态的,则将其转换为有状态的小部件,并在关闭setState(() {_myState = newValue;});后尝试添加代码


-3
找到了最好的解决方案。 如果您正在使用无状态小部件,则无法使用set state,因此只需将无状态小部件转换为有状态小部件。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接