【问题标题】:Why does updating a List<StatefulWidget> that is used in a ListView fail to update the display?为什么更新 ListView 中使用的 List<StatefulWidget> 无法更新显示?
【发布时间】:2018-10-17 03:51:17
【问题描述】:

我正在尝试创建一个应用程序来显示搜索结果。我有以下非常简单的StatefulWidget。它有一个状态变量data,在构造函数中传递。

class Entry extends StatefulWidget {
  Entry({Key key, @required this.data}) : super(key: key);

  final String data;

  @override
  _EntryState createState() => _EntryState(data);
}

class _EntryState extends State<Entry> {
  _EntryState(this.data) : super();
  final String data;

  @override
  Widget build(BuildContext context) {
    return Text(data);
  }
}

为了生成随机长度(0-9 之间)的数据集,我使用以下辅助函数,它基本上跟踪 DateTime 和传递给它的字符串:

List<String> render(String searchTerm) {
  final Random rng = new Random();
  final String date = DateTime.now().toString();
  return List<String>.generate(
    rng.nextInt(10),
    (int i) => '$searchTerm $i $date',
  );
}

最后,我的主要小部件就是这样。它使用状态变量_items 来跟踪Entrys 的列表。可以通过搜索栏进行新的查询,这可能会返回一个随机长度的数据集。然后通过ListView 显示该列表。

class MyWidget extends StatefulWidget {
  MyWidget({Key key}) : super(key: key);

  final String title = "Demo";

  @override
  _State createState() => _State();
}

class _State extends State<MyWidget> {
  List<String> _items = [];

  Future<Null> getData(String searchTerm) async {
    final entriesToAdd = render(searchTerm);
    setState(() {
      _items = entriesToAdd;
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
        appBar: AppBar(
          title: TextField(
            onSubmitted: getData,
            decoration:
                InputDecoration(hintText: 'Search', icon: Icon(Icons.search)),
          ),
        ),
        body: Center(
          child: Column(
            mainAxisAlignment: MainAxisAlignment.center,
            children: <Widget>[
              Expanded(
                  child: ListView.builder(
                      itemCount: _items.length,
                      itemBuilder: (context, index) {
                        return Container(
                            padding: EdgeInsets.all(8.0), child: Entry(data: _items[index]));
                      })),
            ],
          ),
        ));
  }
}

所以我的问题是我希望当我通过按钮进行搜索时(即调用getData),显示的列表将使用新查询的结果进行更新,完全覆盖旧查询。这不是发生的事情。显示的列表长度正确,但这些项目通常是各种先前/当前搜索的混合,如下面的屏幕截图所示,它显示了 2 个搜索的混合。但是,当在ListViewitemBuilder 函数中执行打印语句时,_item 变量似乎正在正确更新,并且无论如何不应该真正混合 2 个不同的搜索结果。是什么导致了这种行为?

这是完整的代码:

import 'dart:async';
import 'dart:math';

import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';

void main() => runApp(App());

class App extends StatelessWidget {
  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Demo',
      theme: ThemeData(
        primarySwatch: Colors.green,
      ),
      initialRoute: '/',
      routes: <String, WidgetBuilder>{
        '/': (BuildContext context) => MyWidget(),
      },
    );
  }
}

class MyWidget extends StatefulWidget {
  MyWidget({Key key}) : super(key: key);

  final String title = "Demo";

  @override
  _State createState() => _State();
}

class _State extends State<MyWidget> {
  List<String> _items = [];

  Future<Null> getData(String searchTerm) async {
    final entriesToAdd = render(searchTerm);
    setState(() {
      _items = entriesToAdd;
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
        appBar: AppBar(
          title: TextField(
            onSubmitted: getData,
            decoration:
                InputDecoration(hintText: 'Search', icon: Icon(Icons.search)),
          ),
        ),
        body: Center(
          child: Column(
            mainAxisAlignment: MainAxisAlignment.center,
            children: <Widget>[
              Expanded(
                  child: ListView.builder(
                      itemCount: _items.length,
                      itemBuilder: (context, index) {
                        return Container(
                            padding: EdgeInsets.all(8.0), child: Entry(data: _items[index]));
                      })),
            ],
          ),
        ));
  }
}

class Entry extends StatefulWidget {
  Entry({Key key, @required this.data}) : super(key: key);

  final String data;

  @override
  _EntryState createState() => _EntryState(data);
}

class _EntryState extends State<Entry> {
  _EntryState(this.data) : super();
  final String data;

  @override
  Widget build(BuildContext context) {
    return Text(data);
  }
}


List<String> render(String searchTerm) {
  final Random rng = new Random();
  final String date = DateTime.now().toString();
  return List<String>.generate(
    rng.nextInt(10),
    (int i) => '$searchTerm $i $date',
  );
}

【问题讨论】:

    标签: dart flutter


    【解决方案1】:

    不要缓存小部件!您应该在每次调用 build 时重新构建它们。 (如果您担心这样做效率低下,请不要这样做。这就是 Flutter 的设计方式并对其进行了大量优化以支持这一点。)只需保留字符串列表并每次重新构建文本即可。

    避免只使用静态函数的类。这些可以替换为顶级函数。

    试试这个:

    import 'dart:math';
    
