【问题标题】:Flutter, render widget after async callFlutter,异步调用后渲染小部件
【发布时间】: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() 方法,但小部件没有更新。

【问题讨论】:

标签: async-await dart 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 成员的其他类型,这会更有效,但我仍然不建议这样做。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2018-03-25
  • 2019-10-18
  • 1970-01-01
  • 2020-04-09
  • 1970-01-01
相关资源
最近更新 更多