发生更改时如何在Flutter中重建窗口小部件

时间:2018-06-26 00:00:37

标签: dart flutter

编辑:我已经编辑了以下代码,以介绍用于获取数据的方法以及用于构建列车估算值的小部件(使用"API_URL"替换所有API信息)的方法和"API_STOP_ID")。我希望这能更好地帮助我们解决问题!我真的很感谢任何人都可以提供的任何信息-我一直在为这个项目而努力!再次谢谢大家!

原始帖子: 我有一个ListTiles的ListView,每个列表视图都有一个尾随小部件,该小部件在新的Text小部件中构建火车到达估计。这些尾随的小部件每五秒钟更新一次(由打印语句证明)。当应用程序从火车的API提取数据时,它会填充_buildEstimatesNull()构建的“无数据”文本小部件。

但是,问题是,即使应用程序完成了数据和_isLoading = false的获取(由打印语句证明),仍然没有显示“没有数据”。尽管如此,即使解决了问题,火车的估算值也会很快过时,因为尾随的小部件每隔五秒钟就会更新一次,但这不会反映在实际的应用程序中,因为小部件是基于页面加载构建的。因此,我需要一种在尾随小部件获取新信息时对其进行重建的方法。

是否有办法让Flutter每隔五秒(或每当_buildEstimatesS1更新一次/跟踪小部件的内部更新一次)自动重建ListTile的跟踪小部件?

class ShuttleApp extends StatefulWidget {
  @override
  State<StatefulWidget> createState() {
    return new ShuttleState();
  }
}

class ShuttleState extends State<ShuttleApp> {
  @override
  Widget build(BuildContext context) {
    return new MaterialApp(
      home: new HomeScreen(),
    );
  }
}

class HomeScreen extends StatefulWidget {
  @override
  State<StatefulWidget> createState() {
    return new HomeState();
  }
}

class HomeState extends State<HomeScreen> {

  var _isLoading = true;

  void initState() {
    super.initState();
    _fetchData();
    const fiveSec = const Duration(seconds: 5);
    new Timer.periodic(fiveSec, (Timer t) {
      _fetchData();
    });
  }

  var arrivalsList = new List<ArrivalEstimates>();

  _fetchData() async {
    arrivalsList.clear();
    stopsList.clear();
    final url = "API_URL";
    print("Fetching: " + url);
    final response = await http.get(url);
    final busesJson = json.decode(response.body);
    if (busesJson["service_id"] == null) {
      globals.serviceActive = false;
    } else {
      busesJson["ResultSet"]["Result"].forEach((busJson) {
        if (busJson["arrival_estimates"] != null) {
          busJson["arrival_estimates"].forEach((arrivalJson) {
            globals.serviceActive = true;
            final arrivalEstimate = new ArrivalEstimates(
                arrivalJson["route_id"], arrivalJson["arrival_at"], arrivalJson["stop_id"]
            );
            arrivalsList.add(arrivalEstimate);
          });
        }
      });
    }
    setState(() {
      _isLoading = false;
    });
  }

  Widget _buildEstimateNull() {
    return new Container(
      child: new Center(
        child: new Text("..."),
      ),
    );
  }

  Widget _buildEstimateS1() {
    if (globals.serviceActive == false) {
      print('serviceNotActive');
      _buildEstimateNull();
    } else {
      final String translocStopId = "API_STOP_ID";
      final estimateMatches = new List<String>();
      arrivalsList.forEach((arrival) {
        if (arrival.stopId == translocStopId) {
          estimateMatches.add(arrival.arrivalAt);
        }
      });
      estimateMatches.sort();
      if (estimateMatches.length == 0) {
        print("zero");
        return _buildEstimateNull();
      } else {
        return new Container(
          child: new Center(
            child: new Text(estimateMatches[0]),
          ),
        );
      }
    }
  }

  @override
  Widget build(BuildContext context) {
    return new Scaffold(
        backgroundColor: const Color(0xFF171717),
        appBar: new AppBar(),
        body: new DefaultTextStyle(
          style: new TextStyle(color: const Color(0xFFaaaaaa),),
          child: new ListView(
            children: <Widget>[
              new ListTile(
                title: new Text('S1: Forest Hills',
                    style: new TextStyle(fontWeight: FontWeight.w500, fontSize: 20.0)),
                subtitle: new Text('Orange Line'),
                contentPadding: new EdgeInsets.symmetric(vertical: 16.0, horizontal: 16.0),
                trailing: _isLoading ? _buildEstimateNull() : _buildEstimateS1(),
              ),
            ],
          ),
        )
    );
  }

class ArrivalEstimates {
  final String routeId;
  final String arrivalAt;
  final String stopId;
  ArrivalEstimates(this.routeId, this.arrivalAt, this.stopId);
}

非常感谢您提供的任何帮助!我真的非常感谢! :)

