Flutter,异步调用后渲染小部件

Posted

技术标签:

【中文标题】Flutter,异步调用后渲染小部件【英文标题】:Flutter, render widget after async call 【发布时间】:2018-09-30 11:52:06 【问题描述】:

我想渲染一个需要 HTTP 调用来收集数据的小部件。

得到以下代码(简体)

import 'package:flutter/material.dart';
import 'dart:async';
import 'dart:convert';

void main() 
  runApp(new MyApp());


class MyApp extends StatelessWidget 
  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) 
    return new MaterialApp(
      title: 'Flutter demo',
      theme: new ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: new MyHomePage(title: 'async demo'),
    );
  


class MyHomePage extends StatefulWidget 
  MyHomePage(Key key, this.title) : super(key: key);

  final String title;

  @override
  _MyHomePageState createState() => new _MyHomePageState();


class _MyHomePageState extends State<MyHomePage> 

  var asyncWidget;

  @override
  initState() 
    super.initState();

    loadData().then((result) 
      print(result);
      setState(() 
       asyncWidget = result;
      );
    );
  

  loadData() async 
    var widget = new AsyncWidget();
    return widget.build();
  

  @override
  Widget build(BuildContext context) 

    if(asyncWidget == null) 
      return new Scaffold(
        appBar: new AppBar(
          title: new Text("Loading..."),
        ),
      );
     else 
      return new Scaffold(
        appBar: new AppBar(
          title: new Text(widget.title),
        ),
        body: new Center(
          child: this.asyncWidget,
        ),
      );
    
  


class MyRenderer 

  MyRenderer();

  Widget render (List data) 

    List<Widget> renderedWidgets = new List<Widget>();

    data.forEach((element) 
      renderedWidgets.add(new ListTile(
        title: new Text("one element"),
        subtitle: new Text(element.toString()),
      ));
    );
    var lv = new ListView(
      children: renderedWidgets,
    );
    return lv;
  


class MyCollector 

  Future gather() async 

    var response = await // do the http request here;

    return response.body;
  


class AsyncWidget 

  MyCollector collector;
  MyRenderer renderer;

  AsyncWidget() 
    this.collector = new MyCollector();
    this.renderer = new MyRenderer();
  

  Widget build() 

    var data = this.collector.gather();
    data.then((response) 
      var responseObject = JSON.decode(response);
      print(response);
      return this.renderer.render(responseObject);
    );
    data.catchError((error) 
      return new Text("Oups");
    );
  

我的代码是这样工作的:使用异步数据的小部件需要一个收集器(进行 http 调用)和一个渲染器,它将使用 http 数据渲染小部件。 我在 initState() 上创建了这个小部件的一个实例,然后进行异步调用。

我发现一些文档说我们应该使用 setState() 方法用新数据更新小部件,但这对我不起作用。

但是,当我放置一些日志时,我看到 HTTP 调用已完成并调用了 setState() 方法,但小部件没有更新。

【问题讨论】:

Firestore async load and populate listview flutter的可能重复 【参考方案1】:

最好的方法是使用FutureBuilder。

来自 FutureBuilder 文档:

new FutureBuilder<String>(
  future: _calculation, // a Future<String> or null
  builder: (BuildContext context, AsyncSnapshot<String> snapshot) 
    switch (snapshot.connectionState) 
      case ConnectionState.none: return new Text('Press button to start');
      case ConnectionState.waiting: return new Text('Awaiting result...');
      default:
        if (snapshot.hasError)
          return new Text('Error: $snapshot.error');
        else
          return new Text('Result: $snapshot.data');
    
  ,
)

另一件事是您在 State.build 方法之外构建小部件并保存小部件本身,这是一种反模式。您实际上应该每次都在 build 方法中构建小部件。

你可以在没有 FutureBuilder 的情况下让它工作,但你应该保存 http 调用的结果(经过适当处理),然后在你的构建函数中使用数据。

请参阅此内容,但请注意,使用 FutureBuilder 是执行此操作的更好方法,我只是提供此内容供您学习。

import 'dart:async';
import 'dart:convert';

import 'package:flutter/material.dart';

void main() 
  runApp(new MyApp());


class MyApp extends StatelessWidget 
  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) 
    return new MaterialApp(
      title: 'Flutter demo',
      theme: new ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: new MyHomePage(title: 'async demo'),
    );
  


class MyHomePage extends StatefulWidget 
  MyHomePage(Key key, this.title) : super(key: key);

  final String title;

  @override
  _MyHomePageState createState() => new _MyHomePageState();


class _MyHomePageState extends State<MyHomePage> 
  List data;

  @override
  initState() 
    super.initState();

    new Future<String>.delayed(new Duration(seconds: 5), () => '["123", "456", "789"]').then((String value) 
      setState(() 
        data = json.decode(value);
      );
    );
  

  @override
  Widget build(BuildContext context) 
    if (data == null) 
      return new Scaffold(
        appBar: new AppBar(
          title: new Text("Loading..."),
        ),
      );
     else 
      return new Scaffold(
        appBar: new AppBar(
          title: new Text(widget.title),
        ),
        body: new Center(
          child: new ListView(
            children: data
                .map((data) => new ListTile(
                      title: new Text("one element"),
                      subtitle: new Text(data),
                    ))
                .toList(),
          ),
        ),
      );
    
  

