通过异步调用加载初始化数据时,难以使用具有多个 future 的 future 构建器

问题描述 投票:0回答:1

我在尝试在我的主应用程序中实现 futurebuilder 时遇到了一些困难(我已经在其他地方成功地使用了它,但我正在努力弄清楚我正在做的不同事情)。

我已经从一个更大更复杂的应用程序中重新创建了下面的问题,以尝试确定我遇到的问题...如果那里仍然有一些多余的东西,我深表歉意,但因为我不确定是哪一点导致了我我留下的问题。

总之,我正在加载一些 xml 数据,我需要在初始加载中显示某些内容。

如果我将数据加载到 tmp 值中,然后在 setState 中设置状态属性,它可以工作,但我必须为状态属性设置很多任意值,这似乎是错误的。

这是有效的代码版本(好吧,它至少可以运行......事实上,它正在拾取真实事物中的硬编码值,这并不是什么问题,因为我可以将它们设置为我想要的值,并且后续操作可以使一切正确)。

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

class MyApp extends StatefulWidget {
  const MyApp({super.key});
  @override
  _MyAppState createState() => _MyAppState();
}

class _MyAppState extends State<MyApp> {

  final String xmlAssetPath = 'data.xml';

  DataTree dataTree = DataTree(categories: []);
  Category selectedCategory = Category(name: 'not_set', groupNames: []); 
  Group selectedGroup = Group(name: "not_set", items: []);
  String groupName ="Citrus";
  Item selectedItem = Item(name: "Orange");
  int selectedItemIndex = 0;

  void _loadData() async {
    DataTree tmpDataTree = await loadXml(xmlAssetPath);
    debugPrint ("Loading data tree...");
    for (var category in tmpDataTree.categories) {
      debugPrint ("Loading category ${category.name}...");
      if ((category.isDefault ?? false) && (category.defaultGroupName != null)) {
         debugPrint ("Setting groupName to ${category.defaultGroupName}...");
         groupName = category.defaultGroupName!;
       }
    } 
    debugPrint ("Loading selected group $groupName..."); 
    Group tmpSelectedGroup = await loadGroup(xmlAssetPath, groupName); 
    for (var item in tmpSelectedGroup.items) {
      debugPrint ("Loading item ${item.name}...");
      if (item.name == tmpSelectedGroup.defaultItemName) {
         selectedItem = item;
         selectedItemIndex = tmpSelectedGroup.items.indexOf(selectedItem);
      }
    }

    setState(() {
      dataTree = tmpDataTree;
      selectedGroup = tmpSelectedGroup;
    });
  }

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

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: selectedCategory.name,
      home: ListView.builder(
        itemBuilder: (context, index) => Card(
          key: ValueKey(selectedGroup.items[index].name),
          child: ListTile(
            title: Text(selectedGroup.items[index].name),
            subtitle: Text((selectedGroup.items[index].name==selectedItem.name) ? "Selected" : "Not selected"),
          ),
        ),
        itemCount: selectedGroup.items.length,
      ),
    );
  }
}

但理想情况下,我想避免硬编码值,所以我尝试像这样使用 FutureBuilder:

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

class MyApp extends StatefulWidget {
  const MyApp({super.key});
  @override
  _MyAppState createState() => _MyAppState();
}

class _MyAppState extends State<MyApp> {

  final String xmlAssetPath = 'data.xml';

  Future<DataTree> dataTree;
  Category selectedCategory = Category(name: 'not_set', groupNames: []); 
  Future<Group> selectedGroup;
  // Group selectedGroup = Group(name: "not_set", items: []);
  String groupName ="Citrus";
  Item selectedItem = Item(name: "Orange");
  int selectedItemIndex = 0;

  void _loadData() async {
    dataTree = await loadXml(xmlAssetPath); 
    debugPrint ("Loading data tree...");
    for (var category in dataTree.categories) {
      debugPrint ("Loading category ${category.name}...");
      if ((category.isDefault ?? false) && (category.defaultGroupName != null)) {
         debugPrint ("Setting groupName to ${category.defaultGroupName}...");
         groupName = category.defaultGroupName!;
       }
    } 
    debugPrint ("Loading selected group $groupName..."); 
    selectedGroup = await loadGroup(xmlAssetPath, groupName); 
    for (var item in selectedGroup.items) {
      debugPrint ("Loading item ${item.name}...");
      if (item.name == selectedGroup.defaultItemName) {
         selectedItem = item;
         selectedItemIndex = selectedGroup.items.indexOf(selectedItem);
      }
    }
  }

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