4 个答案:

答案 0 :(得分:3)

有几种方法可以解决此问题。但是,在看不到更多代码的情况下很难告诉正在发生的事情-特别是如何获取数据以及如何处理数据。但我想我还是可以给您一个充分的答案。

执行此操作的简单方法是:

  1. 具有一个StatefulWidget,该控件可跟踪列表中所有项目的构建估算。它应该从您的API请求数据,获取结果,然后调用setState(() => this.listData = data);。调用setState可以告诉窗口小部件需要重建。
  2. 对于列表中的每个项目都有一个StatefulWidget。他们每个人每隔5秒执行一次API请求,获取结果,然后每个人都将调用setState(() => this.itemData = data);。这意味着需要多次调用API等。

#1的优点是您可以批处理API调用,而#2的优点是您构建的总体更改较少(尽管颤动的方式,这将是最小的)...所以我可能如果可能,请选择#1。

但是,有一种更好的方法!

执行此操作的更好方法是使用某种API管理器(或您要调用的任何一种)来处理与API的通信。它可能会在您的窗口小部件树中更高的位置,并且可以使用所需的任何逻辑来启动/停止。根据小部件树的高度,您可以将其传递给每个子级,或者更有可能将其保存在InheritedWidget中,然后将其用于从每个列表元素或整个列表中检索它。

API管理器将提供各种streams-根据您的API提供一堆命名字段/方法,或者使用getStream(id)这种结构。

然后,在各种列表元素中,将使用StreamBuilder小部件基于数据构建每个元素-通过使用StreamBuilder,您将获得一个ConnectionState对象,该对象可让您知道流是否已接收到任何数据但是,您可以选择显示isLoading类型的小部件,而不是显示数据的小部件。

使用这种更高级的方法,您将得到:

  • 可维护性
    • 如果您的api发生了更改,则只需更改API管理器
    • 由于API交互和UI交互是分开的,因此您可以编写更好的测试
  • 可扩展性
    • 如果您以后使用推送通知进行更新,而不是每5秒对服务器进行一次ping操作,则可以将其合并到API管理器中,以便它可以简单地更新流而无需触摸UI

编辑:根据OP的评论,他们已经或多或少地实施了第一条建议。但是,代码存在一些问题。我将在下面列出它们,并在代码中进行了一些更改。

  1. 每次完成新构建时,都应更换arrivalsList而不是简单地对其进行更改。这是因为dart会比较列表,并且如果找到相同的列表,则不一定会比较所有元素。同样,虽然在函数中间进行更改不一定会引起问题,但通常最好使用局部变量,然后在最后更改值。请注意,该成员实际上是在setState中设置的。
  2. 如果serviceActive == false,则返回return _buildEstimateNull();中缺少返回。

代码如下:

class HomeState extends State<HomeScreen> {

  var _isLoading = true;

  void initState() {
    super.initState();
    _fetchData();
    const fiveSec = const Duration(seconds: 5);
    new Timer.periodic(fiveSec, (Timer t) {
      _fetchData();
    });
  }

  var arrivalsList = new List<ArrivalEstimates>();

  _fetchData() async {
    var arrivalsList = new List<ArrivalEstimates>(); // *********** #1
    stopsList.clear();
    final url = "API_URL";
    print("Fetching: " + url);
    final response = await http.get(url);
    final busesJson = json.decode(response.body);
    if (busesJson["service_id"] == null) {
      print("no service id");
      globals.serviceActive = false;
    } else {
      busesJson["ResultSet"]["Result"].forEach((busJson) {
        if (busJson["arrival_estimates"] != null) {
          busJson["arrival_estimates"].forEach((arrivalJson) {
            globals.serviceActive = true;
            final arrivalEstimate = new ArrivalEstimates(
                arrivalJson["route_id"], arrivalJson["arrival_at"], arrivalJson["stop_id"]
            );
            arrivalsList.add(arrivalEstimate);
          });
        }
      });
    }
    setState(() {
      _isLoading = false;
      this.arrivalsList = arrivalsList; // *********** #1
    });
  }

  Widget _buildEstimateNull() {
    return new Container(
      child: new Center(
        child: new Text("..."),
      ),
    );
  }

