Flutter - 如何更新用于构建 ListView 的 Future/List 的状态(或值?)(通过 FutureBuilder)

Posted

技术标签:

【中文标题】Flutter - 如何更新用于构建 ListView 的 Future/List 的状态(或值?)(通过 FutureBuilder)【英文标题】:Flutter - How to update state (or value?) of a Future/List used to build ListView (via FutureBuilder) 【发布时间】:2019-12-05 01:08:08 【问题描述】:

我在下面粘贴相关代码,但您可以根据我的伪解释来回答。

我正在使用 FutureBuilder 来构建列表视图。

我首先使用 init() 来异步 HTTP 调用 API,并将其解析为映射为表示 json 结果的对象列表(位置)。 然后将位置列表返回到Future<List<Location>> _listFuture 变量(这是 FutureBuilder 的未来)。 一旦未来“返回”或“完成”,FutureBuilder 就会启动并使用 ListView.builder/Container/ListTile 循环并构建列表。 在某些时候,我需要一个 onTap() 处理程序(在 ListTile 中),它可以更改所选列表项的背景颜色。 为了支持这一点,我在 Location 类(保存 JSON 响应)中有一个 backgroundColor 成员,我默认为所有项目设置为“#fc7303”(假设所有项目最初始终未选中)。然后我想将 onTap() 中选择的任何内容的背景更改为“#34bdeb”。 我假设我可以调用 setState() 来触发刷新,并且在重绘时会注意到/使用新的背景颜色。

问题是 ListView/Contrainer/ListTile 是由一个驱动的

Future<List<Location>>