  @override
  Widget build(BuildContext context) {
    return FutureBuilder (
        future: Future.wait([dataTree,selectedGroup]),
        builder: (BuildContext context, AsyncSnapshot<List<dynamic>> snapshot) {
          if (snapshot.hasData) {
           return MaterialApp (
             title: snapshot.data![1].name,
             home: ListView.builder(
              itemBuilder: (context, index) => Card(
                key: ValueKey(snapshot.data![1].items[index].name),
                child: ListTile(
                  title: Text(snapshot.data![1].items[index].name),
                  subtitle: Text((snapshot.data![1].items[index].name==selectedItem.name) ? "Selected" : "Not selected"),
                ),
              ),
              itemCount: snapshot.data![1].items.length,
            )
           );
          } else {
            return Container();
          }
        }
    );
  }
}

但我当然明白

lib/my_app_await_not_working.dart:23:16: Error: A value of type 'DataTree' can't be assigned to a variable of type 'Future<DataTree>'.
 - 'DataTree' is from 'package:test_init/load_xml.dart' ('lib/load_xml.dart').
 - 'Future' is from 'dart:async'.
    dataTree = await loadXml(xmlAssetPath); 
...

我从here发现是因为我正在使用await,它阻止了它成为未来。

但是如果我这样做..

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

class MyApp extends StatefulWidget {
  const MyApp({super.key});
  @override
  _MyAppState createState() => _MyAppState();
}

class _MyAppState extends State<MyApp> {

  final String xmlAssetPath = 'data.xml';

  Future<DataTree> dataTree;
  Category selectedCategory = Category(name: 'not_set', groupNames: []); 
  Future<Group> selectedGroup;
  // Group selectedGroup = Group(name: "not_set", items: []);
  String groupName ="Citrus";
  Item selectedItem = Item(name: "Orange");
  int selectedItemIndex = 0;

  void _loadData() {
    dataTree = loadXml(xmlAssetPath); 
    debugPrint ("Loading data tree...");
    for (var category in dataTree.categories) {
      debugPrint ("Loading category ${category.name}...");
      if ((category.isDefault ?? false) && (category.defaultGroupName != null)) {
         debugPrint ("Setting groupName to ${category.defaultGroupName}...");
         groupName = category.defaultGroupName!;
       }
    } 
    debugPrint ("Loading selected group $groupName..."); 
    selectedGroup = loadGroup(xmlAssetPath, groupName); 
    for (var item in selectedGroup.items) {
      debugPrint ("Loading item ${item.name}...");
      if (item.name == selectedGroup.defaultItemName) {
         selectedItem = item;
         selectedItemIndex = selectedGroup.items.indexOf(selectedItem);
      }
    }
  }

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

  @override
  Widget build(BuildContext context) {
    return FutureBuilder (
        future: Future.wait([dataTree,selectedGroup]),
        builder: (BuildContext context, AsyncSnapshot<List<dynamic>> snapshot) {
          if (snapshot.hasData) {
           return MaterialApp (
             title: snapshot.data![1].name,
             home: ListView.builder(
              itemBuilder: (context, index) => Card(
                key: ValueKey(snapshot.data![1].items[index].name),
                child: ListTile(
                  title: Text(snapshot.data![1].items[index].name),
                  subtitle: Text((snapshot.data![1].items[index].name==selectedItem.name) ? "Selected" : "Not selected"),
                ),
              ),
              itemCount: snapshot.data![1].items.length,
            )
           );
          } else {
            return Container();
          }
        }
    );
  }
}

然后我得到类似的东西

ib/my_app_fb_not_working.dart:14:20: Error: Field 'dataTree' should be initialized because its type 'Future<DataTree>' doesn't allow null.
 - 'Future' is from 'dart:async'.
 - 'DataTree' is from 'package:test_init/load_xml.dart' ('lib/load_xml.dart').
  Future<DataTree> dataTree;
                   ^^^^^^^^

在这两种情况下我也会遇到类似的错误。

lib/my_app_fb_not_working.dart:25:35: Error: The getter 'categories' isn't defined for the class 'Future<DataTree>'.
 - 'Future' is from 'dart:async'.
 - 'DataTree' is from 'package:test_init/load_xml.dart' ('lib/load_xml.dart').