  Widget _buildEstimateS1() {
    if (globals.serviceActive == false) {
      print('serviceNotActive');
      return _buildEstimateNull();  // ************ #2
    } else {
      final String translocStopId = "API_STOP_ID";
      final estimateMatches = new List<String>();
      print("arrivalsList length: ${arrivalsList.length});
      arrivalsList.forEach((arrival) {
        if (arrival.stopId == translocStopId) {
          print("Estimate match found: ${arrival.stopId}");
          estimateMatches.add(arrival.arrivalAt);
        }
      });
      estimateMatches.sort();
      if (estimateMatches.length == 0) {
        print("zero");
        return _buildEstimateNull();
      } else {
        return new Container(
          child: new Center(
            child: new Text(estimateMatches[0]),
          ),
        );
      }
    }
  }

  @override
  Widget build(BuildContext context) {
    return new Scaffold(
        backgroundColor: const Color(0xFF171717),
        appBar: new AppBar(),
        body: new DefaultTextStyle(
          style: new TextStyle(color: const Color(0xFFaaaaaa),),
          child: new ListView(
            children: <Widget>[
              new ListTile(
                title: new Text('S1: Forest Hills',
                    style: new TextStyle(fontWeight: FontWeight.w500, fontSize: 20.0)),
                subtitle: new Text('Orange Line'),
                contentPadding: new EdgeInsets.symmetric(vertical: 16.0, horizontal: 16.0),
                trailing: _isLoading ? _buildEstimateNull() : _buildEstimateS1(),
              ),
            ],
          ),
        )
    );
  }

答案 1 :(得分:2)

每次清除数据时都创建一个新列表,而不是清除并重新使用arrivalsList。否则Flutter无法检测到列表是否已更改。

此外,无论何时更改列表,只要调用setState,代码就会更清晰。

_fetchData() async {

  final url = "API_URL";
  print("Fetching: " + url);
  final response = await http.get(url);
  final busesJson = json.decode(response.body);
  if (busesJson["service_id"] == null) {
    globals.serviceActive = false;
    setState(() {
      _isLoading = false;
    });
  } else {
    final newArrivalsList = new List<ArrivalEstimates>();
    busesJson["ResultSet"]["Result"].forEach((busJson) {
      if (busJson["arrival_estimates"] != null) {
        busJson["arrival_estimates"].forEach((arrivalJson) {
          globals.serviceActive = true;
          final arrivalEstimate = new ArrivalEstimates(
              arrivalJson["route_id"], arrivalJson["arrival_at"], arrivalJson["stop_id"]
          );
          newArrivalsList.add(arrivalEstimate);
        });
      }
    });
    setState(() {
      arrivalsList = newArrivalsList;
      _isLoading = false;
    });
  }
}

一些注意事项:

我不确定在获取数据之前是否真的要清除列表。如果状态正确更新,则将每5秒闪烁一次。

我不确定是否简化了代码,但是如果网络速度较慢,每五秒钟调用一次_fetchData方法可能会成为问题。

答案 2 :(得分:1)

如果确定每次调用setState()时都希望其子窗口小部件重新生成并且被顽固地拒绝,则可以为其指定一个UniqueKey()。这样可以确保在setState()触发重建操作时,子窗口小部件键将不匹配,将弹出并丢弃旧的窗口小部件,并在窗口小部件树中替换新的窗口小部件。

请注意,这是以与键相反的方式使用键(以减少重建),但是如果超出您的控制范围而无法进行必要的重建,则这是一种简单的内置方法,可以实现预期的目标

以下是Flutter团队成员Emily Fortuna撰写的有关键的非常有用的中型文章:

https://medium.com/flutter/keys-what-are-they-good-for-13cb51742e7d

答案 3 :(得分:0)

我不确定这是否是您想要的,但我可能迟到了,但我相信您可以有效地使用更改通知程序来实现这一目标。基本上,更改通知程序与您支持的 logic() 挂钩,例如 api 数据获取。然后向与更改通知程序提供程序相同类型的更改通知程序注册小部件。如果数据发生变化,将重新构建在更改通知程序中注册的小部件。

例如

// extend the change notifier class
class DataClass extends ChangeNotifier {
....
getData(){
   Response res = get('https://data/endpoint')
notifyListeners()
}


void onChange() {
  notifyListeners();
}
....
}

每次数据发生变化时,您都会调用 notifyListeners() 来触发消费小部件的重建。

使用 changenotifier 注册您的小部件

class View extends StatefulWidget {
 Widget create(BuildContext context) {
  return ChangeNotifierProvider<ModelClass>(
    builder: (context) => DataClass(auth: auth),
    child: Consumer<ModelClass>(
      builder: (context, model, _) => View(model: model),
    ),
  );
}
}

您也可以使用 Consumer 进行相同的操作。从 the Documentation

获取更多信息