。我可以将“点击”索引传递给我的 ontap 处理程序,但我不相信我可以让我的 _changeBackground() 只更新所选索引的 backgroundColor 值并调用 setState() 因为你不能直接访问/更新这样的未来(我收到错误ERROR: The operator '[]' isn't defined for the class 'Future&lt;List&lt;Location&gt;&gt;'.)

我不确定我是否采取了正确的方法。在这种情况下,我想我总是可以在理论上将“背景”颜色跟踪分离到一个新的单独列表中(在未来之外),并使用来自 onTap() 的对齐索引以这种方式跟踪/引用它。

但是,我不确定这是否总是有效。将来,我可能需要实际更改将来返回的值/状态。例如,想一想我是否希望能够单击列表项并更新“companyName”。在这种情况下,我将直接更改未来存储的值。我想我可以在技术上将新名称发送到服务器并以这种方式完全刷新列表,但这似乎效率低下(如果他们决定“取消”而不保存更改怎么办?)。

感谢任何帮助。谢谢!

这个类实际上保存了列表的相关数据

// Location
class Location 

  // members
  String locationID;
  String locationName;
  String companyName;
  String backgroundColor = 'fc7303';

  // constructor?
  Location(this.locationID, this.locationName, this.companyName);

  // factory?
  factory Location.fromJson(Map<String, dynamic> json) 
    return Location(
      locationID: json['locationID'],
      locationName: json['locationName'],
      companyName: json['companyName'],
    );

  


此类是具有“结果”(成功/错误)消息的父 json 响应。它将上面的类实例化为一个列表来跟踪实际的公司/位置记录

//jsonResponse
class jsonResponse

  String result;
  String resultMsg;
  List<Location> locations;

  jsonResponse(this.result, this.resultMsg, this.locations);

  factory jsonResponse.fromJson(Map<String, dynamic> parsedJson)

    var list = parsedJson['resultSet'] as List;
    List<Location> locationList = list.map((i) => Location.fromJson(i)).toList();
    return jsonResponse(
        result: parsedJson['result'],
        resultMsg: parsedJson['resultMsg'],
        locations: locationList
    );
  

 // jsonResponse

这里是使用上面的类来解析 API 数据并创建 ListView 的 state 和 stateful 小部件

class locationsApiState extends State<locationsApiWidget> 

  // list to track AJAX results
  Future<List<Location>> _listFuture;

  // init - set initial values
  @override
  void initState() 
    super.initState();
    // initial load
    _listFuture = updateAndGetList();
  

  Future<List<Location>> updateAndGetList() async 

    var response = await http.get("http://XXX.XXX.XXX.XXX/api/listCompanies.php");
    if (response.statusCode == 200) 
      var r1 = json.decode(response.body);
      jsonResponse r = new jsonResponse.fromJson(r1);
      return r.locations;
     else 
      throw Exception('Failed to load internet');
    

  

  _changeBackground(int index)
    print("in changebackground(): $index");       // this works!
    _listFuture[index].backgroundColor = '34bdeb';   // ERROR: The operator '[]' isn't defined for the class 'Future<List<Location>>'.
  

  // build() method
  @override
  Widget build(BuildContext context) 

    return new FutureBuilder<List<Location>>(
        future: _listFuture,
        builder: (context, snapshot)

          if (snapshot.connectionState == ConnectionState.waiting) 
            return new Center(
              child: new CircularProgressIndicator(),
            );
           else if (snapshot.hasError) 
            return new Text('Error: $snapshot.error');
           else 
            final items = snapshot.data;
            return new Scrollbar(
              child: new RefreshIndicator(
                  child: ListView.builder(
                    physics: const AlwaysScrollableScrollPhysics(),
                    //Even if zero elements to update scroll
                    itemCount: items.length,
                    itemBuilder: (context, index) 
                      return
                        Container(
                            color: HexColor(items[index].backgroundColor),
                            child:
                            ListTile(
                              title: Text(items[index].companyName),
                              onTap: () 
                                print("Item at $index is $items[index].companyName");
                                _changeBackground(index);
                                // onTap
                            )
                        );
                    ,
                  ),
                  onRefresh: () 
                    // implement later
                    return;
                   // refreshList,
              ),
            );
          // else
         // builder
    ); // FutureBuilder
   // build
 // locationsApiState class


class locationsApiWidget extends StatefulWidget 
  @override
  locationsApiState createState() => locationsApiState();

帮助类(取自 *** 上的某处)用于将 HEX 转换为整数颜色

class HexColor extends Color 
  static int _getColorFromHex(String hexColor) 
    hexColor = hexColor.toUpperCase().replaceAll("#", "");
    if (hexColor.length == 6) 
      hexColor = "FF" + hexColor;
    
    return int.parse(hexColor, radix: 16);
  

  HexColor(final String hexColor) : super(_getColorFromHex(hexColor));

谢谢!

【问题讨论】:

我已经成功地能够通过使用单独的列表来“改变状态”。我基本上将未来的 API 结果克隆到 FutureBuilder 构建器代码中的本地副本。然后我使用 onTap 处理程序在 setState() 中操作本地副本。完美运行。我觉得数据翻倍很奇怪。如果有人可以验证这种方法或解释正确的方法会很好吗?我之前没有注意到的是我没有直接使用未来来创建列表。无论如何,我正在将它们复制到一个名为 items 的最终变量中。所以我原来的问题是错误的...... 【参考方案1】:

我建议从您的位置类中删除背景颜色,而是在您的州中存储选择的位置。这样,您的位置列表在选择项目时不需要更改。我还将为您的位置项目创建一个 StatelessWidget,它将设置背景颜色,具体取决于它是否被选中。所以:

// for the LocationItem widget callback
typedef void tapLocation(int index);

class locationsApiState extends State<locationsApiWidget> 

  // list to track AJAX results
  Future<List<Location>> _listFuture;
  final var selectedLocationIndices = Set<int>();

  // init - set initial values
  @override
  void initState() 
    super.initState();
    // initial load
    _listFuture = updateAndGetList();
  

  Future<List<Location>> updateAndGetList() async 

    var response = await http.get("http://XXX.XXX.XXX.XXX/api/listCompanies.php");
    if (response.statusCode == 200) 
      var r1 = json.decode(response.body);
      jsonResponse r = new jsonResponse.fromJson(r1);
      return r.locations;
     else 
      throw Exception('Failed to load internet');
    
  

  void _toggleLocation(int index) 
    if (selectedLocationIndices.contains(index))
      selectedLocationIndices.remove(index);
    else
      selectedLocationIndices.add(index);
  

  // build() method
  @override
  Widget build(BuildContext context) 

    return new FutureBuilder<List<Location>>(
        future: _listFuture,
        builder: (context, snapshot)

          if (snapshot.connectionState == ConnectionState.waiting) 
            return new Center(
              child: new CircularProgressIndicator(),
            );
           else if (snapshot.hasError) 
            return new Text('Error: $snapshot.error');
           else 
            final items = snapshot.data;
            return new Scrollbar(
              child: new RefreshIndicator(
                  child: ListView.builder(
                    physics: const AlwaysScrollableScrollPhysics(),
                    //Even if zero elements to update scroll
                    itemCount: items.length,
                    itemBuilder: (context, index) 
                      return LocationItem(
                        isSelected: selectedLocationIndices.contains(index),
                        onTap: () => setState(
                          _toggleLocation(index);
                        )
                      );
                    ,
                  ),
                  onRefresh: () 
                    // implement later
                    return;
                   // refreshList,
              ),
            );
          // else
         // builder
    ); // FutureBuilder
   // build
 // locationsApiState class


class locationsApiWidget extends StatefulWidget 
  @override
  locationsApiState createState() => locationsApiState();

还有项目列表条目:

class LocationItem extends StatelessWidget 

  final bool isSelected;
  final Function tapLocation;

  const LocationItem(@required this.isSelected, @required this.tapLocation, Key key) : super(key: key);

  @override
  Widget build(BuildContext context) 
    return Container(
      color: isSelected ? HexColor('34bdeb') : HexColor('fc7303'),
      child: ListTile(
        title: Text(items[index].companyName),
        onTap: () => tapLocation() // onTap
      )
    );
  

原谅我,我无法编译它,所以我希望它是正确的。但我想你明白了:让 Stateful 小部件分别跟踪选定的位置,并让位置决定在重建时如何呈现自己。

【讨论】:

还有一件事:您可能想记住哪些位置是通过它们的 locationID 而不是它们在未来列表中的索引选择/突出显示的,我这样做只是为了简单起见。 我最终分别使用了您的推荐和跟踪选择/颜色。谢谢! 如果 _toggleLocation 在不调用 setState() 的情况下设置所选项目,这会起作用吗?【参考方案2】:

您可能必须使用 ListviewBuilder 而不是 FutureBuilder。我有一个类似的问题,我必须从 firestore 加载数据,对其进行操作,然后才将其发送到 ListView,因此我无法使用 FutureBuilder。我基本上循环了 QuerySnapShot,在每个文档中进行了适当的更改,然后将其添加到 List 对象(List chatUsersList = List();):

String seenText = "";
chatSnapShot.forEach((doc) 
      if (doc.seen) 
        seenText = "not seen yet";
       else 
        seenText = "seen " + doc.seenDate;
      
      ...
      chatUsersList.add(Users(id, avatar, ..., seenText));
    

然后在 ListViewBuilder 中:

ListView.builder(
  itemBuilder: (context, index)        
    return
   UserTile(uid, chatUsersList[index].peerId,
      chatUsersList[index].avatar,..., chatUsersList[index].seenText);
  ,
  itemCount: chatUsersList.length, ),

然后在 UserTile 中:

class UserTile extends StatelessWidget 
  final String uid;
  final String avatar;
  ...
  final String seenText;

  ContactTile(this.uid, this.avatar, ..., this.seenText);
  @override
  Widget build(BuildContext context) 

    var clr = Colors.blueGrey;
    if (seenText == "not seen yet") clr = Colors.red;
    ...

    return
    ListTile(
      isThreeLine: true,
      leading:
      Container(

        width: 60.0,

        height: 60.0,
        decoration: new BoxDecoration(
          shape: BoxShape.circle,
          image: new DecorationImage(
            fit: BoxFit.cover,
            image: new CachedNetworkImageProvider(avatar),
          ),
        ),
      ),
      title: Row(
        mainAxisSize: MainAxisSize.max,
        mainAxisAlignment: MainAxisAlignment.spaceBetween,
        children: < Widget > [
          Expanded(
            child: Text(
              name, style: TextStyle(fontSize: 18.0, fontWeight: FontWeight.bold, ),
              overflow: TextOverflow.ellipsis,
              maxLines: 1
            )
          ),
          Text(
            timestamp, style: TextStyle(fontSize: 14.0, ),
            overflow: TextOverflow.ellipsis,
            maxLines: 1
          ),
        ],
      ),
      subtitle: Column(
        mainAxisSize: MainAxisSize.min,
        crossAxisAlignment: CrossAxisAlignment.start,
        children: < Widget > [
          Text(sub, style: TextStyle(color: Colors.black87, fontSize: 14.0, ),
            overflow: TextOverflow.ellipsis, ),
          Text(**seenText**, style: TextStyle(color: **clr**, fontSize: 12.0, fontStyle: FontStyle.italic), ),
        ],
      ),
      trailing: Icon(Icons.keyboard_arrow_right),
      onTap: () 
     ...

      );

  

【讨论】:

以上是关于Flutter - 如何更新用于构建 ListView 的 Future/List 的状态(或值?)(通过 FutureBuilder)的主要内容,如果未能解决你的问题,请参考以下文章

在 Flutter 中构建列表项时如何更新 ListView.builder 的 itemCount?

在 Flutter 中,如何确保用于构建 GridView 的数据可用?

Flutter:如何知道列表视图中的项目位置?

Flutter中将ListView Builder的索引增加2

如何设置 Flutter 应用的构建和版本号

在 Android 上构建 Flutter 应用程序时如何修复“依赖失败”