    import 'package:flutter/material.dart';
    import 'package:flutter/rendering.dart';
    
    void main() => runApp(App());
    
    class App extends StatelessWidget {
      @override
      Widget build(BuildContext context) {
        return MaterialApp(
          title: 'Demo',
          theme: ThemeData(
            primarySwatch: Colors.green,
          ),
          initialRoute: '/',
          routes: <String, WidgetBuilder>{
            '/': (BuildContext context) => const MyWidget(),
          },
        );
      }
    }
    
    class MyWidget extends StatefulWidget {
      const MyWidget({Key key}) : super(key: key);
    
      @override
      MyWidgetState createState() => MyWidgetState();
    }
    
    class MyWidgetState extends State<MyWidget> {
      List<String> _items = <String>[];
    
      void getData(String searchTerm) {
        final List<String> newEntries = helper(searchTerm);
        setState(() {
          _items = newEntries;
        });
      }
    
      @override
      Widget build(BuildContext context) {
        return Scaffold(
          appBar: AppBar(
            title: TextField(
              onSubmitted: getData,
              decoration: const InputDecoration(
                hintText: 'Search',
                icon: Icon(Icons.search),
              ),
            ),
          ),
          body: Center(
            child: Column(
              mainAxisAlignment: MainAxisAlignment.center,
              children: <Widget>[
                Expanded(
                  child: ListView.builder(
                    itemCount: _items.length,
                    itemBuilder: (BuildContext context, int index) => Container(
                          padding: const EdgeInsets.all(8.0),
                          child: Text(_items[index]),
                        ),
                  ),
                ),
              ],
            ),
          ),
        );
      }
    }
    
    List<String> helper(String searchTerm) {
      final Random rng = new Random();
      final String date = DateTime.now().toString();
      return List<String>.generate(
        rng.nextInt(10),
        (int i) => '$searchTerm $i $date',
      );
    }
    

    这是一个说明使用StatefulWidget的版本:

    class MyWidget extends StatefulWidget {
      const MyWidget({Key key}) : super(key: key);
    
      @override
      MyWidgetState createState() => MyWidgetState();
    }
    
    class MyWidgetState extends State<MyWidget> {
      List<String> _items = <String>[];
    
      void getData(String searchTerm) {
        final List<String> newEntries = helper(searchTerm);
        setState(() {
          _items = newEntries;
        });
      }
    
      @override
      Widget build(BuildContext context) {
        return Scaffold(
          appBar: AppBar(
            title: TextField(
              onSubmitted: getData,
              decoration: const InputDecoration(
                hintText: 'Search',
                icon: Icon(Icons.search),
              ),
            ),
          ),
          body: Center(
            child: Column(
              mainAxisAlignment: MainAxisAlignment.center,
              children: <Widget>[
                Expanded(
                  child: ListView.builder(
                    itemCount: _items.length,
                    itemBuilder: (BuildContext context, int index) => Container(
                          padding: const EdgeInsets.all(8.0),
                          child: Entry(data: _items[index]),
                        ),
                  ),
                ),
              ],
            ),
          ),
        );
      }
    }
    
    class Entry extends StatefulWidget {
      const Entry({Key key, @required this.data}) : super(key: key);
    
      final String data;
    
      @override
      _EntryState createState() => _EntryState();
    }
    
    class _EntryState extends State<Entry> {
      bool bold = true;
    
      @override
      Widget build(BuildContext context) => GestureDetector(
            onTap: () {
              setState(() {
                bold = !bold;
              });
            },
            child: Text(
              widget.data,
              style: TextStyle(
                fontWeight: bold ? FontWeight.bold : FontWeight.normal,
              ),
            ),
          );
    }
    

    【讨论】:

    • 我实际上并没有尝试缓存小部件。 class Entry 的状态变量可以是String,如果我们在class Entry 的构建函数中创建Text 小部件,也会发生同样的问题。我需要 StatefulWidget class Entry 的原因是因为我需要跟踪它内部的一些内部状态,这根本不会影响这个问题的结果。
    • 最好在您的应用程序状态中保持任何持久状态(如收藏夹、加星标)。您在Entry 中保持的任何状态都应该是临时的,例如与可以重新创建的动画或文本条目有关
    • 小部件没有state 属性。 States 确实有 widget 属性。您可以(在某种程度上)通过将 Key 分配给其小部件来保留对 State 的引用,但如果可以的话,您应该只保留数据本身,如此答案所示。
    • 一个 Flutter-y 状态管理解决方案(如果您的应用程序的大小/复杂性需要它):InheritedModel
    • @RichardHeap 这里支持的唯一解决方案是不使用StatefulWidget,这对我来说似乎不是一个可行的解决方案。将_items 设为List&lt;String&gt;List&lt;Widget&gt; 的选择对最终结果没有任何影响。例如,如果我想使用GestureDetector 来允许每个单独的条目有一个单独的手势指示器,flutter.io/tutorials/interactive 的文档表明这是必需的。
    猜你喜欢
    • 2021-01-26
    • 2021-11-27
    • 1970-01-01
    • 2022-01-05
    • 1970-01-01
    • 2014-01-10
    • 2019-02-12
    • 1970-01-01
    • 2015-06-26
    相关资源
    最近更新 更多