Try correcting the name to the name of an existing getter, or defining a getter or field named 'categories'.
    for (var category in dataTree.categories) {
                                  ^^^^^^^^^^

我尝试了很多不同的方法来声明它们,但不知道如何通过 future 获取数据。

作为参考,如果它有助于复制,这里是我正在测试的其他模块(不要太担心低效/错误的 xml 加载,我可以稍后解决。我认为从测试的角度来看,它工作得足够好这个)....

main.dart

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

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

load_xml.dart

import 'package:flutter/services.dart';   
import 'package:flutter/material.dart';   
import 'package:xml/xml.dart' as xml;
import 'dart:async';

class DataTree {
  List<Category> categories = [];
  DataTree({required this.categories}); 
}

class Category {
  final List<String> groupNames; 
  final String name;
  final bool? isDefault;
  final String? defaultGroupName;
  const Category({required this.groupNames, required this.name, this.isDefault, this.defaultGroupName}); 
}

class Group { 
  final List<Item> items;
  final String name;
  final bool? isDefault;
  final String? defaultItemName; 

  const Group({required this.items, required this.name, this.isDefault, this.defaultItemName});
}

class Item { 
  final String name;
  final bool? isDefault;
  const Item({required this.name, this.isDefault});
}

Future<Group> loadGroup(String xmlAssetPath, String groupName) async {

  debugPrint ("Inside load group for $groupName");

  Group group;
  List<Item> itemList = []; 

  String xmlString = await rootBundle.loadString(xmlAssetPath); 
  final document = xml.XmlDocument.parse(xmlString);

  final groups = document.findAllElements('group')              
       .where((group) => group.getAttribute('name') == groupName)
       .toList();

  // do we need these?
  String name='not set';

  bool isGroupDefault = false;
  bool isItemDefault = false;
  String defaultItemName='not set';

  for (var element in groups) {
    String? name = element.getAttribute('name');
    debugPrint("Checking group $name");

    if (name == groupName) { 
      debugPrint("Matches $name");
      name = element.getAttribute('name').toString();
      isGroupDefault = (element.getAttribute('default') == 'true') ? true : false;

      final items = element.findElements('item'); 
      for (var item in items) {
        String name = item.getAttribute('name').toString();
        debugPrint("Parsing item $name");
        isItemDefault = (item.getAttribute('default') == 'true') ? true : false;
          
        if (isItemDefault == true) {
            defaultItemName = name; 
            debugPrint ("Got default item $defaultItemName");     
        } // change this to id at some point

        itemList.add ( Item (
          name: name,
        ));

        isItemDefault = false;
      }
      break;
    }
  }

  group = Group (
      items: itemList,
      name: groupName,
      isDefault: isGroupDefault,
      defaultItemName: defaultItemName,
  );
  return group;
}

Future<DataTree> loadXml(String xmlAssetPath) async { 
  DataTree dataTree;
  List<Category> categoryList = []; 

  String xmlString = await rootBundle.loadString(xmlAssetPath); 

  final document = xml.XmlDocument.parse(xmlString);

  final categories = document.findAllElements('category'); 

  for (var element in categories) {
    List<String> groupNames = []; 
    String categoryName = element.getAttribute('name').toString();

    final groups = element.findElements('group');
    for (var element in groups) { 
      String groupName = element.getAttribute('name').toString();
      groupNames.add(groupName);

    } 
    categoryList.add(Category(
        name: categoryName,
        groupNames: groupNames,
    )); 
  } 
  dataTree = DataTree(categories: categoryList);
  return dataTree;
}

资产/data.xml

<?xml version="1.0" encoding="UTF-8" standalone="no" ?>

<data>
  <category name="Fruit" default="true">
    <group name="Citrus">
      <item name="Orange"/>
      <item name="Lemon" default="true"/>
      <item name="Lime"/>
    </group>
    <group name="Berry" default="true">
      <item name="Strawberry"/>
      <item name="Raspberry" default="true"/>
      <item name="Gooseberry"/>
    </group>
    <group name="Exotic">
      <item name="Pineapple"/>
      <item name="Papaya"/>
      <item name="Mango"/>
    </group>
  </category>
  <category name="Vegetable">
    <group name="Root">
      <item name="Carrot"/>
      <item name="Potato"/>
      <item name="Parsnip"/>
    </group>
    <group name="Brassica">
      <item name="Cabbage"/>
      <item name="Broccoli"/>
      <item name="Kale"/>
    </group>
  </category>
</data>
flutter dart future flutter-futurebuilder
1个回答
0
投票

是时候停止使用 FutureBuilder 并开始使用 Riverpod 了。 Riverpod 可以很好地管理来自异步操作的缓存数据,并将这些数据聚合以获得总体结果。 当我对 Riverpod 提供商感到满意后,我就停止使用 FutureBuilder。

© www.soinside.com 2019 - 2024. All rights reserved.