【讨论】:

我的代码中的 Futures 确实搞砸了,你的例子更简单。谢谢,将来会尝试使用futureBuilder;) 考虑存在网络错误的情况也很重要,例如没有互联网连接,如果 (snapshot?.data?.exception?.clientException 是 NetworkException,您可以在 switch 语句中添加检查) return buildEmptyPlaceHolder(); Future Builder 完成后我们是否可以更改状态(SetState)? 如果你打算这样做,你不妨遵循第二个例子,它只是调用 setState 并使用调用的结果。如果你真的想在 FutureBuilder 中做这件事,你有几个选择;一种是简单地将您想要发生的任何事情添加到 Future 的末尾(即Future(...).then((result) doWhatever(); return result;);。或者您可以在 FutureBuilder 的构建器中执行此操作,但要知道这可以称为多个次。【参考方案2】:

完整示例

Best way在异步调用后使用 FutureBuilder()

class _DemoState extends State<Demo> 
  @override
  Widget build(BuildContext context) 
    return FutureBuilder<String>(
      future: downloadData(), // function where you call your api
      builder: (BuildContext context, AsyncSnapshot<String> snapshot)   // AsyncSnapshot<Your object type>
        if( snapshot.connectionState == ConnectionState.waiting)
            return  Center(child: Text('Please wait its loading...'));
        else
            if (snapshot.hasError)
              return Center(child: Text('Error: $snapshot.error'));
            else
              return Center(child: new Text('$snapshot.data'));  // snapshot.data  :- get your object which is pass from your downloadData() function
        
      ,
    );
  
  Future<String> downloadData()async
    //   var response =  await http.get('https://getProjectList');    
    return Future.value("Data download successfully"); // return your response
  

在future builder中,它调用future函数来等待结果,一旦产生结果,它就会调用我们构建widget的builder函数。

AsyncSnapshot 有 3 个状态: 1. connectionState.none -- 在这种状态下,future 为空 2. connectionState.waiting -- [future] 不为空,但尚未完成 3. connectionState.done -- [future] 不为空,并且已经完成。如果未来成功完成,[AsyncSnapshot.data] 将设置为未来完成的值。如果它以错误完成,[AsyncSnapshot.hasError] 将为真

【讨论】:

这不是违反 FutureBuiler 文档中说的 - 未来必须更早获得,例如在 State.initState、State.didUpdateConfig 或 State.didChangeDependencies 期间。在构造 FutureBuilder 时,不能在 State.build 或 StatelessWidget.build 方法调用期间创建它。如果future与FutureBuilder同时创建,那么每次FutureBuilder的parent重建时,异步任务都会重启。【参考方案3】:

要添加到@rmtmckenzie 接受的答案,处理出现网络错误时的情况很重要,因为snapshot.data 将有数据但错误将在snapshot.data.exception 所以

new FutureBuilder<String>(
future: _calculation, // a Future<String> or null
builder: (BuildContext context, AsyncSnapshot<String> snapshot) 
switch (snapshot.connectionState) 
  case ConnectionState.none: return new Text('Press button to start');
  case ConnectionState.waiting: return new Text('Awaiting result...');
  default:
    if (snapshot.hasError)
      return new Text('Error: $snapshot.error');
       if (snapshot?.data?.exception?.clientException
            is NetworkException) 
         return new 
                Text('Result:$snapshot..data?.exception?.clientException?.message');
        
    else
      return new Text('Result: $snapshot.data');
  
 ,
)

【讨论】:

你在这里做了一些不必要甚至是错误的事情 - 快照不是可选的,所以使用没有意义?在它之后,'data' 在这种情况下应该是一个字符串,所以 data?.exception?实际上是无效的。如果您查看我的答案的第一部分,它已经显示了如何使用snapshot.hasErrorsnapshot.error 正确执行此操作。 我试图处理的是snapshot.hasData 为真而snapshot.hasError 为假但snapshot.data 中有错误的情况,即snapshot.data.exception.clientException 不为空的情况 snapshot.data.exception 无效,因为它是一个字符串,并且字符串没有成员.exception。也许如果您使用的是具有exception 成员的其他类型,这会更有效,但我仍然不建议这样做。

以上是关于Flutter,异步调用后渲染小部件的主要内容,如果未能解决你的问题,请参考以下文章

Flutter:从 UI 调用异步代码的最佳实践

如何测试 initState() 中有异步调用的 Flutter 应用程序?

使用异步数据调用在小部件内绑定“foreach”

当 onDatasetChanged 方法包含异步网络调用 Android 时,小部件 ListView 不刷新

在从 AppWidgetProvider 调用的异步任务中将字符串分配给文本值

同步与异步,阻塞与非阻塞的区别,以及select,poll和epoll